UNPKG

@knennigtri/merge-markdown

Version:

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

484 lines (459 loc) 21.3 kB
"use strict"; function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } 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 _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } 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; } function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } } function _arrayWithHoles(r) { if (Array.isArray(r)) return r; } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } var fs = require("fs"); var path = require("path"); var yaml = require("js-yaml"); var _require = require("child_process"), spawn = _require.spawn; var debug = require("debug")("manifest"); var debugDeprecation = require("debug")("manifest:deprecation"); var debugJson = require("debug")("manifest:json"); exports.debbugOptions = { "manifest": "", "manifest:deprecation": "", "manifest:json": "" }; var DEFAULT_MANIFEST = { NAME: "manifest", EXT_TYPES: ["yml", "yaml", "json"], get EXTS() { return this.EXT_TYPES.map(function (ext) { return ".".concat(ext); }); }, get FILE() { return "".concat(this.NAME).concat(this.EXTS[0]); }, get FILE_TYPES() { return "".concat(this.NAME, "[").concat(this.EXTS.join("|"), "]"); } }; // TODO A super simple manifest should not error out. Example: // input: // course-website-agenda.md // course-website-topics.md // output: // name: merged/website.md var manifestWriteDir = process.cwd(); // When using -c relative/path/to/markdown/files, the manifest will where the command is run /** * Creates a valid manifest JSON based on input (or no input) * DEBUG=manifest:json */ function getManifestObj(inputManifestFile, qaMode) { var fileType = path.extname(inputManifestFile).toLowerCase(); if (!DEFAULT_MANIFEST.EXTS.includes(fileType)) { console.log("Manifest extension must be: [" + DEFAULT_MANIFEST.EXTS.join("|") + "]"); return; } debug("Found manifest to use: " + inputManifestFile); var fileContents = fs.readFileSync(inputManifestFile, "utf8"); var jsonObj = ""; try { //Attempt to read the YAML and output JSON var data = yaml.loadAll(fileContents, "json"); var yamlContents = JSON.stringify(data[0], null, 2); jsonObj = JSON.parse(yamlContents); } catch (_unused) { debugJson("Could not read YAML, attemping JSON"); try { //Attempt to read JSON jsonObj = JSON.parse(fileContents); } catch (e) { console.log("Manifest file does not contain valid YAML or JSON content."); throw e; } } // If the manifest doesn"t have an output, generate the output name based on the manifest directory if (!jsonObj.output || !jsonObj.output.name) throw new Error("Manifest is missing output.name. Consider auto-creating an initial manifest"); // jsonObj = fixDeprecatedEntry(jsonObj); if (jsonObj.output.name.split(".").pop() != "md") throw new Error("output.name needs to be a .md file but found: " + jsonObj.output.name); if (!jsonObj.input) throw new Error("Manifest is missing input. Consider auto-creating an initial manifest"); jsonObj = fixDeprecatedEntry(jsonObj); if (qaMode) { if (!jsonObj.qa || !jsonObj.qa.exclude) { console.log("No exclude patterns given for QA. Using default `frontmatter` for exclusion."); jsonObj.qa = { "exclude": "frontmatter" }; } } return jsonObj; } /** * * @param {*} inputManifestFile * @param {*} qaMode * @returns */ /** TODO implement this method to guarantee minimum fields are set or FAIL * !m.input - fail * At least one m.input[key] - fail * !m.output - create default * !m.output.name - create default * !m.output.pandoc - create default * !m.output.wkhtmltopdf - create default * !m.qa.exclude - create default */ exports.getJSON_withABSPaths = function (inputManifestFile, qaMode) { var manifestObj = getManifestObj(inputManifestFile, qaMode); var baseDir = path.dirname(inputManifestFile); // Update input paths to absolute paths if (manifestObj.input) { // manifest.input[key] var inputObjABS = {}; for (var keyPath in manifestObj.input) { var absPath = path.resolve(baseDir, keyPath); inputObjABS = _objectSpread(_objectSpread({}, inputObjABS), {}, _defineProperty({}, absPath, _objectSpread({}, manifestObj.input[keyPath]))); } debugJson("manifest.input (ABS): ".concat(JSON.stringify(inputObjABS, null, 2))); manifestObj.input = inputObjABS; } // Update output paths to absolute paths if (manifestObj.output) { for (var _keyPath in manifestObj.output) { // manifest.output.name if (_keyPath === "name") { manifestObj.output.name = path.resolve(baseDir, manifestObj.output.name); debugJson("manifestObj.output.name (ABS): ".concat(manifestObj.output.name)); } // manifest.output.pandoc[key]: "-c relPath/to/theme.css" // manifest.output.pandoc[key]: "--template relPath/to/latex/template.html" // manifest.output.pandoc[key]: "--reference-doc relPath/to/reference.docx">" if (_keyPath === "pandoc") { // let p = manifestObj.output.pandoc; // Iterate through all pandoc keys and resolve paths for specific options for (var _i = 0, _Object$entries = Object.entries(manifestObj.output.pandoc); _i < _Object$entries.length; _i++) { var _Object$entries$_i = _slicedToArray(_Object$entries[_i], 2), key = _Object$entries$_i[0], value = _Object$entries$_i[1]; debugJson("Key: ".concat(key, ", Value: ").concat(value)); if (typeof value === "string") { if (value.startsWith("-c ") || value.startsWith("--template") || value.startsWith("--reference-doc")) { var splitPath = value.split(" "); var command = splitPath[0]; // Get the command part var relativePath = splitPath.slice(1).join(" "); // Join the rest as the path var newValue = "".concat(command, " ").concat(path.resolve(baseDir, relativePath)); manifestObj.output.pandoc[key] = newValue; debugJson("manifest.output.pandoc (ABS): ".concat(manifestObj.output.pandoc[key])); } } } } } } return manifestObj; }; /** * Method to organize the manifest for merge and presentation to * allow for non-destructive updates to merge-markdown. * Important if users are coming from earlier versions of merge-markdown */ function fixDeprecatedEntry(manifestFix) { var updatesNeeded = ""; //Fix output to allow for keys under the output if (typeof manifestFix.output === "string") { debugDeprecation("Found: manifest.output"); var name = manifestFix.output; delete manifestFix.output; manifestFix.output = {}; manifestFix.output.name = name; updatesNeeded += " manifest.output >> manifest.output.name.\n"; } //Move all outputOptions under the output if (Object.prototype.hasOwnProperty.call(manifestFix, "mergedTOC")) { debugDeprecation("Found: manifest.mergedTOC"); manifestFix.output.doctoc = manifestFix.mergedTOC; delete manifestFix.mergedTOC; updatesNeeded += " manifest.mergedTOC >> manifest.output.doctoc.\n"; } if (Object.prototype.hasOwnProperty.call(manifestFix, "pandoc")) { debugDeprecation("Found: manifest.pandoc"); manifestFix.output.pandoc = manifestFix.pandoc; delete manifestFix.pandoc; updatesNeeded += " manifest.pandoc >> manifest.output.pandoc.\n"; } if (Object.prototype.hasOwnProperty.call(manifestFix, "wkhtmltopdf")) { debugDeprecation("Found: manifest.wkhtmltopdf"); manifestFix.output.wkhtmltopdf = manifestFix.wkhtmltopdf; delete manifestFix.wkhtmltopdf; updatesNeeded += " manifest.wkhtmltopdf >> manifest.output.wkhtmltopdf.\n"; } //Update all TOC and mergedTOC keys to doctoc if (Object.prototype.hasOwnProperty.call(manifestFix.output, "TOC")) { debugDeprecation("Found: manifest.output.TOC"); manifestFix.output.doctoc = manifestFix.output.TOC; delete manifestFix.output.TOC; updatesNeeded += " manifest.output.TOC >> manifest.output.doctoc.\n"; } if (Object.prototype.hasOwnProperty.call(manifestFix.output, "mergedTOC")) { debugDeprecation("Found: manifest.output.mergedTOC"); manifestFix.output.doctoc = manifestFix.output.mergedTOC; delete manifestFix.output.mergedTOC; updatesNeeded += " manifest.output.mergedTOC >> manifest.output.doctoc.\n"; } if (Object.prototype.hasOwnProperty.call(manifestFix, "TOC")) { debugDeprecation("Found: manifest.TOC"); manifestFix.doctoc = manifestFix.TOC; delete manifestFix.TOC; updatesNeeded += " manifest.TOC >> manifest.doctoc.\n"; } if (Object.prototype.hasOwnProperty.call(manifestFix, "input")) { var update = false; for (var i in manifestFix.input) { if (Object.prototype.hasOwnProperty.call(manifestFix.input[i], "TOC")) { debugDeprecation("Found: manifest.input[".concat(i, "].TOC")); manifestFix.input[i].doctoc = manifestFix.input[i].TOC; delete manifestFix.input[i].TOC; update = true; } } if (update) updatesNeeded += " manifest.input[item].TOC >> manifest.input[item].doctoc.\n"; } //Display to the user which keys should be updated in their manifest if (updatesNeeded) { console.log("[WARNING] Below entries are old. Consider updating your manifest:"); console.log(updatesNeeded); } return manifestFix; } /** * Checks if a manifest file exists * @param {*} inputArg file/directory given in -m param * @returns valid manifest file or false */ exports.exists = function (inputArg) { try { var fsStat = fs.lstatSync(inputArg); if (fsStat.isFile()) { //Set if file is given var e = path.extname(inputArg).toLowerCase(); if (DEFAULT_MANIFEST.EXTS.includes(e)) { debug("Found Manifest: ".concat(inputArg)); return inputArg; } else { console.log("Manifest file can only be [".concat(DEFAULT_MANIFEST.EXTS.join("|"), "]")); return false; } } else if (fsStat.isDirectory()) { //Search for default manifest if directory debug("Searching for ".concat(DEFAULT_MANIFEST.FILE_TYPES, " in ").concat(inputArg)); var directory = inputArg; var possibleFileNames = DEFAULT_MANIFEST.EXTS.map(function (ext) { return "".concat(DEFAULT_MANIFEST.NAME).concat(ext); }); //Look for a manifest file in the given directory var _iterator = _createForOfIteratorHelper(possibleFileNames), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var fileName = _step.value; var filePath = path.join(directory, fileName); var fileStat = fs.lstatSync(filePath); if (fileStat.isFile()) { debug("Found Default Manifest: ".concat(filePath)); return filePath; } } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } console.log("No default ".concat(DEFAULT_MANIFEST.FILE_TYPES, " file found in ").concat(directory)); } } catch (err) { console.log("".concat(inputArg, " is not a valid file or directory")); return false; } }; /** * Autocreates a starter manifest file * @param {*} dir location of input files * @param {*} fullProject boolean to add 'full project on create' paths */ exports.createManifestFile = function (dir, fullProject) { var jsonObject = { input: {}, noYAML: true, doctoc: true, replace: { "<!--{timestamp}-->": "01/01/2024", "<!--{title}-->": "My Title", "<!--{author}-->": "Chuck Grant", "<!--{documentType}-->": "Instruction Manual", "### My h3 title": "#### My h4 title", "({#(.*?)})": "" }, output: { "name": "target/mergedFile.md", "doctoc": true, "pandoc": { "css": "-c path/to/theme.css", "latexTemplate": "--template path/to/latex/template.html", "referenceDoc": "--reference-doc path/to/reference.docx" }, "wkhtmltopdf": { "marginBottom": ".7in", "marginTop": "1in", "marginLeft": ".7in", "marginRight": ".7in", "pageSize": "Letter", "headerFontSize": 8, "headerSpacing": 5, "headerRight": "[section]", "footerLine": true, "footerFontSize": 8, "footerLeft": "[doctitle]", "footerCenter": "", "footerRight": "[page]" } }, docker: { excludePaths: ["/target/", "/dist/"] }, qa: { exclude: "(frontmatter|preamble)" } }; if (fullProject) { jsonObject.input["theme/frontmatter.md"] = { noYAML: false, doctoc: false }; jsonObject.output.pandoc.css = "-c theme/theme.css"; jsonObject.output.pandoc.latexTemplate = "--template theme/template.html"; jsonObject.output.pandoc.referenceDoc = "--reference-doc theme/reference.docx"; } var inputArr = findMarkdownFiles(dir); var counter = 0; inputArr.forEach(function (file) { var inputOptions = { replace: { "\\[#\\]": counter } }; jsonObject.input[file] = inputOptions; counter++; }); //Write YAML File var yamlString = yaml.dump(jsonObject); var manifestPath = path.join(manifestWriteDir, "".concat(DEFAULT_MANIFEST.FILE)); try { fs.writeFileSync(manifestPath, yamlString); console.log("YAML file successfully created: " + manifestPath); } catch (error) { console.error("Error writing: " + manifestPath, error); } // Write the package.json file if the fullProject flag is set if (fullProject) { writeNPMFile(); } }; /** * Finds all markdown (.md) files within a directory * @param {*} directoryPath path to search * @returns array of .md paths */ function findMarkdownFiles(directoryPath) { var markdownFiles = []; // Synchronously read the contents of the directory var files = fs.readdirSync(directoryPath); // Iterate through each file in the directory var _iterator2 = _createForOfIteratorHelper(files), _step2; try { for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { var file = _step2.value; var filePath = path.join(directoryPath, file); var stats = fs.statSync(filePath); // Check if the file is a directory if (stats.isDirectory()) { // Recursively search for .md files in the subdirectory markdownFiles = markdownFiles.concat(findMarkdownFiles(filePath)); } else { if (path.extname(file).toLowerCase() === ".md") { markdownFiles.push(filePath); } } } } catch (err) { _iterator2.e(err); } finally { _iterator2.f(); } return markdownFiles; } function writeNPMFile() { var packageJsonPath = path.join(process.cwd(), "package.json"); // Check if package.json already exists if (fs.existsSync(packageJsonPath)) { console.log("package.json already exists, skipping npm init"); return; } return new Promise(function (resolve, reject) { console.log("Running npm init - please fill out your project details..."); // Run npm init interactively var npmInit = spawn("npm", ["init"], { stdio: "inherit", shell: true }); npmInit.on("close", function (code) { if (code === 0) { debug("package.json created successfully"); // Now read and modify the package.json try { var packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); // Add your custom fields to package.json packageJson.scripts = _objectSpread(_objectSpread({}, packageJson.scripts), {}, { "pre": "rimraf merged || rimraf target || true", "merge-markdown": "npm run pre && npm run mm:docker && npm run cleanup", "cleanup": "", "mm:docker": "merge-markdown -m manifest.yml --pdf --docker", "mm:html": "merge-markdown -m manifest.yml --html", "mm:word": "merge-markdown -m manifest.yml --word", "install:docker:mac": "brew install caskroom/cask/brew-cask; brew cask install docker", "install:docker:win": "powershell -Command \"Start-Process -Wait -FilePath 'winget' -ArgumentList 'install -e --id Docker.DockerDesktop'\"" }); packageJson.dependencies = _objectSpread(_objectSpread({}, packageJson.dependencies), {}, { "@knennigtri/merge-markdown": "*", "rimraf": "^5.0.0" }); packageJson.devDependencies = _objectSpread({}, packageJson.devDependencies); packageJson.bugs = { "url": "https://github.com/knennigtri/merge-markdown/issues/new/choose" }; // Write the modified package.json back fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); console.log("package.json updated with merge-markdown configurations."); console.log("Run `npm install` to install the project dependencies to use the npm scripts."); console.log("It's highly recommended to install docker for optimal use: https://docs.docker.com/engine/install"); console.log("Run `npm run merge-markdown` to build your project. (Requires docker)"); resolve(packageJsonPath); } catch (error) { console.error("Error modifying package.json:", error); reject(error); } } else { console.error("npm init failed with code:", code); reject(new Error("npm init failed with code: ".concat(code))); } }); npmInit.on("error", function (error) { console.error("Error running npm init:", error); reject(error); }); }); } exports.DEFAULT_MANIFEST = DEFAULT_MANIFEST;