markdown-notes-tree
Advanced tools
Comparing version 1.10.2 to 1.11.0-beta.0
{ | ||
"name": "markdown-notes-tree", | ||
"version": "1.10.2", | ||
"version": "1.11.0-beta.0", | ||
"description": "Generate Markdown trees that act as a table of contents for a folder structure with Markdown notes", | ||
@@ -36,3 +36,3 @@ "main": "src/index.js", | ||
"fs-extra": "^8.1.0", | ||
"jest": "^26.1.0", | ||
"jest": "^26.6.3", | ||
"prettier": "^1.19.1", | ||
@@ -43,2 +43,3 @@ "prettier-check": "^2.0.0" | ||
"front-matter": "^3.1.0", | ||
"mdast-util-from-markdown": "^0.8.5", | ||
"minimatch": "^3.0.4", | ||
@@ -45,0 +46,0 @@ "minimist": "^1.2.5" |
@@ -114,2 +114,8 @@ # markdown-notes-tree | ||
## Known limitations | ||
The tool does not support Markdown links inside the titles of notes and subdirectory README files. This is intentional, because these titles will be turned into links by the tool and Markdown does not support nested links. | ||
As a workaround, HTML links can be used ([example](test-data/subdirectory-title-rich-text/expected/sub2/sub2b/README.md)). | ||
## Development | ||
@@ -116,0 +122,0 @@ |
@@ -5,2 +5,4 @@ "use strict"; | ||
const markdownParser = require("./markdown-parser"); | ||
module.exports = { | ||
@@ -37,13 +39,24 @@ getTitleFromMarkdownContents, | ||
const contentsWithoutFrontMatter = parsedFrontMatter.body.trimLeft(); | ||
const lines = contentsWithoutFrontMatter.split(/\r\n|\r|\n/); | ||
const astNode = markdownParser.getAstNodeFromContents(contentsWithoutFrontMatter); | ||
const titleNode = markdownParser.getFirstLevel1HeadingChild(astNode); | ||
for (const line of lines) { | ||
if (line.startsWith("# ")) { | ||
return line.substring(2); | ||
} else if (line !== "" && line !== markers.directoryReadmeStart) { | ||
return undefined; | ||
} | ||
if (!titleNode) { | ||
return undefined; | ||
} | ||
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)" | ||
); | ||
} | ||
const contentStartIndex = markdownParser.getContentStartIndex(titleNode); | ||
const contentEndIndex = markdownParser.getContentEndIndex(titleNode); | ||
if (contentStartIndex === contentEndIndex) { | ||
return undefined; | ||
} | ||
return contentsWithoutFrontMatter.substring(contentStartIndex, contentEndIndex); | ||
} | ||
@@ -53,30 +66,27 @@ | ||
currentContents = normalizeContents(currentContents); | ||
const astNode = markdownParser.getAstNodeFromContents(currentContents); | ||
const indexTreeStartMarker = currentContents.indexOf(markers.mainReadmeTreeStart); | ||
const treeStartMarkerPresent = indexTreeStartMarker >= 0; | ||
let contentsBeforeTree; | ||
const treeStartMarkerNode = markdownParser.getFirstHtmlChildWithValue( | ||
markers.mainReadmeTreeStart, | ||
astNode | ||
); | ||
if (treeStartMarkerPresent) { | ||
contentsBeforeTree = currentContents.substring(0, indexTreeStartMarker); | ||
} else { | ||
contentsBeforeTree = currentContents + environment.endOfLine.repeat(2); | ||
} | ||
const treeEndMarkerNode = markdownParser.getFirstHtmlChildWithValue( | ||
markers.mainReadmeTreeEnd, | ||
astNode | ||
); | ||
const indexTreeEndMarker = currentContents.indexOf(markers.mainReadmeTreeEnd); | ||
const treeEndMarkerPresent = indexTreeEndMarker >= 0; | ||
let contentsAfterTree; | ||
const contentsBeforeTree = getMainReadmeContentsBeforeTree( | ||
currentContents, | ||
treeStartMarkerNode, | ||
environment | ||
); | ||
const treeEndMarkerValid = | ||
treeEndMarkerPresent && treeStartMarkerPresent && indexTreeEndMarker > indexTreeStartMarker; | ||
const contentsAfterTree = getMainReadmeContentsAfterTree( | ||
currentContents, | ||
treeStartMarkerNode, | ||
treeEndMarkerNode, | ||
environment | ||
); | ||
if (treeEndMarkerValid) { | ||
contentsAfterTree = currentContents.substring( | ||
indexTreeEndMarker + markers.mainReadmeTreeEnd.length | ||
); | ||
} else if (treeEndMarkerPresent) { | ||
throw new Error("Invalid file structure: tree end marker found before tree start marker"); | ||
} else { | ||
contentsAfterTree = environment.endOfLine; | ||
} | ||
return ( | ||
@@ -93,2 +103,34 @@ contentsBeforeTree + | ||
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) { | ||
@@ -98,15 +140,8 @@ return contents | ||
.replace(markers.mainReadmeTreeEnd_v_1_8_0, markers.mainReadmeTreeEnd) | ||
.replace(markers.directoryReadmeStart_v_1_8_0, markers.directoryReadmeStart) | ||
.trimLeft(); | ||
.replace(markers.directoryReadmeStart_v_1_8_0, markers.directoryReadmeStart); | ||
} | ||
function getNewDirectoryReadmeContents(name, currentContents, markdownForTree, environment) { | ||
currentContents = normalizeContents(currentContents); | ||
function getNewDirectoryReadmeContents(title, description, markdownForTree, environment) { | ||
const titleHeading = `# ${title}`; | ||
const currentTitle = getTitleFromMarkdownContents(currentContents); | ||
const title = currentTitle || name; | ||
const titleLine = `# ${title}`; | ||
const description = getDirectoryDescriptionFromCurrentContents(currentContents); | ||
let partBetweenDescriptionMarkers = environment.endOfLine.repeat(2); | ||
@@ -122,3 +157,3 @@ | ||
environment.endOfLine.repeat(2) + | ||
titleLine + | ||
titleHeading + | ||
environment.endOfLine.repeat(2) + | ||
@@ -135,23 +170,32 @@ markers.directoryReadmeDescriptionStart + | ||
function getDirectoryDescriptionFromCurrentContents(currentContents) { | ||
const indexStartMarker = currentContents.indexOf(markers.directoryReadmeDescriptionStart); | ||
const indexEndMarker = currentContents.indexOf(markers.directoryReadmeDescriptionEnd); | ||
const astNode = markdownParser.getAstNodeFromContents(currentContents); | ||
const startMarkerPresent = indexStartMarker >= 0; | ||
const endMarkerPresent = indexEndMarker >= 0; | ||
const startMarkerNode = markdownParser.getFirstHtmlChildWithValue( | ||
markers.directoryReadmeDescriptionStart, | ||
astNode | ||
); | ||
const endMarkerNode = markdownParser.getFirstHtmlChildWithValue( | ||
markers.directoryReadmeDescriptionEnd, | ||
astNode | ||
); | ||
if (!startMarkerNode && !endMarkerNode) { | ||
return ""; | ||
} | ||
const markersValid = | ||
startMarkerPresent && endMarkerPresent && indexEndMarker > indexStartMarker; | ||
startMarkerNode && | ||
endMarkerNode && | ||
markdownParser.getStartIndex(endMarkerNode) > markdownParser.getStartIndex(startMarkerNode); | ||
if (markersValid) { | ||
const descriptionStart = indexStartMarker + markers.directoryReadmeDescriptionStart.length; | ||
const descriptionEnd = indexEndMarker; | ||
const descriptionStart = markdownParser.getEndIndex(startMarkerNode); | ||
const descriptionEnd = markdownParser.getStartIndex(endMarkerNode); | ||
return currentContents.substring(descriptionStart, descriptionEnd).trim(); | ||
} else if (startMarkerPresent || endMarkerPresent) { | ||
} else { | ||
throw new Error( | ||
"Invalid file structure: only one description marker found or end marker found before start marker" | ||
); | ||
} else { | ||
return ""; | ||
} | ||
} |
@@ -11,2 +11,12 @@ "use strict"; | ||
describe("getTitleFromMarkdownContents", () => { | ||
test("it should handle empty files", () => { | ||
const contents = ""; | ||
expect(fileContents.getTitleFromMarkdownContents(contents)).toBeUndefined(); | ||
}); | ||
test("it should handle empty title", () => { | ||
const contents = "# "; | ||
expect(fileContents.getTitleFromMarkdownContents(contents)).toBeUndefined(); | ||
}); | ||
test("it should support CRLF line endings", () => { | ||
@@ -33,3 +43,8 @@ const contents = "# test" + "\r\n" + "second line"; | ||
test("it should ignore the directory README start marker", () => { | ||
const contents = "<!-- generated by markdown-notes-tree -->" + "\n" + "\n" + "# test"; | ||
const contents = dedent( | ||
`<!-- generated by markdown-notes-tree --> | ||
# test` | ||
); | ||
expect(fileContents.getTitleFromMarkdownContents(contents)).toBe("test"); | ||
@@ -39,3 +54,8 @@ }); | ||
test("it should ignore the old directory README start marker", () => { | ||
const contents = "<!-- this entire file is auto-generated -->" + "\n" + "\n" + "# test"; | ||
const contents = dedent( | ||
`<!-- this entire file is auto-generated --> | ||
# test` | ||
); | ||
expect(fileContents.getTitleFromMarkdownContents(contents)).toBe("test"); | ||
@@ -45,13 +65,10 @@ }); | ||
test("it should return the tree title from YAML front matter if available", () => { | ||
const contents = | ||
"---" + | ||
"\n" + | ||
"tree_title: TreeTitle" + | ||
"\n" + | ||
"---" + | ||
"\n" + | ||
"\n" + | ||
"# test" + | ||
"\n" + | ||
"second line"; | ||
const contents = dedent( | ||
`--- | ||
tree_title: TreeTitle | ||
--- | ||
# test | ||
second line` | ||
); | ||
@@ -62,13 +79,10 @@ expect(fileContents.getTitleFromMarkdownContents(contents)).toBe("TreeTitle"); | ||
test("it should ignore YAML front matter that has no tree_title attribute", () => { | ||
const contents = | ||
"---" + | ||
"\n" + | ||
"description: Some text" + | ||
"\n" + | ||
"---" + | ||
"\n" + | ||
"\n" + | ||
"# test" + | ||
"\n" + | ||
"second line"; | ||
const contents = dedent( | ||
`--- | ||
description: Some text | ||
--- | ||
# test | ||
second line` | ||
); | ||
@@ -78,6 +92,30 @@ expect(fileContents.getTitleFromMarkdownContents(contents)).toBe("test"); | ||
test("it should return undefined if the first real line doesn't have a title", () => { | ||
test("it should return undefined if the file doesn't have a title", () => { | ||
const contents = "some non-title content"; | ||
expect(fileContents.getTitleFromMarkdownContents(contents)).toBeUndefined(); | ||
}); | ||
test("it should support content before the title", () => { | ||
const contents = dedent( | ||
`content before title | ||
# test | ||
some other content` | ||
); | ||
expect(fileContents.getTitleFromMarkdownContents(contents)).toBe("test"); | ||
}); | ||
test("it should fail if title contains content that is not supported inside links", () => { | ||
const contents = dedent( | ||
`# test [mistermicheels](http://mistermicheels.com) | ||
some other content` | ||
); | ||
expect(() => fileContents.getTitleFromMarkdownContents(contents)).toThrow( | ||
"Title cannot contain Markdown links since this would mess up the links in the tree (consider using HTML as a workaround)" | ||
); | ||
}); | ||
}); | ||
@@ -228,11 +266,24 @@ | ||
}); | ||
test("it should ignore markers inside code snippets etc.", () => { | ||
const currentContents = | ||
dedent(`some content | ||
\`<!-- tree generated by markdown-notes-tree ends here -->\` | ||
markdownForTree`) + endOfLine; | ||
expect(() => | ||
fileContents.getNewMainReadmeContents(currentContents, "markdownForTree", { | ||
endOfLine | ||
}) | ||
).not.toThrow(); | ||
}); | ||
}); | ||
describe("getNewDirectoryReadmeContents", () => { | ||
test("it should handle empty current contents", () => { | ||
const currentContents = ""; | ||
test("it should handle empty description", () => { | ||
const result = fileContents.getNewDirectoryReadmeContents( | ||
"name", | ||
currentContents, | ||
"", | ||
"markdownForTree", | ||
@@ -256,13 +307,6 @@ { endOfLine } | ||
test("it should handle current contents without description markers (as generated by older version)", () => { | ||
const currentContents = | ||
dedent(`<!-- generated by markdown-notes-tree --> | ||
# name | ||
markdownForTree`) + endOfLine; | ||
test("it should handle non-empty description", () => { | ||
const result = fileContents.getNewDirectoryReadmeContents( | ||
"name", | ||
currentContents, | ||
"This is a description.", | ||
"markdownForTree", | ||
@@ -279,2 +323,4 @@ { endOfLine } | ||
This is a description. | ||
<!-- optional markdown-notes-tree directory description ends here --> | ||
@@ -286,4 +332,12 @@ | ||
}); | ||
}); | ||
test("it should handle current contents without description between markers", () => { | ||
describe("getDirectoryDescriptionFromCurrentContents", () => { | ||
test("it should handle empty current contents", () => { | ||
const currentContents = ""; | ||
const result = fileContents.getDirectoryDescriptionFromCurrentContents(currentContents); | ||
expect(result).toBe(""); | ||
}); | ||
test("it should handle current contents without description markers (as generated by older version)", () => { | ||
const currentContents = | ||
@@ -293,17 +347,11 @@ dedent(`<!-- generated by markdown-notes-tree --> | ||
# name | ||
<!-- optional markdown-notes-tree directory description starts here --> | ||
<!-- optional markdown-notes-tree directory description ends here --> | ||
markdownForTree`) + endOfLine; | ||
const result = fileContents.getNewDirectoryReadmeContents( | ||
"name", | ||
currentContents, | ||
"markdownForTree", | ||
{ endOfLine } | ||
); | ||
const result = fileContents.getDirectoryDescriptionFromCurrentContents(currentContents); | ||
expect(result).toBe(""); | ||
}); | ||
const expected = | ||
test("it should handle current contents without description between markers", () => { | ||
const currentContents = | ||
dedent(`<!-- generated by markdown-notes-tree --> | ||
@@ -319,3 +367,4 @@ | ||
expect(result).toBe(expected); | ||
const result = fileContents.getDirectoryDescriptionFromCurrentContents(currentContents); | ||
expect(result).toBe(""); | ||
}); | ||
@@ -337,23 +386,4 @@ | ||
const result = fileContents.getNewDirectoryReadmeContents( | ||
"name", | ||
currentContents, | ||
"markdownForTree", | ||
{ endOfLine } | ||
); | ||
const expected = | ||
dedent(`<!-- generated by markdown-notes-tree --> | ||
# name | ||
<!-- optional markdown-notes-tree directory description starts here --> | ||
This is a description. | ||
<!-- optional markdown-notes-tree directory description ends here --> | ||
markdownForTree`) + endOfLine; | ||
expect(result).toBe(expected); | ||
const result = fileContents.getDirectoryDescriptionFromCurrentContents(currentContents); | ||
expect(result).toBe("This is a description."); | ||
}); | ||
@@ -375,23 +405,4 @@ | ||
const result = fileContents.getNewDirectoryReadmeContents( | ||
"name", | ||
currentContents, | ||
"markdownForTree", | ||
{ endOfLine } | ||
); | ||
const expected = | ||
dedent(`<!-- generated by markdown-notes-tree --> | ||
# name | ||
<!-- optional markdown-notes-tree directory description starts here --> | ||
This is a description. | ||
<!-- optional markdown-notes-tree directory description ends here --> | ||
markdownForTree`) + endOfLine; | ||
expect(result).toBe(expected); | ||
const result = fileContents.getDirectoryDescriptionFromCurrentContents(currentContents); | ||
expect(result).toBe("This is a description."); | ||
}); | ||
@@ -412,8 +423,3 @@ | ||
expect(() => | ||
fileContents.getNewDirectoryReadmeContents( | ||
"name", | ||
currentContents, | ||
"markdownForTree", | ||
{ endOfLine } | ||
) | ||
fileContents.getDirectoryDescriptionFromCurrentContents(currentContents) | ||
).toThrow( | ||
@@ -423,36 +429,3 @@ "Invalid file structure: only one description marker found or end marker found before start marker" | ||
}); | ||
test("it should preserve the title from the current contents if provided", () => { | ||
const currentContents = | ||
dedent(`<!-- generated by markdown-notes-tree --> | ||
# Custom title goes here | ||
<!-- optional markdown-notes-tree directory description starts here --> | ||
<!-- optional markdown-notes-tree directory description ends here --> | ||
markdownForTree`) + endOfLine; | ||
const result = fileContents.getNewDirectoryReadmeContents( | ||
"name", | ||
currentContents, | ||
"markdownForTree", | ||
{ endOfLine } | ||
); | ||
const expected = | ||
dedent(`<!-- generated by markdown-notes-tree --> | ||
# Custom title goes here | ||
<!-- optional markdown-notes-tree directory description starts here --> | ||
<!-- optional markdown-notes-tree directory description ends here --> | ||
markdownForTree`) + endOfLine; | ||
expect(result).toBe(expected); | ||
}); | ||
}); | ||
}); |
@@ -48,6 +48,7 @@ "use strict"; | ||
const readmeContents = getCurrentContents(relativeReadmePath); | ||
const readmeTitle = getTitleFromMarkdownFile(readmeContents, relativeReadmePath); | ||
treeNodes.push({ | ||
isDirectory: true, | ||
title: fileContents.getTitleFromMarkdownContents(readmeContents) || directory.name, | ||
title: readmeTitle || directory.name, | ||
description: getDescriptionFromDirectoryReadmeContents( | ||
@@ -72,6 +73,7 @@ readmeContents, | ||
const relativePath = path.join(relativeParentPath, file.name); | ||
const contents = getCurrentContents(relativePath); | ||
treeNodes.push({ | ||
isDirectory: false, | ||
title: getTitleFromMarkdownFileOrThrow(relativePath), | ||
title: getTitleFromMarkdownFileOrThrow(contents, relativePath), | ||
filename: file.name | ||
@@ -108,6 +110,14 @@ }); | ||
function getTitleFromMarkdownFileOrThrow(relativePath) { | ||
const contents = getCurrentContents(relativePath); | ||
const title = fileContents.getTitleFromMarkdownContents(contents); | ||
function getTitleFromMarkdownFile(contents, relativePath) { | ||
try { | ||
return fileContents.getTitleFromMarkdownContents(contents); | ||
} catch (error) { | ||
const absolutePath = pathUtils.getAbsolutePath(relativePath); | ||
throw new Error(`Cannot get title from file ${absolutePath}: ${error.message}`); | ||
} | ||
} | ||
function getTitleFromMarkdownFileOrThrow(contents, relativePath) { | ||
const title = getTitleFromMarkdownFile(contents, relativePath); | ||
if (!title) { | ||
@@ -114,0 +124,0 @@ const absolutePath = pathUtils.getAbsolutePath(relativePath); |
@@ -37,3 +37,4 @@ "use strict"; | ||
[treeNode.filename], | ||
treeNode.filename, | ||
treeNode.title, | ||
treeNode.description, | ||
treeNode.children, | ||
@@ -46,4 +47,4 @@ environment | ||
function writeTreesForDirectory(pathParts, name, treeForDirectory, environment) { | ||
writeTreeToDirectoryReadme(pathParts, name, treeForDirectory, environment); | ||
function writeTreesForDirectory(pathParts, title, description, treeForDirectory, environment) { | ||
writeTreeToDirectoryReadme(pathParts, title, description, treeForDirectory, environment); | ||
@@ -54,3 +55,4 @@ for (const treeNode of treeForDirectory) { | ||
[...pathParts, treeNode.filename], | ||
treeNode.filename, | ||
treeNode.title, | ||
treeNode.description, | ||
treeNode.children, | ||
@@ -63,3 +65,3 @@ environment | ||
function writeTreeToDirectoryReadme(pathParts, name, treeForDirectory, environment) { | ||
function writeTreeToDirectoryReadme(pathParts, title, description, treeForDirectory, environment) { | ||
const filePathParts = [...pathParts, "README.md"]; | ||
@@ -78,4 +80,4 @@ const relativeFilePath = path.join(...filePathParts); | ||
const newContents = fileContents.getNewDirectoryReadmeContents( | ||
name, | ||
currentContents, | ||
title, | ||
description, | ||
markdownForTree, | ||
@@ -82,0 +84,0 @@ environment |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
61112
20
1214
127
4
2
+ Added@types/mdast@3.0.15(transitive)
+ Added@types/unist@2.0.11(transitive)
+ Addedcharacter-entities@1.2.4(transitive)
+ Addedcharacter-entities-legacy@1.1.4(transitive)
+ Addedcharacter-reference-invalid@1.1.4(transitive)
+ Addeddebug@4.3.7(transitive)
+ Addedis-alphabetical@1.0.4(transitive)
+ Addedis-alphanumerical@1.0.4(transitive)
+ Addedis-decimal@1.0.4(transitive)
+ Addedis-hexadecimal@1.0.4(transitive)
+ Addedmdast-util-from-markdown@0.8.5(transitive)
+ Addedmdast-util-to-string@2.0.0(transitive)
+ Addedmicromark@2.11.4(transitive)
+ Addedms@2.1.3(transitive)
+ Addedparse-entities@2.0.0(transitive)
+ Addedunist-util-stringify-position@2.0.3(transitive)