// convert our markdown documentation files // to 'static' html/ejs partials: // while this is a bit inconvenient (you need to restart // the server everytime you want to see // md changes), it is more efficient in // that we aren't converting MD -> ejs // on EVERY request const showdown = require('showdown'), showdownHighlight = require("showdown-highlight"), fs = require('fs'), mkdirp = require('mkdirp'), recipeInputDir = './recipes/', recipeOutputDir = './views/partials/md/recipes/', recipeListGeneratedOutputDir = './views/partials/generated/', projectClassMap = { h1: 'display-1' //tag type : class to add to all tags of that type (class="display-1" added to all

) }; const { assert } = require('console'); function addClassToTag(text, classMap) { var modifiedText = text; Object.keys(classMap).forEach(function (key) { var regex = new RegExp(`<(${key})(.*?)>`, 'g'); matcher = regex.exec(modifiedText); // only proceed if we found a match, and the class we add isn't already on the tag somehow while (matcher != null && !matcher[2].includes(classMap[key])) { // add the class content WHILE preserving any other properties already in the tag! console.log("adding class content in: " + matcher[0]); var restOfTag = matcher[2]; modifiedText = modifiedText.replace(matcher[0], `<${key} class="${classMap[key]}" ${restOfTag}>`); matcher = regex.exec(modifiedText); } }); return modifiedText; } // handles adding classes to specific // tag types automatically in project writeups const projectsAddHeaderClass = { type: 'output', // when it's triggered -> output is at the very end when text is html filter: text => { return addClassToTag(text, projectClassMap); } }; function convertRecipeMarkdown(inputDir, outputDir) { var md = require('markdown-it')() .use(require('markdown-it-hashtag')); md.renderer.rules.hashtag_open = function (tokens, idx) { var tagName = tokens[idx].content.toLowerCase(); return ''; } md.renderer.rules.hashtag_close = function () { return ''; } // This is a hardcoded markdown header section number to html file name // // Example.md: // """ // ... maybe some other header info here -| - exported as filename-title.ejs // # Delicious Recipe Name -| // Catch phrase or yield -| // | - exported as filename-subtitle // image of the food | // -| // ## Ingredients -| // ... ingredients table, etc | - exported as filename-ingredients.ejs // -| // ## Instructions // """ // // NOTE: these titles are HARDCODED in recipe_template.ejs! const mdSectionHtmlTitles = [ 'title', // 'subtitle', 'ingredients', 'instructions', ] mkdirp.sync(outputDir); fs.readdir(inputDir, (err, files) => { files.forEach(file => { if (!file.endsWith('.md')) { return; } let fileNameNoExtension = file.slice(0, -3); console.log('converting: ' + fileNameNoExtension); fs.readFile(inputDir + file, 'utf8', (err, data) => { if (err) { console.error(err); return; } var ingredientTableRegex = new RegExp(`^(\\|?.*?)\\|(.*?)(\\|.*?\\|.*?)\n`, `gm`); var ingredientDashCheck = new RegExp("^\-+$"); ingredientTableMatcher = ingredientTableRegex.exec(data); while (ingredientTableMatcher != null) { meas = ingredientTableMatcher[1]; let unit = ingredientTableMatcher[2].toLowerCase().trim(); if (unit != "unit" && unit && !ingredientDashCheck.test(unit)) { meas += unit + " "; } data = data.replace(ingredientTableMatcher[0], `${meas}${ingredientTableMatcher[3]}\n`); ingredientTableMatcher = ingredientTableRegex.exec(data); } ingredientTableMatcher = ingredientTableRegex.exec(data); while (ingredientTableMatcher != null) { meas = ingredientTableMatcher[1]; let unit = ingredientTableMatcher[2].toLowerCase().trim(); if (unit != "unit" && unit && !ingredientDashCheck.test(unit)) { meas += unit + " "; } data = data.replace(ingredientTableMatcher[0], `${meas}${ingredientTableMatcher[3]}\n`); ingredientTableMatcher = ingredientTableRegex.exec(data); } let tokens = md.parse(data) let sections = [] sections.push([]); // start off the array and put everything before and including the first header in title let numSections = 0; for (const token of tokens) { if (token.type === 'heading_open') { if (numSections == 0) { numSections++; } else if (numSections < mdSectionHtmlTitles.length) { numSections++; sections.push([]); } } sections[sections.length - 1].push(token) } assert(sections.length <= mdSectionHtmlTitles.length); // hardcode bootstrap class attribute to add to tag in ingredients for (let ii = 0; ii < sections[1].length; ii++) { if (sections[1][ii].type == 'table_open') { sections[1][ii].attrs = [["class", "table table-striped table-sm table-hover"]]; break; } } for (let ii = 0; ii < sections.length; ii++) { let html = md.renderer.render(sections[ii], md.options); // hardcode making images in the title section larger if (ii == 0) { var regex = new RegExp(``, `g`); matcher = regex.exec(html); while (matcher != null && !matcher[1].includes("w-100")) { var restOfTag = matcher[1]; html = html.replace(matcher[0], ``); matcher = regex.exec(html); } } fs.writeFileSync(outputDir + fileNameNoExtension + '-' + mdSectionHtmlTitles[ii] + '.ejs', html, 'utf8'); } }); }); }); } function generateRecipeNavigatorList(recipeSrcDir, generatedOutputDir) { // generate a list of recipe links. While doing so generate an array // of unique hashtags found in all recipes mkdirp.sync(generatedOutputDir); let recipeListPartialOut = ""; let allRecipeHashtags = []; fs.readdir(recipeSrcDir, (err, files) => { files.sort().forEach(file => { if (!file.endsWith('.md')) { return; } let fileNameNoExtension = file.slice(0, -3); const data = fs.readFileSync(recipeSrcDir + file, { encoding: 'utf8', flag: 'r' }); // find all hashtags in the file var hashtagRegex = new RegExp(`#(\\w+)`, `g`); hashtagMatcher = hashtagRegex.exec(data); var recipeTags = []; // hashtags of the current recipe only while (hashtagMatcher != null) { var hashtag = hashtagMatcher[1].toLowerCase(); if (!allRecipeHashtags.includes(hashtag)) { allRecipeHashtags.push(hashtag); } if (!recipeTags.includes(hashtag)) { recipeTags.push(hashtag); } hashtagMatcher = hashtagRegex.exec(data); } let combinedRecipeTags = ""; if (recipeTags.length > 0) { combinedRecipeTags = recipeTags.join(","); } // get first recipe title from document var titleRegex = new RegExp(`#\\s+(.+)\\n`, `g`); titleMatcher = titleRegex.exec(data); var recipeTitle = fileNameNoExtension; if (titleMatcher != null) { recipeTitle = titleMatcher[1]; } recipeListPartialOut += `${recipeTitle}\n`; }); // writeout the link list partial fs.writeFileSync(generatedOutputDir + "recipe-links.ejs", recipeListPartialOut, "utf-8"); // now generate the hashtag button list partial // TODO: in the future sort the list by number of hashtag hits (most -> least common) // instead of alphabetically let tagListPartialOut = ""; allRecipeHashtags.sort().forEach(hashtag => { tagListPartialOut += `\n`; }); fs.writeFileSync(generatedOutputDir + "recipe-tags.ejs", tagListPartialOut, "utf-8"); }); } convertRecipeMarkdown(recipeInputDir, recipeOutputDir); generateRecipeNavigatorList(recipeInputDir, recipeListGeneratedOutputDir);