@sytone/markdown-snippet-injector
Version:
The MarkDown snippet injector generates MD code snippets by extracting them from the source code of your projects.
360 lines (354 loc) • 17.5 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;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SnippetInjector = void 0;
/* eslint-disable @typescript-eslint/naming-convention */
/* eslint-disable import/extensions */
const fs = __importStar(require("node:fs"));
const path = __importStar(require("node:path"));
const os = __importStar(require("node:os"));
const log_1 = require("./log");
const snippet_1 = require("./snippet");
const source_file_1 = require("./source-file");
const ws = '[\\t ]*';
class SnippetInjector {
programOptions;
storedSnippets = new Map();
snippetTitles = '';
sourceFileExtensionFilter = '';
targetFileExtensionFilter = '';
toWrap = true;
useOsEol = false;
endOfLineValue = '\n';
addAutoGeneratedFooter = false;
placeholderPrefix = '%%';
placeholderSuffix = '%%';
_storedSourceTypes = [];
_storedTargetTypes = [];
_storedSourceTitles = {};
constructor(programOptions) {
this.programOptions = programOptions;
this.programOptions = programOptions;
this.sourceFileExtensionFilter = programOptions.sourceFileExtensionFilter;
this.targetFileExtensionFilter = programOptions.targetFileExtensionFilter;
this.placeholderPrefix = programOptions.placeholderPrefix;
this.placeholderSuffix = programOptions.placeholderSuffix;
this.snippetTitles = programOptions.snippetTitles;
this.toWrap = programOptions.wrap;
this.useOsEol = programOptions.useOsEol;
this.addAutoGeneratedFooter = programOptions.addAutoGeneratedFooter;
if (this.useOsEol) {
this.endOfLineValue = os.EOL;
}
}
/*
// >> id='snippetinjector-process'
Loads the code snippets from the source-tree at the specified location.
@param root The root of the source-tree to load the snippets from.
// << snippetinjector-process
*/
process(root) {
const lStat = fs.lstatSync(root);
this.init();
for (const storedSourceType of this._storedSourceTypes) {
if (lStat.isDirectory()) {
this.processDirectory(root, storedSourceType);
}
else if (lStat.isFile()) {
this.processFile(root, storedSourceType);
}
}
}
/*
// >> id='snippetinjector-injectSnippets' options='file=injectSnippets.md'
Loads the code snippets from the source-tree at the specified location.
@param root The root of the source-tree to load the snippets from.
// << snippetinjector-injectSnippets
*/
injectSnippets(docsRoot) {
if (this.storedSnippets.size > 0) {
// Look for snippets that have an explicit file option and create
// the file with the snippet content. Adding header and footer content
// if specified or found.
for (const [_, value] of this.storedSnippets) {
const { file, header, footer, processedValue } = value;
/*
// >> id='snippet-injector-implicit-header-footer' options='file=implicit-header-footer/injectSnippets.md'
title: Snippet Injector - Implicit Header Footer
---
This section looks for the `_header.md` and `_footer.md` files in the same director as the file
to be created then the root of the docs directory. IF specified the header should be
relative to the root of the docs-root value.
// << snippet-injector-implicit-header-footer
*/
const headerContent = this.getFileContentWithImplicitLookup(header, docsRoot, file, '_header.md');
const footerContent = this.getFileContentWithImplicitLookup(footer, docsRoot, file, '_footer.md');
if (file) {
const filePath = `${docsRoot}/${file}`;
this.ensureDirectoryExistence(filePath);
const dateTime = new Date().toDateString();
const autoGenFooter = this.addAutoGeneratedFooter ? `\n\n${this.placeholderPrefix}This file is auto-generated. Do not edit. Generated at: ${dateTime}${this.placeholderSuffix}` : '';
const fileContents = `${headerContent}${processedValue ?? ''}${footerContent}${autoGenFooter}`;
fs.writeFileSync(filePath, fileContents, 'utf8');
}
}
const lStat = fs.lstatSync(docsRoot);
for (const storedTargetType of this._storedTargetTypes) {
if (lStat.isDirectory()) {
this.processDocsDirectory(docsRoot, storedTargetType);
}
else if (lStat.isFile()) {
this.processDocsFile(docsRoot, storedTargetType);
}
}
}
}
/**
* Returns the content of a file with implicit lookup in the specified directories.
* @param filePathRelativeToDocsRoot - The path to the file relative to the docs root directory.
* @param docsRoot - The root directory of the documentation.
* @param file - The path to the current file.
* @param implicitFileName - The name of the file to look for implicitly.
* @returns The content of the file.
*/
getFileContentWithImplicitLookup(filePathRelativeToDocsRoot, docsRoot, file, implicitFileName) {
log_1.log.debug('filePathRelativeToDocsRoot', filePathRelativeToDocsRoot);
log_1.log.debug('docsRoot', docsRoot);
log_1.log.debug('file', file);
log_1.log.debug('implicitFileName', implicitFileName);
let filePath = '';
if (filePathRelativeToDocsRoot && fs.existsSync(`${docsRoot}/${filePathRelativeToDocsRoot}`)) {
filePath = `${docsRoot}/${filePathRelativeToDocsRoot}`;
}
else if (fs.existsSync(`${path.dirname(`${docsRoot}/${file}`)}/${implicitFileName}`)) {
filePath = `${path.dirname(`${docsRoot}/${file}`)}/${implicitFileName}`;
}
else if (fs.existsSync(`${docsRoot}/${implicitFileName}`)) {
filePath = `${docsRoot}/${implicitFileName}`;
}
log_1.log.info('Getting content for', filePath);
return filePath ? fs.readFileSync(filePath, 'utf8') : '';
}
ensureDirectoryExistence(filePath) {
const dirname = path.dirname(filePath);
if (fs.existsSync(dirname)) {
return true;
}
this.ensureDirectoryExistence(dirname);
fs.mkdirSync(dirname);
}
/*
// >> id='snippetinjector-hasSnippet' options='file=snippetinjector/hassnippet.md&header=snippetinjector/test-header.md&footer=snippetinjector/test-footer.md'
title: Snippet Injector - Has Snippet
---
Loads the code snippets from the source-tree at the specified location.
@param root The root of the source-tree to load the snippets from.
// << snippetinjector-hasSnippet
*/
hasSnippet(snippet) {
return this.storedSnippets.has(snippet.fileExtension + snippet.id);
}
addSnippet(snippet) {
this.storedSnippets.set(snippet.fileExtension + snippet.id, snippet);
}
init() {
log_1.log.info('Source File Extension Filter', this.sourceFileExtensionFilter);
this._storedSourceTypes = this.sourceFileExtensionFilter.split('|');
this._storedTargetTypes = this.targetFileExtensionFilter.split('|');
this._storedSourceTitles = {};
const currentTitles = this.snippetTitles.split('|');
for (let i = 0; i < this._storedSourceTypes.length; i++) {
this._storedSourceTitles[this._storedSourceTypes[i]] = (currentTitles[i] || '');
}
log_1.log.info('Stored Source Titles', this._storedSourceTitles);
}
processDirectory(directory, extensionFilter) {
const files = fs.readdirSync(directory);
for (const currentFile of files) {
const fullPath = path.normalize(directory + '/' + currentFile);
const fileStat = fs.lstatSync(fullPath);
if (fileStat.isDirectory()) {
this.processDirectory(directory + '/' + currentFile, extensionFilter);
}
else if (fileStat.isFile() && path.extname(fullPath) === extensionFilter) {
this.processFile(fullPath, extensionFilter);
}
}
}
processFile(directory, extensionFilter) {
log_1.log.info('Processing source file:', directory, extensionFilter);
const sourceFile = new source_file_1.SourceFile(this.programOptions, extensionFilter, directory);
// Get the format spec based off the file extension.
const spec = sourceFile.spec;
// Create all the regular expressions needed to find the snippets.
const regExpOpen = sourceFile.openRegExp;
const fileContents = sourceFile.fileContents;
// Find all the snippet matches in the file.
let match = regExpOpen.exec(fileContents);
if (match) {
log_1.log.info(`Found ${match?.length} snippet matches`);
}
while (match) {
const matchIndex = match.index;
const matchLength = match[0].length;
log_1.log.info(`Processing ${match[1]}`);
const snippetEntry = new snippet_1.Snippet(this.programOptions, match[0], extensionFilter, sourceFile, spec);
// If the snippet is already in the list, skip it.
if (this.hasSnippet(snippetEntry)) {
match = regExpOpen.exec(fileContents);
continue;
}
// Find the closing tag for the snippet.
// {comment} << the-name-of-the-snippet {/comment}
const regExpCurrentClosingEOF = sourceFile.getClosingEofRegExp(snippetEntry.id);
const closingTagMatchEOF = regExpCurrentClosingEOF.exec(fileContents);
let indexOfClosingTag;
if (closingTagMatchEOF) {
indexOfClosingTag = closingTagMatchEOF.index;
}
else {
// {comment} << the-name-of-the-snippet([^-]){/comment}
const regExpCurrentClosing = sourceFile.getClosingRegExp(snippetEntry.id);
const closingTagMatch = regExpCurrentClosing.exec(fileContents);
if (!closingTagMatch) {
throw new Error('Closing tag not found for: ' + snippetEntry.id);
}
indexOfClosingTag = closingTagMatch.index;
}
snippetEntry.value = fileContents.slice(matchIndex + matchLength, indexOfClosingTag);
log_1.log.debug('Snippet value: ' + snippetEntry.value);
log_1.log.info('Snippet resolved: ' + snippetEntry.id);
this.addSnippet(snippetEntry);
match = regExpOpen.exec(fileContents);
}
}
replaceWrappedSnippetsWithCorrespondingTags(fileContent) {
let content = '';
const regex = new RegExp(`${this.placeholderPrefix}${snippet_1.whitespace.source}*snippet${snippet_1.whitespace.source}+${snippet_1.snippetToken.source}[\\S\\s]*?${this.placeholderSuffix}[\\S\\s]*?${this.placeholderPrefix}\\/snippet${this.placeholderSuffix}`, 'g');
log_1.log.debug('replaceWrappedSnippetsWithCorrespondingTags:', regex);
content = fileContent.replace(regex, `${this.placeholderPrefix}snippet id='$1' options='$2'/${this.placeholderSuffix}`);
log_1.log.debug('replaceWrappedSnippetsWithCorrespondingTags:', content);
return content;
}
wrapSnippetWithComments(snippetTag, snippetId, snippetOptions) {
let wrappedSnippetTag = '';
wrappedSnippetTag += `${this.placeholderPrefix}snippet id='${snippetId}' options='${snippetOptions}'${this.placeholderSuffix}\n`;
wrappedSnippetTag += snippetTag;
wrappedSnippetTag += `\n${this.placeholderPrefix}/snippet${this.placeholderSuffix}`;
return wrappedSnippetTag;
}
processDocsDirectory(directory, extensionFilter) {
const files = fs.readdirSync(directory);
for (const currentFile of files) {
const fullPath = path.normalize(directory + '/' + currentFile);
const fileStat = fs.lstatSync(fullPath);
if (fileStat.isDirectory()) {
this.processDocsDirectory(fullPath, extensionFilter);
}
else if (fileStat.isFile() && path.extname(fullPath) === extensionFilter) {
this.processDocsFile(fullPath, extensionFilter);
}
}
}
// >> id='ts-snippet-with-hidden-section' options=''
div(a, b) {
// >> (hide)
console.log('You should not see this!');
// << (hide)
return a / b;
}
// << ts-snippet-with-hidden-section
/*
// >> id='snippetinjector-processDocsFile' options=''
Handles the injection into the markdown files.
// << snippetinjector-processDocsFile
*/
processDocsFile(directory, extensionFilter) {
log_1.log.info('Processing docs file: ' + directory);
let fileContents = fs.readFileSync(directory, 'utf8');
fileContents = this.replaceWrappedSnippetsWithCorrespondingTags(fileContents);
const regex = new RegExp(`${this.placeholderPrefix}${snippet_1.whitespace.source}*snippet${snippet_1.whitespace.source}+${snippet_1.snippetToken.source}[\\s]*/[\\s]*${this.placeholderSuffix}`, 'g');
log_1.log.debug('processDocsFile:', regex);
let match = regex.exec(fileContents);
log_1.log.debug('processDocsFile:', match);
let hadMatches = false;
while (match) {
const matchedString = match[0];
const placeholderId = match[1];
const placeholderOptions = match[2];
let finalSnippet = '';
log_1.log.info('Placeholder resolved: ' + matchedString);
for (const storedSourceType of this._storedSourceTypes) {
const currentSourceType = storedSourceType;
const snippetForSourceType = this.storedSnippets.get(currentSourceType + placeholderId);
if (snippetForSourceType !== undefined) {
log_1.log.debug('Snippet Token', snippetForSourceType.token);
log_1.log.debug('Snippet Original Value', snippetForSourceType.originalValue);
hadMatches = true;
if (finalSnippet.length > 0) {
finalSnippet += this.endOfLineValue;
}
if (placeholderOptions.includes('nocodeblock')) {
finalSnippet += snippetForSourceType.processedValue;
}
else {
const currentSnippetTitle = this._storedSourceTitles[currentSourceType] || '';
finalSnippet += '```' + currentSnippetTitle + this.endOfLineValue + snippetForSourceType.processedValue + this.endOfLineValue + '```';
}
log_1.log.debug('Final Snippet:', finalSnippet);
}
}
if (finalSnippet.length > 0) {
/*
Check whether it should be wrapped or replaced.
If the tag is closed it will be replaced by the snippet.
From:
<snippet id="snippetId"/>
To:
{your_snippet}
If there is open and closed tag the snippet will be wrapped around snippet tag.
From:
<snippet id="snippetId"></snippet>
To:
<snippet id="snippetId">
{your_snippet}
</snippet>
*/
if (this.toWrap) {
const temporaryMatchedString = this.wrapSnippetWithComments(matchedString, placeholderId, placeholderOptions);
fileContents = fileContents.replace(matchedString, temporaryMatchedString);
}
fileContents = fileContents.replace(matchedString, finalSnippet);
log_1.log.info('Token replaced: ' + matchedString);
}
match = regex.exec(fileContents);
}
if (hadMatches) {
fs.writeFileSync(directory, fileContents, 'utf8');
}
}
}
exports.SnippetInjector = SnippetInjector;
//# sourceMappingURL=snippet-injector.js.map