#!/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();