@knennigtri/merge-markdown
Version:
Prepare input files to merge together and optionally output html/pdf/word presentation
370 lines (348 loc) âĸ 15.1 kB
JavaScript
;
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("
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
};