concat-md
Version:
CLI and API to concatenate markdown files and modify as necessary.
239 lines • 10.3 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.concatMdSync = exports.gitHubLink = void 0;
/* eslint-disable @typescript-eslint/no-var-requires, prefer-destructuring */
const fs_1 = __importStar(require("fs"));
const path_1 = require("path");
const front_matter_1 = __importDefault(require("front-matter"));
const globby_1 = __importDefault(require("globby"));
const lodash_startcase_1 = __importDefault(require("lodash.startcase"));
/** @ignore */
const transformLinks = require("transform-markdown-links");
/** @ignore */
const transform = require("doctoc/lib/transform");
/** @ignore */
const readFile = fs_1.default.promises.readFile;
/** @ignore */
function gitHubLink(val) {
/* istanbul ignore next */
const value = val || "";
return value
.trim()
.toLowerCase()
.replace(/[^\w\- ]+/g, "")
.replace(/\s/g, "-")
.replace(/-+$/, "");
}
exports.gitHubLink = gitHubLink;
/**
* Makes given input array and returns it.
*
* @param input to construct array from
* @returns created array.
* @ignore
*/
function arrify(input) {
return Array.isArray(input) ? input : [input];
}
/** @ignore */
class MarkDownConcatenator {
constructor(dir, { toc = false, tocLevel = 3, ignore = [], decreaseTitleLevels = false, startTitleLevelAt = 1, joinString = "\n", titleKey, dirNameAsTitle = false, fileNameAsTitle = false, hideAnchorLinks = false, sorter = undefined, } = {}) {
this.visitedDirs = new Set();
this.fileTitleIndex = new Map();
this.files = [];
this.dir = dir;
this.toc = toc;
this.tocLevel = tocLevel;
this.ignore = ignore;
this.decreaseTitleLevels = decreaseTitleLevels;
this.startTitleLevelAt = startTitleLevelAt;
this.joinString = joinString;
this.titleKey = titleKey;
this.dirNameAsTitle = dirNameAsTitle;
this.fileNameAsTitle = fileNameAsTitle;
this.hideAnchorLinks = hideAnchorLinks;
this.sorter = sorter;
}
decreaseTitleLevelsBy(body, level) {
return !this.decreaseTitleLevels || level <= 0 ? body : body.replace(/(^#+)/gm, `$1${"#".repeat(level)}`);
}
async getFileNames() {
const paths = await (0, globby_1.default)([`**/*.md`], { cwd: this.dir, ignore: arrify(this.ignore) });
return paths.sort(this.sorter).map((path) => (0, path_1.join)(this.dir, path));
}
getFileNamesSync() {
const paths = globby_1.default.sync([`**/*.md`], { cwd: this.dir, ignore: arrify(this.ignore) });
return paths.sort(this.sorter).map((path) => (0, path_1.join)(this.dir, path));
}
async getFileDetails() {
const fileNames = await this.getFileNames();
return Promise.all(fileNames.map(async (fileName) => ({ path: fileName, ...(0, front_matter_1.default)(await readFile(fileName, { encoding: "utf8" })) })));
}
getFileDetailsSync() {
const fileNames = this.getFileNamesSync();
return fileNames.map((fileName) => ({ path: fileName, ...(0, front_matter_1.default)((0, fs_1.readFileSync)(fileName, { encoding: "utf8" })) }));
}
getDirParts(file) {
return this.dir === (0, path_1.dirname)(file.path) ? [] : (0, path_1.relative)(this.dir, (0, path_1.dirname)(file.path)).split(path_1.sep);
}
addTitle(file) {
let titleMd = "";
let fileTitle;
const titleSuffix = "\n\n";
let level = this.startTitleLevelAt - 1;
if (this.dirNameAsTitle) {
let currentDir = "";
const dirParts = this.getDirParts(file);
dirParts.forEach((part) => {
currentDir += currentDir ? path_1.sep + part : part;
level += 1;
if (!this.visitedDirs.has(currentDir)) {
const dirTitlePrefix = "#".repeat(level); // #, ##, ### ...etc.
const dirTitle = (0, lodash_startcase_1.default)(part);
titleMd += `${dirTitlePrefix} ${dirTitle}${titleSuffix}`;
this.visitedDirs.add(currentDir);
this.fileTitleIndex.set((0, path_1.join)(this.dir, currentDir), { title: dirTitle, md: titleMd, level });
}
});
}
const titleFromMeta = this.titleKey && file.attributes && file.attributes[this.titleKey];
const titleFromFileName = `${(0, lodash_startcase_1.default)((0, path_1.basename)(file.path, (0, path_1.extname)(file.path)))}`;
if (titleFromMeta || this.fileNameAsTitle) {
fileTitle = titleFromMeta || titleFromFileName;
level += 1;
const titlePrefix = "#".repeat(level); // #, ##, ### ...etc.
titleMd += `${titlePrefix} ${fileTitle}${titleSuffix}`;
}
else {
fileTitle = gitHubLink((0, path_1.relative)(this.dir, file.path));
if (!this.hideAnchorLinks) {
titleMd += `\n<a name="${fileTitle}"></a>\n\n`; // Provide an anchor to point links to this location. (For existing links pointing to file.)
}
}
this.fileTitleIndex.set(file.path, { title: fileTitle, md: titleMd, level });
}
getTitle(filePath) {
const title = this.fileTitleIndex.get(filePath);
/* istanbul ignore next */
if (!title) {
throw new Error(`Cannot get title for ${filePath}`);
}
return title;
}
addToc(content) {
if (!this.toc) {
return content;
}
const TOC_TAG = "<!-- START doctoc -->\n<!-- END doctoc -->";
let result = content;
if (!result.includes(TOC_TAG)) {
result = `${TOC_TAG}\n\n${result}`;
}
const docTocResult = transform(result, "github.com", this.tocLevel, undefined, true);
if (docTocResult.transformed) {
result = docTocResult.data;
}
return result;
}
modifyLinks(file) {
return transformLinks(file.body, (link) => {
if (link.startsWith("http")) {
return link;
}
// [ModifyCondition](../interfaces/modifycondition.md) - Link to file.
// [FileFormat](../README.md#fileformat) - Section in relative file.
// [saveSync](datafile.md#savesync) - Link in same file
// <a name="there_you_go"></a>Take me there
const absoluteTargetPath = (0, path_1.join)((0, path_1.dirname)(file.path), link);
const hashPosition = absoluteTargetPath.indexOf("#");
const hash = hashPosition > -1 ? absoluteTargetPath.slice(hashPosition) : "";
const targetFile = hashPosition > -1 ? absoluteTargetPath.slice(0, hashPosition) : absoluteTargetPath;
try {
const newLink = hash || `#${gitHubLink(this.getTitle(targetFile).title)}`;
return newLink;
}
catch (e) {
/* istanbul ignore next */
return "";
}
// console.log(link, newLink, targetFile, hash);
});
}
concatFiles(files) {
files.forEach((file) => {
this.addTitle(file);
const { level, md } = this.getTitle(file.path);
const body = this.decreaseTitleLevelsBy(file.body, level);
file.body = `${md}${body}`; // eslint-disable-line no-param-reassign
});
// 2nd pass loop is necessary, because all titles has to be processed.
files.forEach((file) => {
file.body = this.modifyLinks(file); // eslint-disable-line no-param-reassign
});
const result = files.map((file) => file.body).join(this.joinString);
return this.addToc(result);
}
async concat() {
const files = await this.getFileDetails();
return this.concatFiles(files);
}
concatSync() {
const files = this.getFileDetailsSync();
return this.concatFiles(files);
}
}
/**
* Scans and concatenates all markdown files in given directory.
*
* @param dir is the directory to scan markdown files in.
* @param options are several parameters to modify concatenation behaviour.
* @returns concatenated contents of markdown files.
* @example
* import concatMd, { concatMdSync } from "concat-md"
*/
function concatMdSync(dir, options) {
const markDownConcatenator = new MarkDownConcatenator(dir, options);
return markDownConcatenator.concatSync();
}
exports.concatMdSync = concatMdSync;
/**
* Scans and concatenates all markdown files in given directory.
*
* @param dir is the directory to scan markdown files in.
* @param options are several parameters to modify concatenation behaviour.
* @returns concatenated contents of markdown files.
* @example
* import concatMd, { concatMdSync } from "concat-md"
*/
async function concatMd(dir, options) {
const markDownConcatenator = new MarkDownConcatenator(dir, options);
return markDownConcatenator.concat();
}
exports.default = concatMd;
//# sourceMappingURL=index.js.map