markdown-notes-tree
Advanced tools
Comparing version 1.7.0 to 1.8.0
{ | ||
"name": "markdown-notes-tree", | ||
"version": "1.7.0", | ||
"version": "1.8.0", | ||
"description": "Generate Markdown trees that act as a table of contents for a folder structure with Markdown notes", | ||
@@ -10,4 +10,5 @@ "main": "src/index.js", | ||
"scripts": { | ||
"start": "node src/index.js", | ||
"test": "jest" | ||
"test": "jest --collectCoverage", | ||
"check-formatting": "prettier-check *.js src/*.js", | ||
"build": "npm run check-formatting && npm run test" | ||
}, | ||
@@ -26,10 +27,12 @@ "keywords": [ | ||
"devDependencies": { | ||
"codecov": "^3.6.5", | ||
"dedent": "^0.7.0", | ||
"fs-extra": "^8.1.0", | ||
"jest": "^24.9.0", | ||
"prettier": "^1.19.1" | ||
"prettier": "^1.19.1", | ||
"prettier-check": "^2.0.0" | ||
}, | ||
"dependencies": { | ||
"minimatch": "^3.0.4", | ||
"minimist": "^1.2.0" | ||
"minimist": "^1.2.5" | ||
}, | ||
@@ -36,0 +39,0 @@ "files": [ |
# markdown-notes-tree | ||
[![Codecov Coverage](https://img.shields.io/codecov/c/github/mistermicheels/markdown-notes-tree?style=flat)](https://codecov.io/gh/mistermicheels/markdown-notes-tree/) | ||
[![npm](https://img.shields.io/npm/v/markdown-notes-tree?style=flat)](https://www.npmjs.com/package/markdown-notes-tree) | ||
If you have a folder structure with Markdown notes, you can use this tool to generate Markdown trees that act as a table of contents for the folder structure. | ||
@@ -38,2 +41,6 @@ | ||
## Subdirectory descriptions | ||
The generated `README.md` files for subdirectories provide some space to add a description for the directory. If a description is provided, it will be preserved and it will also be included in generated trees containing the directory. | ||
## Command line arguments | ||
@@ -57,3 +64,13 @@ | ||
- Example: `markdown-notes-tree --silent` | ||
- `--subdirectoryDescriptionOnNewLine`: If subdirectory descriptions are provided, put them on a new line in the tree. | ||
- Example: `markdown-notes-tree --subdirectoryDescriptionOnNewLine` | ||
- `--useTabs`: Use tabs (instead of the standard four spaces) for indentation. | ||
- Example: `markdown-notes-tree --useTabs` | ||
## Development | ||
This project is using Prettier to format the JavaScript code. Installing the VS Code plugin recommended through the `extensions.json` file should make this easy. | ||
The build script verifies that the formatting actually matches Prettier style and that the unit and integration tests are passing. | ||
During development, you can run the tool on a folder by navigating to the folder in the command line and then executing `node path/to/cli.js`, adding arguments as needed. Example: `node ../../../cli.js --silent` |
@@ -7,5 +7,16 @@ "use strict"; | ||
getNewMainReadmeContents, | ||
getDirectoryReadmeContents | ||
getNewDirectoryReadmeContents, | ||
getDirectoryDescriptionFromCurrentContents | ||
}; | ||
const markers = { | ||
mainReadmeTreeStart: "<!-- auto-generated notes tree starts here -->", | ||
mainReadmeTreeEnd: "<!-- auto-generated notes tree ends here -->", | ||
directoryReadmeGeneratedStart: "<!-- this entire file is auto-generated -->", | ||
directoryReadmeDescriptionStart: | ||
"<!-- optional markdown-notes-tree directory description starts here -->", | ||
directoryReadmeDescriptionEnd: | ||
"<!-- optional markdown-notes-tree directory description ends here -->" | ||
}; | ||
function getTitleFromMarkdownContents(contents) { | ||
@@ -22,7 +33,7 @@ const firstLine = contents.split(/\r\n|\r|\n/, 1)[0]; | ||
function getMarkdownForTree(tree, endOfLine, options) { | ||
const lines = getMarkdownLinesForTree(tree, [], options); | ||
const lines = getMarkdownLinesForTree(tree, [], endOfLine, options); | ||
return lines.join(endOfLine); | ||
} | ||
function getMarkdownLinesForTree(tree, parentPathParts, options) { | ||
function getMarkdownLinesForTree(tree, parentPathParts, endOfLine, options) { | ||
const markdownLines = []; | ||
@@ -32,4 +43,11 @@ const indentationUnit = getIndentationUnit(options); | ||
for (const treeNode of tree) { | ||
markdownLines.push(getMarkdownLineForTreeNode(treeNode, parentPathParts, options)); | ||
const markdownForTreeNode = getMarkdownForTreeNode( | ||
treeNode, | ||
parentPathParts, | ||
endOfLine, | ||
options | ||
); | ||
markdownLines.push(...markdownForTreeNode.split(endOfLine)); | ||
if (treeNode.isDirectory) { | ||
@@ -41,2 +59,3 @@ const fullPathParts = [...parentPathParts, treeNode.filename]; | ||
fullPathParts, | ||
endOfLine, | ||
options | ||
@@ -62,6 +81,14 @@ ); | ||
function getMarkdownLineForTreeNode(treeNode, parentPath, options) { | ||
function getMarkdownForTreeNode(treeNode, parentPath, endOfLine, options) { | ||
const linkText = getLinkTextForTreeNode(treeNode); | ||
const linkTarget = getLinkTargetForTreeNode(treeNode, parentPath, options); | ||
return `- [${linkText}](${linkTarget})`; | ||
const basicLine = `- [${linkText}](${linkTarget})`; | ||
if (treeNode.description) { | ||
const descriptionSeparator = getDescriptionSeparator(endOfLine, options); | ||
return basicLine + descriptionSeparator + treeNode.description; | ||
} else { | ||
return basicLine; | ||
} | ||
} | ||
@@ -88,46 +115,63 @@ | ||
function getDescriptionSeparator(endOfLine, options) { | ||
if (options.subdirectoryDescriptionOnNewLine) { | ||
return " " + endOfLine + getIndentationUnit(options); | ||
} else { | ||
return " - "; | ||
} | ||
} | ||
function getNewMainReadmeContents(currentContents, markdownForTree, endOfLine) { | ||
const treeStartMarker = "<!-- auto-generated notes tree starts here -->"; | ||
const treeEndMarker = "<!-- auto-generated notes tree ends here -->"; | ||
const indexTreeStartMarker = currentContents.indexOf(markers.mainReadmeTreeStart); | ||
let contentsBeforeTree; | ||
const indexStartMarker = currentContents.indexOf(treeStartMarker); | ||
let contentsBeforeStartMarker; | ||
if (indexStartMarker >= 0) { | ||
contentsBeforeStartMarker = currentContents.substring(0, indexStartMarker); | ||
if (indexTreeStartMarker >= 0) { | ||
contentsBeforeTree = currentContents.substring(0, indexTreeStartMarker); | ||
} else { | ||
contentsBeforeStartMarker = currentContents + endOfLine.repeat(2); | ||
contentsBeforeTree = currentContents + endOfLine.repeat(2); | ||
} | ||
const indexEndMarker = currentContents.indexOf(treeEndMarker); | ||
let contentsAfterEndMarker; | ||
const indexTreeEndMarker = currentContents.indexOf(markers.mainReadmeTreeEnd); | ||
let contentsAfterTree; | ||
if (indexEndMarker >= 0 && indexEndMarker < indexStartMarker) { | ||
if (indexTreeEndMarker >= 0 && indexTreeEndMarker < indexTreeStartMarker) { | ||
throw new Error("Invalid file structure: tree end marker found before tree start marker"); | ||
} else if (indexEndMarker >= 0) { | ||
contentsAfterEndMarker = currentContents.substring(indexEndMarker + treeEndMarker.length); | ||
} else if (indexTreeEndMarker >= 0) { | ||
contentsAfterTree = currentContents.substring( | ||
indexTreeEndMarker + markers.mainReadmeTreeEnd.length | ||
); | ||
} else { | ||
contentsAfterEndMarker = endOfLine; | ||
contentsAfterTree = endOfLine; | ||
} | ||
return ( | ||
contentsBeforeStartMarker + | ||
treeStartMarker + | ||
contentsBeforeTree + | ||
markers.mainReadmeTreeStart + | ||
endOfLine.repeat(2) + | ||
markdownForTree + | ||
endOfLine.repeat(2) + | ||
treeEndMarker + | ||
contentsAfterEndMarker | ||
markers.mainReadmeTreeEnd + | ||
contentsAfterTree | ||
); | ||
} | ||
function getDirectoryReadmeContents(name, markdownForTree, endOfLine) { | ||
const autoGenerationComment = "<!-- this entire file is auto-generated -->"; | ||
function getNewDirectoryReadmeContents(name, currentContents, markdownForTree, endOfLine) { | ||
const title = `# ${name}`; | ||
const description = getDirectoryDescriptionFromCurrentContents(currentContents); | ||
let partBetweenDescriptionMarkers = endOfLine.repeat(2); | ||
if (description) { | ||
partBetweenDescriptionMarkers = endOfLine.repeat(2) + description + endOfLine.repeat(2); | ||
} | ||
return ( | ||
autoGenerationComment + | ||
markers.directoryReadmeGeneratedStart + | ||
endOfLine.repeat(2) + | ||
title + | ||
endOfLine.repeat(2) + | ||
markers.directoryReadmeDescriptionStart + | ||
partBetweenDescriptionMarkers + | ||
markers.directoryReadmeDescriptionEnd + | ||
endOfLine.repeat(2) + | ||
markdownForTree + | ||
@@ -137,1 +181,31 @@ endOfLine | ||
} | ||
function getDirectoryDescriptionFromCurrentContents(currentContents) { | ||
const indexDescriptionStartMarker = currentContents.indexOf( | ||
markers.directoryReadmeDescriptionStart | ||
); | ||
const indexDescriptionEndMarker = currentContents.indexOf( | ||
markers.directoryReadmeDescriptionEnd | ||
); | ||
const validMarkers = | ||
indexDescriptionStartMarker >= 0 && | ||
indexDescriptionEndMarker >= 0 && | ||
indexDescriptionEndMarker > indexDescriptionStartMarker; | ||
if (validMarkers) { | ||
const descriptionStart = | ||
indexDescriptionStartMarker + markers.directoryReadmeDescriptionStart.length; | ||
const descriptionEnd = indexDescriptionEndMarker; | ||
return currentContents.substring(descriptionStart, descriptionEnd).trim(); | ||
} else if (indexDescriptionStartMarker >= 0 || indexDescriptionEndMarker >= 0) { | ||
throw new Error( | ||
"Invalid file structure: only one description marker found or end marker found before start marker" | ||
); | ||
} else { | ||
return ""; | ||
} | ||
} |
@@ -49,5 +49,2 @@ "use strict"; | ||
test("it should generate a tree with proper formatting and indentation", () => { | ||
const expected = | ||
"- [**sub1**](sub1)" + endOfLine + " - [Title for file1a](sub1/file1a.md)"; | ||
const result = fileContents.getMarkdownForTree(tree, endOfLine, { | ||
@@ -58,2 +55,5 @@ linkToSubdirectoryReadme: false, | ||
const expected = | ||
"- [**sub1**](sub1)" + endOfLine + " - [Title for file1a](sub1/file1a.md)"; | ||
expect(result).toEqual(expected); | ||
@@ -63,2 +63,7 @@ }); | ||
test("it should allow linking directly to subdirectory README files", () => { | ||
const result = fileContents.getMarkdownForTree(tree, endOfLine, { | ||
linkToSubdirectoryReadme: true, | ||
useTabs: false | ||
}); | ||
const expected = | ||
@@ -69,7 +74,2 @@ "- [**sub1**](sub1/README.md)" + | ||
const result = fileContents.getMarkdownForTree(tree, endOfLine, { | ||
linkToSubdirectoryReadme: true, | ||
useTabs: false | ||
}); | ||
expect(result).toEqual(expected); | ||
@@ -79,5 +79,2 @@ }); | ||
test("it should allow using tabs instead of spaces", () => { | ||
const expected = | ||
"- [**sub1**](sub1)" + endOfLine + "\t- [Title for file1a](sub1/file1a.md)"; | ||
const result = fileContents.getMarkdownForTree(tree, endOfLine, { | ||
@@ -88,4 +85,78 @@ linkToSubdirectoryReadme: false, | ||
const expected = | ||
"- [**sub1**](sub1)" + endOfLine + "\t- [Title for file1a](sub1/file1a.md)"; | ||
expect(result).toEqual(expected); | ||
}); | ||
describe("for a tree with description", () => { | ||
const treeIncludingFolderDescription = [ | ||
{ | ||
isDirectory: true, | ||
title: "sub1", | ||
description: "description1", | ||
filename: "sub1", | ||
children: [ | ||
{ | ||
isDirectory: true, | ||
title: "sub1a", | ||
description: "description1a", | ||
filename: "sub1a", | ||
children: [] | ||
}, | ||
{ | ||
isDirectory: false, | ||
title: "Title for file1a", | ||
filename: "file1a.md" | ||
} | ||
] | ||
} | ||
]; | ||
test("it should include the description", () => { | ||
const result = fileContents.getMarkdownForTree( | ||
treeIncludingFolderDescription, | ||
endOfLine, | ||
{ | ||
linkToSubdirectoryReadme: false, | ||
subdirectoryDescriptionOnNewLine: false, | ||
useTabs: false | ||
} | ||
); | ||
const expected = | ||
"- [**sub1**](sub1) - description1" + | ||
endOfLine + | ||
" - [**sub1a**](sub1/sub1a) - description1a" + | ||
endOfLine + | ||
" - [Title for file1a](sub1/file1a.md)"; | ||
expect(result).toEqual(expected); | ||
}); | ||
test("it should allow putting the description on a new line", () => { | ||
const result = fileContents.getMarkdownForTree( | ||
treeIncludingFolderDescription, | ||
endOfLine, | ||
{ | ||
linkToSubdirectoryReadme: false, | ||
subdirectoryDescriptionOnNewLine: true, | ||
useTabs: false | ||
} | ||
); | ||
const expected = | ||
"- [**sub1**](sub1) " + | ||
endOfLine + | ||
" description1" + | ||
endOfLine + | ||
" - [**sub1a**](sub1/sub1a) " + | ||
endOfLine + | ||
" description1a" + | ||
endOfLine + | ||
" - [Title for file1a](sub1/file1a.md)"; | ||
expect(result).toEqual(expected); | ||
}); | ||
}); | ||
}); | ||
@@ -189,6 +260,9 @@ | ||
describe("getDirectoryReadmeContents", () => { | ||
test("it should return the contents with marker, title and tree", () => { | ||
const result = fileContents.getDirectoryReadmeContents( | ||
describe("getNewDirectoryReadmeContents", () => { | ||
test("it should handle empty current contents", () => { | ||
const currentContents = ""; | ||
const result = fileContents.getNewDirectoryReadmeContents( | ||
"name", | ||
currentContents, | ||
"markdownForTree", | ||
@@ -202,2 +276,6 @@ endOfLine | ||
# name | ||
<!-- optional markdown-notes-tree directory description starts here --> | ||
<!-- optional markdown-notes-tree directory description ends here --> | ||
@@ -208,3 +286,126 @@ markdownForTree`) + endOfLine; | ||
}); | ||
test("it should handle current contents without description markers (as generated by older version)", () => { | ||
const currentContents = | ||
dedent(`<!-- this entire file is auto-generated --> | ||
# name | ||
markdownForTree`) + endOfLine; | ||
const result = fileContents.getNewDirectoryReadmeContents( | ||
"name", | ||
currentContents, | ||
"markdownForTree", | ||
endOfLine | ||
); | ||
const expected = | ||
dedent(`<!-- this entire file is auto-generated --> | ||
# name | ||
<!-- optional markdown-notes-tree directory description starts here --> | ||
<!-- optional markdown-notes-tree directory description ends here --> | ||
markdownForTree`) + endOfLine; | ||
expect(result).toBe(expected); | ||
}); | ||
test("it should handle current contents without description between markers", () => { | ||
const currentContents = | ||
dedent(`<!-- this entire file is auto-generated --> | ||
# 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 expected = | ||
dedent(`<!-- this entire file is auto-generated --> | ||
# name | ||
<!-- optional markdown-notes-tree directory description starts here --> | ||
<!-- optional markdown-notes-tree directory description ends here --> | ||
markdownForTree`) + endOfLine; | ||
expect(result).toBe(expected); | ||
}); | ||
test("it should handle current contents with description between markers", () => { | ||
const currentContents = | ||
dedent(`<!-- this entire file is auto-generated --> | ||
# name | ||
<!-- optional markdown-notes-tree directory description starts here --> | ||
This is a description. | ||
<!-- optional markdown-notes-tree directory description ends here --> | ||
markdownForTree`) + endOfLine; | ||
const result = fileContents.getNewDirectoryReadmeContents( | ||
"name", | ||
currentContents, | ||
"markdownForTree", | ||
endOfLine | ||
); | ||
const expected = | ||
dedent(`<!-- this entire file is auto-generated --> | ||
# 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); | ||
}); | ||
test("it should fail for current contents having invalid markers", () => { | ||
const currentContents = | ||
dedent(`<!-- this entire file is auto-generated --> | ||
# name | ||
<!-- optional markdown-notes-tree directory description ends here --> | ||
<!-- optional markdown-notes-tree directory description starts here --> | ||
markdownForTree`) + endOfLine; | ||
expect(() => | ||
fileContents.getNewDirectoryReadmeContents( | ||
"name", | ||
currentContents, | ||
"markdownForTree", | ||
endOfLine | ||
) | ||
).toThrow( | ||
"Invalid file structure: only one description marker found or end marker found before start marker" | ||
); | ||
}); | ||
}); | ||
}); |
@@ -18,2 +18,3 @@ "use strict"; | ||
silent: parsedArguments.silent || false, | ||
subdirectoryDescriptionOnNewLine: parsedArguments.subdirectoryDescriptionOnNewLine || false, | ||
useTabs: parsedArguments.useTabs || false | ||
@@ -20,0 +21,0 @@ }; |
@@ -16,2 +16,3 @@ "use strict"; | ||
silent: false, | ||
subdirectoryDescriptionOnNewLine: false, | ||
useTabs: false | ||
@@ -18,0 +19,0 @@ }; |
@@ -45,2 +45,3 @@ "use strict"; | ||
title: directory.name, | ||
description: getDescriptionFromDirectoryReadme(relativePath), | ||
filename: directory.name, | ||
@@ -60,5 +61,7 @@ children: buildTreeStartingAt(relativePath, options) | ||
if (!ignores.shouldIgnoreFile(file.name, relativeParentPath, options)) { | ||
const relativePath = path.join(relativeParentPath, file.name); | ||
treeNodes.push({ | ||
isDirectory: false, | ||
title: getTitleFromMarkdownFileOrThrow(path.join(relativeParentPath, file.name)), | ||
title: getTitleFromMarkdownFileOrThrow(relativePath), | ||
filename: file.name | ||
@@ -87,1 +90,12 @@ }); | ||
} | ||
function getDescriptionFromDirectoryReadme(relativeDirectoryPath) { | ||
const absolutePath = pathUtils.getAbsolutePath(path.join(relativeDirectoryPath, "README.md")); | ||
if (fs.existsSync(absolutePath)) { | ||
const contents = fs.readFileSync(absolutePath, { encoding: "utf-8" }); | ||
return fileContents.getDirectoryDescriptionFromCurrentContents(contents); | ||
} else { | ||
return ""; | ||
} | ||
} |
@@ -58,5 +58,2 @@ "use strict"; | ||
function writeTreeToDirectoryReadme(pathParts, name, treeForDirectory, endOfLine, options, logger) { | ||
const markdownForTree = fileContents.getMarkdownForTree(treeForDirectory, endOfLine, options); | ||
const contents = fileContents.getDirectoryReadmeContents(name, markdownForTree, endOfLine); | ||
const filePathParts = [...pathParts, "README.md"]; | ||
@@ -66,4 +63,19 @@ const relativeFilePath = path.join(...filePathParts); | ||
let currentContents = ""; | ||
if (fs.existsSync(absoluteFilePath)) { | ||
currentContents = fs.readFileSync(absoluteFilePath, { encoding: "utf-8" }); | ||
} | ||
const markdownForTree = fileContents.getMarkdownForTree(treeForDirectory, endOfLine, options); | ||
const newContents = fileContents.getNewDirectoryReadmeContents( | ||
name, | ||
currentContents, | ||
markdownForTree, | ||
endOfLine | ||
); | ||
logger(`Writing to ${absoluteFilePath}`); | ||
fs.writeFileSync(absoluteFilePath, contents, { encoding: "utf-8" }); | ||
fs.writeFileSync(absoluteFilePath, newContents, { encoding: "utf-8" }); | ||
} |
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
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
44101
854
75
6
Updatedminimist@^1.2.5