UNPKG

@knennigtri/merge-markdown

Version:

Prepare input files to merge together and optionally output html/pdf/word presentation

370 lines (348 loc) â€ĸ 15.1 kB
"use strict"; function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); } function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); } function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t["return"] || t["return"](); } finally { if (u) throw o; } } }; } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } var fs = require("fs"); var path = require("path"); var yaml = require("js-yaml"); var debug = require("debug")("asset-cleaner"); // Asset detection regex - same as used in merge.js updateAssetRelPaths function var ASSET_REGEX = /(!\[(.*?)\][(](.*?)[)])|(src=[""](.*?)[""])/g; /** * Checks if an asset is referenced in markdown content using the same logic as merge.js * @param {string} content - Markdown content to search * @param {string} assetFile - Asset filename to look for * @param {string} assetsFolderName - Name of the assets folder * @returns {boolean} True if asset is referenced */ function isAssetReferenced(content, assetFile, assetsFolderName) { // Reset regex for each search var regex = new RegExp(ASSET_REGEX.source, ASSET_REGEX.flags); var lines = content.split("\n"); var _iterator = _createForOfIteratorHelper(lines), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var line = _step.value; var match = void 0; while ((match = regex.exec(line)) !== null) { var assetPath = ""; // Extract asset path from match - same logic as merge.js if (match[0].startsWith("![")) { // Markdown image: ![alt](path) assetPath = match[3]; } else if (match[0].startsWith("src=")) { // HTML img: src="path" assetPath = match[5]; } if (assetPath) { // Check if this path references our asset file var fileName = path.basename(assetPath); if (fileName === assetFile) { return true; } // Also check for folder/file pattern if (assetPath.includes("".concat(assetsFolderName, "/").concat(assetFile))) { return true; } } } // Reset regex for next line regex.lastIndex = 0; } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } return false; } /** * Extracts the 'copy-images-to' value from YAML frontmatter in a markdown file * @param {string} mdFilePath - Path to the markdown file * @returns {string|null} The copy-images-to value or null if not found */ function extractAssetsDirFromFrontmatter(mdFilePath) { try { var content = fs.readFileSync(mdFilePath, "utf8"); // Check if file starts with YAML frontmatter (---) if (!content.startsWith("---")) { return null; } // Extract the frontmatter between the first two --- var frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/); if (!frontmatterMatch) { return null; } // Parse the YAML var frontmatter = yaml.load(frontmatterMatch[1]); // Return the copy-images-to value if it exists return frontmatter && frontmatter["copy-images-to"] ? frontmatter["copy-images-to"] : null; } catch (error) { debug("Error reading frontmatter from ".concat(mdFilePath, ": ").concat(error.message)); return null; } } /** * Scans for unused assets in a specific module/directory * @param {Array} mdFiles - Array of markdown file paths * @param {string} assetsDir - Path to assets directory * @param {string} moduleName - Name of the module (for logging) * @param {string} assetsFolderName - Name of the assets folder * @param {boolean} usingFrontmatter - Whether assets dir came from frontmatter * @param {boolean} fixMode - Whether to actually delete files * @param {boolean} verbose - Whether to show detailed output * @returns {Object} Results object with counts and details */ function scanModuleAssets(mdFiles, assetsDir, moduleName, assetsFolderName) { var usingFrontmatter = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false; var fixMode = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : false; var verbose = arguments.length > 6 && arguments[6] !== undefined ? arguments[6] : true; var result = { moduleName: moduleName, assetsDir: assetsDir, assetsFolderName: assetsFolderName, usingFrontmatter: usingFrontmatter, totalAssets: 0, keptAssets: 0, deletedAssets: 0, deletedFiles: [], error: null }; if (!fs.existsSync(assetsDir)) { result.error = "Assets directory not found: ".concat(assetsDir); if (verbose) console.log("\u26A0\uFE0F ".concat(result.error)); return result; } if (mdFiles.length === 0) { result.error = "No markdown files found in: ".concat(moduleName); if (verbose) console.log("\u26A0\uFE0F ".concat(result.error)); return result; } if (verbose) { console.log("\uD83D\uDCC2 Processing: ".concat(moduleName)); console.log(" Found ".concat(mdFiles.length, " markdown file(s)")); if (usingFrontmatter) { console.log(" Using assets folder from frontmatter: '".concat(assetsFolderName, "'")); } } // Read all markdown files var allMdContent = ""; mdFiles.forEach(function (mdFile) { try { var content = fs.readFileSync(mdFile, "utf8"); allMdContent += content + "\n"; } catch (error) { debug("Error reading ".concat(mdFile, ": ").concat(error.message)); } }); // Get all image files var imageExtensions = [".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp", ".bmp"]; var assetFiles = []; try { assetFiles = fs.readdirSync(assetsDir).filter(function (file) { return imageExtensions.includes(path.extname(file).toLowerCase()); }); } catch (error) { result.error = "Error reading assets directory: ".concat(error.message); if (verbose) console.log("\u26A0\uFE0F ".concat(result.error)); return result; } result.totalAssets = assetFiles.length; assetFiles.forEach(function (assetFile) { // Check if asset is referenced in any markdown file using the same regex as merge.js var isReferenced = isAssetReferenced(allMdContent, assetFile, assetsFolderName); if (isReferenced) { result.keptAssets++; } else { if (fixMode) { try { fs.unlinkSync(path.join(assetsDir, assetFile)); result.deletedFiles.push(assetFile); if (verbose) console.log(" \u274C Deleted: ".concat(assetFile)); } catch (error) { debug("Error deleting ".concat(assetFile, ": ").concat(error.message)); } } else { result.deletedFiles.push(assetFile); if (verbose) console.log(" \uD83D\uDD0D Would delete: ".concat(assetFile)); } result.deletedAssets++; } }); if (verbose) { console.log(" \u2705 Kept: ".concat(result.keptAssets, " assets")); console.log(" ".concat(fixMode ? "❌ Deleted" : "🔍 Would delete", ": ").concat(result.deletedAssets, " assets\n")); } return result; } /** * Main function to clean assets in a target directory * @param {string} targetDir - Directory containing subdirectories with markdown files and assets * @param {string} assetsFolderName - Default name of assets folder * @param {boolean} fixMode - Whether to actually delete files * @param {boolean} verbose - Whether to show detailed output * @returns {Object} Summary results */ function cleanAssets(targetDir) { var assetsFolderName = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "assets"; var fixMode = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; var verbose = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true; var summary = { targetDir: targetDir, assetsFolderName: assetsFolderName, fixMode: fixMode, totalModules: 0, processedModules: 0, totalUnusedAssets: 0, results: [] }; if (verbose) { console.log("=========================================="); console.log(" Cleaning Unreferenced Assets"); console.log("==========================================\n"); if (!fixMode) { console.log("🔍 PREVIEW MODE - No files will be deleted (use --fix to delete)\n"); } else { console.log("âš ī¸ FIX MODE - Unreferenced assets will be deleted\n"); } console.log("\uD83D\uDCC1 Assets folder name (default): '".concat(assetsFolderName, "'")); console.log("\uD83D\uDCC1 Target directory: ".concat(targetDir, "\n")); } if (!fs.existsSync(targetDir)) { var error = "Target directory does not exist: ".concat(targetDir); if (verbose) console.error("\u274C Error: ".concat(error)); summary.error = error; return summary; } var subdirs = []; try { subdirs = fs.readdirSync(targetDir).filter(function (item) { return fs.statSync(path.join(targetDir, item)).isDirectory(); }); } catch (error) { var errorMsg = "Error reading target directory: ".concat(error.message); if (verbose) console.error("\u274C Error: ".concat(errorMsg)); summary.error = errorMsg; return summary; } if (subdirs.length === 0) { if (verbose) console.log("âš ī¸ No subdirectories found in target directory"); return summary; } summary.totalModules = subdirs.length; subdirs.forEach(function (subdirName) { var subdirPath = path.join(targetDir, subdirName); // Find all markdown files in this subdirectory var mdFiles = []; try { var files = fs.readdirSync(subdirPath); mdFiles = files.filter(function (f) { return f.endsWith(".md"); }).map(function (f) { return path.join(subdirPath, f); }); } catch (error) { if (verbose) console.log("\u26A0\uFE0F Error reading directory: ".concat(subdirPath)); return; } // Determine assets folder name - check frontmatter first (takes precedence) var moduleAssetsFolderName = assetsFolderName; var usingFrontmatter = false; var _iterator2 = _createForOfIteratorHelper(mdFiles), _step2; try { for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { var mdFile = _step2.value; var frontmatterAssetsDir = extractAssetsDirFromFrontmatter(mdFile); if (frontmatterAssetsDir) { moduleAssetsFolderName = frontmatterAssetsDir; usingFrontmatter = true; break; // Use the first one found } } } catch (err) { _iterator2.e(err); } finally { _iterator2.f(); } var assetsDir = path.join(subdirPath, moduleAssetsFolderName); // Only process if assets directory exists if (fs.existsSync(assetsDir)) { var result = scanModuleAssets(mdFiles, assetsDir, subdirName, moduleAssetsFolderName, usingFrontmatter, fixMode, verbose); summary.results.push(result); summary.processedModules++; summary.totalUnusedAssets += result.deletedAssets; } }); if (verbose) { console.log("=========================================="); console.log("✨ Cleanup Complete!"); console.log("==========================================\n"); if (!fixMode) { console.log("â„šī¸ This was a preview. Run with --fix to actually delete unreferenced files."); } } return summary; } /** * Quick scan for unused assets (for integration with merge workflow) * @param {Array} inputFiles - Array of input file paths from manifest * @param {string} assetsFolderName - Default assets folder name * @returns {Object} Quick scan results */ function scanForUnusedAssets(inputFiles) { var assetsFolderName = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "assets"; debug("Starting quick asset scan for merge workflow"); var result = { totalUnusedAssets: 0, scannedDirectories: 0, hasUnusedAssets: false }; if (!inputFiles || inputFiles.length === 0) { debug("No input files provided for asset scan"); return result; } // Get unique directories from input files var directories = _toConsumableArray(new Set(inputFiles.map(function (file) { return path.dirname(file); }))); directories.forEach(function (dir) { // Look for assets directory in each input file directory var assetsDir = path.join(dir, assetsFolderName); if (fs.existsSync(assetsDir)) { result.scannedDirectories++; // Find markdown files in this directory var mdFiles = []; try { var files = fs.readdirSync(dir); mdFiles = files.filter(function (f) { return f.endsWith(".md"); }).map(function (f) { return path.join(dir, f); }); } catch (error) { debug("Error reading directory ".concat(dir, ": ").concat(error.message)); return; } // Quick scan without verbose output var scanResult = scanModuleAssets(mdFiles, assetsDir, path.basename(dir), assetsFolderName, false, false, false); if (!scanResult.error) { result.totalUnusedAssets += scanResult.deletedAssets; } } }); result.hasUnusedAssets = result.totalUnusedAssets > 0; debug("Quick scan complete: ".concat(result.totalUnusedAssets, " unused assets found in ").concat(result.scannedDirectories, " directories")); return result; } module.exports = { cleanAssets: cleanAssets, scanForUnusedAssets: scanForUnusedAssets, scanModuleAssets: scanModuleAssets, extractAssetsDirFromFrontmatter: extractAssetsDirFromFrontmatter };