markdown-notes-tree
Version:
Generate Markdown trees that act as a table of contents for a folder structure with Markdown notes
307 lines (253 loc) • 10.9 kB
JavaScript
"use strict";
const frontMatter = require("front-matter");
const markdownParser = require("./markdown-parser");
module.exports = {
getTitleParagraphFromContents,
getNewMainReadmeContents,
getNewDirectoryReadmeContents,
getDirectoryDescriptionParagraphFromCurrentContents
};
const markers = {
mainReadmeTreeStart: "<!-- tree generated by markdown-notes-tree starts here -->",
mainReadmeTreeEnd: "<!-- tree generated by markdown-notes-tree ends here -->",
directoryReadmeStart: "<!-- generated by markdown-notes-tree -->",
directoryReadmeUpwardNavigationStart:
"<!-- upward navigation links generated by markdown-notes-tree start here -->",
directoryReadmeUpwardNavigationEnd:
"<!-- upward navigation links generated by markdown-notes-tree end here -->",
directoryReadmeDescriptionStart:
"<!-- optional markdown-notes-tree directory description starts here -->",
directoryReadmeDescriptionEnd:
"<!-- optional markdown-notes-tree directory description ends here -->",
mainReadmeTreeStart_v_1_8_0: "<!-- auto-generated notes tree starts here -->",
mainReadmeTreeEnd_v_1_8_0: "<!-- auto-generated notes tree ends here -->",
directoryReadmeStart_v_1_8_0: "<!-- this entire file is auto-generated -->"
};
const legacyToNewMarkersMapping = {
[markers.mainReadmeTreeStart_v_1_8_0]: markers.mainReadmeTreeStart,
[markers.mainReadmeTreeEnd_v_1_8_0]: markers.mainReadmeTreeEnd,
[markers.directoryReadmeStart_v_1_8_0]: markers.directoryReadmeStart
};
function getMarkerNodes(markers, astNode) {
return markers.map(marker => markdownParser.getFirstHtmlChildWithValue(marker, astNode));
}
function getTitleParagraphFromContents(contents) {
const normalizedContents = normalizeContents(contents);
const parsedFrontMatter = frontMatter(normalizedContents);
const titleFromFrontMatter = parsedFrontMatter.attributes.tree_title;
if (titleFromFrontMatter) {
return markdownParser.escapeText(titleFromFrontMatter);
}
const contentsWithoutFrontMatter = parsedFrontMatter.body.trimLeft();
const astNode = markdownParser.getAstNodeFromMarkdown(contentsWithoutFrontMatter);
const titleNode = markdownParser.getFirstLevel1HeadingChild(astNode);
if (!titleNode) {
return undefined;
}
if (markdownParser.hasLinkDescendant(titleNode)) {
// links are the only content that can be used inside headings but not inside links
throw new Error(
"Title cannot contain Markdown links since this would mess up the links in the tree (consider using HTML as a workaround)"
);
}
return markdownParser.extractParagraphFromHeadingNode(titleNode);
}
function getNewMainReadmeContents(currentContents, markdownForTree, environment) {
const normalizedContents = normalizeContents(currentContents);
const astNode = markdownParser.getAstNodeFromMarkdown(normalizedContents);
const [treeStartMarkerNode, treeEndMarkerNode] = getMarkerNodes(
[markers.mainReadmeTreeStart, markers.mainReadmeTreeEnd],
astNode
);
const contentsBeforeTree = getMainReadmeContentsBeforeTree(
normalizedContents,
treeStartMarkerNode,
environment
);
const contentsAfterTree = getMainReadmeContentsAfterTree(
normalizedContents,
treeStartMarkerNode,
treeEndMarkerNode,
environment
);
return (
contentsBeforeTree +
markers.mainReadmeTreeStart +
environment.endOfLine.repeat(2) +
markdownForTree +
environment.endOfLine.repeat(2) +
markers.mainReadmeTreeEnd +
contentsAfterTree
);
}
function getMainReadmeContentsBeforeTree(contents, treeStartMarkerNode, environment) {
if (!treeStartMarkerNode) {
return contents + environment.endOfLine.repeat(2);
}
const indexTreeStartMarker = markdownParser.getStartIndex(treeStartMarkerNode);
return contents.substring(0, indexTreeStartMarker);
}
function getMainReadmeContentsAfterTree(
contents,
treeStartMarkerNode,
treeEndMarkerNode,
environment
) {
if (!treeEndMarkerNode) {
return environment.endOfLine;
}
const treeEndMarkerValid =
treeStartMarkerNode &&
markdownParser.getStartIndex(treeEndMarkerNode) >
markdownParser.getStartIndex(treeStartMarkerNode);
if (!treeEndMarkerValid) {
throw new Error("Invalid file structure: tree end marker found before tree start marker");
}
const indexEndOfTreeEndMarker = markdownParser.getEndIndex(treeEndMarkerNode);
return contents.substring(indexEndOfTreeEndMarker);
}
function normalizeContents(contents) {
const astNode = markdownParser.getAstNodeFromMarkdown(contents);
const legacyMarkers = Object.keys(legacyToNewMarkersMapping);
const legacyMarkerNodes = markdownParser.getAllHtmlChildrenWithValues(legacyMarkers, astNode);
let adjustedContents = contents;
// start edits from end of string so earlier edits don't interfere with later ones
for (const legacyMarkerNode of legacyMarkerNodes.reverse()) {
const markerStart = markdownParser.getStartIndex(legacyMarkerNode);
const markerEnd = markdownParser.getEndIndex(legacyMarkerNode);
const marker = adjustedContents.substring(markerStart, markerEnd);
const newMarker = legacyToNewMarkersMapping[marker];
const beforeMarker = adjustedContents.substring(0, markerStart);
const afterMarker = adjustedContents.substring(markerEnd);
adjustedContents = beforeMarker + newMarker + afterMarker;
}
return adjustedContents;
}
function getNewDirectoryReadmeContents(
titleParagraph,
upwardNavigationPaths,
currentContents,
markdownForTree,
environment
) {
const normalizedContents = normalizeContents(currentContents);
const astNode = markdownParser.getAstNodeFromMarkdown(normalizedContents);
let startOfFile = markers.directoryReadmeStart;
if (environment.options.includeUpwardNavigation) {
startOfFile =
startOfFile +
environment.endOfLine.repeat(2) +
getUpwardNavigationContents(upwardNavigationPaths, environment);
}
const [
directoryReadmeStartMarkerNode,
directoryReadmeUpwardNavigationEndMarkerNode,
directoryReadmeDescriptionEndMarkerNode
] = getMarkerNodes(
[
markers.directoryReadmeStart,
markers.directoryReadmeUpwardNavigationEnd,
markers.directoryReadmeDescriptionEnd
],
astNode
);
const userManagedContents = getUserManagedDirectoryReadmeContents(
normalizedContents,
titleParagraph,
directoryReadmeStartMarkerNode,
directoryReadmeUpwardNavigationEndMarkerNode,
directoryReadmeDescriptionEndMarkerNode,
environment
);
return (
startOfFile +
environment.endOfLine.repeat(2) +
userManagedContents +
environment.endOfLine.repeat(2) +
markdownForTree +
environment.endOfLine
);
}
function getUserManagedDirectoryReadmeContents(
contents,
titleParagraph,
directoryReadmeStartMarkerNode,
directoryReadmeUpwardNavigationEndMarkerNode,
directoryReadmeDescriptionEndMarkerNode,
environment
) {
if (!directoryReadmeDescriptionEndMarkerNode) {
// the file did not exist yet or was created with an old version of the tool
// generate contents from scratch, don't care about preserving formatting syntax
const titleHeading = markdownParser.generateLevel1HeadingFromMarkdownParagraph(
titleParagraph
);
return (
titleHeading +
environment.endOfLine.repeat(2) +
markers.directoryReadmeDescriptionStart +
environment.endOfLine.repeat(2) +
markers.directoryReadmeDescriptionEnd
);
}
// the file already existed and the user might have adjusted title, description, ...
// preserve the exact contents of the user-managed part of the file (everything until end of description)
if (directoryReadmeUpwardNavigationEndMarkerNode) {
return contents
.substring(
markdownParser.getEndIndex(directoryReadmeUpwardNavigationEndMarkerNode),
markdownParser.getEndIndex(directoryReadmeDescriptionEndMarkerNode)
)
.trim();
}
return contents
.substring(
markdownParser.getEndIndex(directoryReadmeStartMarkerNode),
markdownParser.getEndIndex(directoryReadmeDescriptionEndMarkerNode)
)
.trim();
}
function getUpwardNavigationContents(upwardNavigationPaths, environment) {
const linkOneLevelUp = markdownParser.generateLinkFromMarkdownParagraphAndUrl(
"Go one level up",
upwardNavigationPaths.oneLevelUp
);
const linkToTopLevel = markdownParser.generateLinkFromMarkdownParagraphAndUrl(
"Go to top level",
upwardNavigationPaths.toTopLevel
);
const linksParagraph = linkOneLevelUp + " / " + linkToTopLevel;
return (
markers.directoryReadmeUpwardNavigationStart +
environment.endOfLine.repeat(2) +
linksParagraph +
environment.endOfLine.repeat(2) +
markers.directoryReadmeUpwardNavigationEnd
);
}
function getDirectoryDescriptionParagraphFromCurrentContents(currentContents) {
const astNode = markdownParser.getAstNodeFromMarkdown(currentContents);
const [startMarkerNode, endMarkerNode] = getMarkerNodes(
[markers.directoryReadmeDescriptionStart, markers.directoryReadmeDescriptionEnd],
astNode
);
if (!startMarkerNode && !endMarkerNode) {
return "";
}
const markersValid =
startMarkerNode &&
endMarkerNode &&
markdownParser.getStartIndex(endMarkerNode) > markdownParser.getStartIndex(startMarkerNode);
if (!markersValid) {
throw new Error(
"Invalid file structure: only one description marker found or end marker found before start marker"
);
}
const descriptionStart = markdownParser.getEndIndex(startMarkerNode);
const descriptionEnd = markdownParser.getStartIndex(endMarkerNode);
const description = currentContents.substring(descriptionStart, descriptionEnd).trim();
if (description && !markdownParser.isSingleMarkdownParagraph(description)) {
throw new Error("Subdirectory description should be just a single paragraph");
}
return description;
}