finance-tools/scripts/repack-pattern-images.mjs

120 lines
3.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
// 將各書頁圖片整理到 pages/005/img_0_.jpg避免全書共用 3 張圖被覆寫。
import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..', '..');
const PATTERNS_DIR = path.join(ROOT, 'content', 'raw', 'patterns');
const META_PATH = path.join(PATTERNS_DIR, 'metadata.json');
function fileMd5(filePath) {
return crypto.createHash('md5').update(fs.readFileSync(filePath)).digest('hex');
}
function loadSharedHashes() {
const flatDir = path.join(PATTERNS_DIR, 'images');
const hashes = new Map();
if (!fs.existsSync(flatDir)) return hashes;
for (const fname of fs.readdirSync(flatDir)) {
if (!/\.(jpe?g|png|webp)$/i.test(fname)) continue;
hashes.set(fname, fileMd5(path.join(flatDir, fname)));
}
return hashes;
}
function copyIfExists(src, dest, { allowShared = false, sharedHashes = null } = {}) {
if (!fs.existsSync(src)) return false;
const fname = path.basename(dest);
if (!allowShared && sharedHashes?.has(fname) && fileMd5(src) === sharedHashes.get(fname)) {
return false;
}
fs.mkdirSync(path.dirname(dest), { recursive: true });
fs.copyFileSync(src, dest);
return true;
}
function candidateSourceDirs() {
const dirs = [];
if (fs.existsSync(META_PATH)) {
try {
const meta = JSON.parse(fs.readFileSync(META_PATH, 'utf8'));
if (meta.output_dir) {
dirs.push(path.resolve(PATTERNS_DIR, meta.output_dir));
dirs.push(path.resolve(ROOT, meta.output_dir));
}
} catch { /* ignore */ }
}
dirs.push(path.join(PATTERNS_DIR, 'output'));
dirs.push(path.join(ROOT, '短線交易日線圖大全'));
return [...new Set(dirs)].filter((d) => fs.existsSync(d));
}
function imagesFromMd(mdPath) {
const raw = fs.readFileSync(mdPath, 'utf8');
return [...raw.matchAll(/!\[\]\((images\/[^)]+)\)/g)].map((m) => path.basename(m[1]));
}
function main() {
const force = process.argv.includes('--force');
const sources = candidateSourceDirs();
const sharedHashes = loadSharedHashes();
let copied = 0;
let missing = 0;
let skippedShared = 0;
for (let i = 1; i <= 72; i++) {
const pageId = String(i).padStart(3, '0');
const mdPath = path.join(PATTERNS_DIR, `${pageId}.md`);
if (!fs.existsSync(mdPath)) continue;
const names = imagesFromMd(mdPath);
if (!names.length) continue;
const destDir = path.join(PATTERNS_DIR, 'pages', pageId);
fs.mkdirSync(destDir, { recursive: true });
for (const name of names) {
const dest = path.join(destDir, name);
if (fs.existsSync(dest)) {
const isSharedArtifact = sharedHashes.get(name) === fileMd5(dest);
if (isSharedArtifact) fs.unlinkSync(dest);
else if (!force) continue;
}
let hit = false;
for (const srcRoot of sources) {
const tries = [
path.join(srcRoot, pageId, 'images', name),
path.join(srcRoot, `${pageId}.images`, name),
path.join(srcRoot, 'images', pageId, name),
path.join(srcRoot, pageId, name),
];
for (const src of tries) {
if (copyIfExists(src, dest, { sharedHashes })) {
copied += 1;
hit = true;
break;
}
}
if (hit) break;
}
if (!hit) {
// flat images/ 是擷取器跨頁共用的檔案,不能當成該頁教材圖。
skippedShared += 1;
missing += 1;
}
}
}
console.log(`[repack-pattern-images] copied=${copied} missing=${missing} shared_fallback=${skippedShared}`);
if (sources.length) console.log(`來源目錄:${sources.join(', ')}`);
if (missing > 0 || skippedShared > 0) {
console.log('缺少各頁專屬圖片;已略過擷取器產生的全書共用圖,避免教材顯示錯圖。');
}
}
main();