From 33d1866f67b9ca762e7955b443dc071b71b9b75d Mon Sep 17 00:00:00 2001 From: Buz Carter Date: Mon, 17 Jun 2024 11:49:57 -0700 Subject: [PATCH] add Tbsp, gm, tsp, and Tbsp to recognized units; add some color to console; set mime types/encoding (Apache) for mardown files; message on finally isn't correct: not async --- .vscode/tasks.json | 26 +++++++++---------- build.js | 43 +++++++++++++++++++++----------- src/buildRecipeIndex.js | 3 ++- src/buildRecipes.js | 27 +++++++++++++++++--- src/libs/ConsoleColors.js | 29 +++++++++++++++++++++ src/libs/__tests__/utils.test.js | 32 ++++++++++++++++++++++++ src/libs/fsUtils.js | 18 +++++++++++++ src/libs/utils.js | 24 +++++++++++++++++- src/static/.htaccess | 7 ++++++ 9 files changed, 175 insertions(+), 34 deletions(-) create mode 100644 src/libs/ConsoleColors.js create mode 100644 src/libs/fsUtils.js create mode 100644 src/static/.htaccess diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 41ac8e7..38d01a2 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,16 +1,14 @@ { - "version": "2.0.0", - "tasks": [ - { - "type": "npm", - "script": "build", - "group": { - "kind": "build", - "isDefault": true - }, - "problemMatcher": [], - "label": "npm: build", - "detail": "node ./build.js" - } - ] + "version": "2.0.0", + "tasks": [{ + "type": "npm", + "script": "build", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [], + "label": "npm: build", + "detail": "node ./build.js" + }] } diff --git a/build.js b/build.js index 6fda6cd..9cc959e 100644 --- a/build.js +++ b/build.js @@ -1,27 +1,40 @@ /* eslint-disable no-console */ -import { - basename, dirname, extname, join, resolve, -} from 'path'; +import { basename, extname, join, resolve } from 'path'; import { rmSync, mkdirSync, rename } from 'fs'; import { cp, readdir, readFile } from 'fs/promises'; import sharp from 'sharp'; -import { fileURLToPath } from 'url'; - -export const __dirname = dirname(fileURLToPath(import.meta.url)); import buildRecipes from './src/buildRecipes.js'; import buildRecipeIndex from './src/buildRecipeIndex.js'; import configs from './config.js'; +import { __dirname, filterByExtension } from './src/libs/fsUtils.js'; + +import { ColorTypes, Colors } from './src/libs/ConsoleColors.js'; const THUMBNAIL_WIDTH = 260; -const filterByExtension = (fileList, basePath, allowedExtensions) => fileList - .filter((fileName) => allowedExtensions.includes(extname(fileName))) - .map((fileName) => ({ - file: resolve(basePath, fileName), - fileName, - name: basename(fileName, extname(fileName)), - })); +const showOverrideMsg = (cmdArgs) => { + const { Reset } = ColorTypes; + const { Orange, Cyan, Yellow, Magenta } = Colors; + console.log(`\n${ + Yellow}Using command line overrides:\n${Magenta}${JSON + .stringify(cmdArgs, null, 3) + .replace(/^(\s*)"([^"]+)": ("?)(.*?)("?)(,?)$/gm, `$1${Magenta}"${Orange}$2${Magenta}": ${Magenta}$3${Cyan}$4${Magenta}$5$6`)}${ + Reset}\n`); +}; + +const showDoneMsg = (recipeCount, time) => { + const { Reset } = ColorTypes; + const { Orange, Cyan, Yellow } = Colors; + console.log(`\n${ + Yellow}Processed ${ + Cyan}${recipeCount}${ + Yellow} recipes in ${ + Cyan}${time}${ + Orange}ms${ + Reset + }\n`); +}; function setupOutputDir(outputPath) { rmSync(outputPath, { recursive: true, force: true }); @@ -134,7 +147,7 @@ function getCommanLineOverrides(args) { }, {}); if (cmdArgs && Object.keys(cmdArgs).length) { - console.log(`Using command line overrides: ${JSON.stringify(cmdArgs, null, 3)}`); + showOverrideMsg(cmdArgs); } return cmdArgs; } @@ -175,7 +188,7 @@ function main(opts) { buildRecipeIndex(indexTemplate, options, markdownFiles, images, recipeInfo); const endTime = new Date(); - console.log(`Processed ${markdownFiles.length} recipes in ${endTime - startTime}ms`); + showDoneMsg(markdownFiles.length, endTime - startTime); }) .catch((err) => { console.error(err); diff --git a/src/buildRecipeIndex.js b/src/buildRecipeIndex.js index e1480c7..d63d529 100644 --- a/src/buildRecipeIndex.js +++ b/src/buildRecipeIndex.js @@ -1,6 +1,7 @@ import { resolve } from 'path'; import { writeFileSync } from 'fs'; import prettyHtml from 'pretty'; +import { fileNameToTitleCase } from './libs/utils.js'; /* eslint-disable key-spacing */ const Substitutions = { @@ -43,7 +44,7 @@ export default function buildRecipeIndex(indexTemplate, { defaultTheme, favicon, fileList.forEach(({ name }) => { const firstLetter = name.charAt(0).toUpperCase(); - const displayName = name.replace(/-/g, ' '); + const displayName = fileNameToTitleCase(name); // if the first letter of the recipe hasn't been // seen yet, add to list of letters and put an achor in diff --git a/src/buildRecipes.js b/src/buildRecipes.js index 9e68f0c..365b31a 100644 --- a/src/buildRecipes.js +++ b/src/buildRecipes.js @@ -3,6 +3,7 @@ import { readFile, writeFile } from 'fs'; import showdown from 'showdown'; import prettyHtml from 'pretty'; +import { ColorTypes, Colors } from './libs/ConsoleColors.js'; import { linkify, shorten, replaceFractions, replaceQuotes, linkifyImages, getAuthor, } from './libs/utils.js'; @@ -80,7 +81,7 @@ const RegExes = Object.freeze({ * "1/4 teaspoon vanilla extract" * "1.5 oz gin" */ - NUMERIC: /
  • (~?[\d½⅓⅔¼¾⅕⅖⅗⅘⅙⅚⅐⅛⅜⅝⅞/ .–-]+(?:(?:to|-) \d+)?(?:["gcltT]|oz|ml|lb|kg)?\.?)\s+(.*)\s*<\/li>/, + NUMERIC: /
  • (~?[\d½⅓⅔¼¾⅕⅖⅗⅘⅙⅚⅐⅛⅜⅝⅞/ .–-]+(?:(?:to|-) \d+)?(?:["gcltT]|cup|oz|ml|lb|kg|gm|tsp|Tbsp)?\.?)\s+(.*)\s*<\/li>/, FRACTION_SYMBOL: /([½⅓⅔¼¾⅕⅖⅗⅘⅙⅚⅐⅛⅜⅝⅞])/g, /** Custom meta tags */ @@ -90,6 +91,27 @@ const RegExes = Object.freeze({ const LINK_SUB_NAME = ''; +let warnNbr = 0; +const showWarnings = (name, warnings) => { + const { Reset } = ColorTypes; + const { Magenta, Cyan, Orange, CoolYellow, Yellow } = Colors; + if (warnNbr === 0) { + // eslint-disable-next-line no-console + console.warn(`\n${ + Yellow}Somw recipes contain unrecognized sections. Content for these sections will be appear under the "${ + Magenta}${SectionTypes.NOTES}${ + Yellow}" section.${ + Reset}\n`); + } + // eslint-disable-next-line no-console + console.warn(`${ + CoolYellow}${++warnNbr}. ${ + Cyan}${name}.md${ + CoolYellow}: ${ + Orange}${warnings}${ + Reset}`); +}; + function setHeadMeta(documentHtml, { author, favicon, ogImgURL, recipeName, titleSuffix }) { return documentHtml .replace(RegExes.PAGE_TITLE, `${recipeName}${author ? ` by ${author}` : ''}${titleSuffix || ''}`) @@ -221,8 +243,7 @@ function convertRecipe(outputHTML, recipeHTML, opts) { const heroImgURL = image ? `images/${image.fileName}` : ''; if (sectionMgr.hasWarnings) { - // eslint-disable-next-line no-console - console.warn(`${name}.md contains unknown sections [${sectionMgr.warnings}] that are included under "${SectionTypes.NOTES}"`); + showWarnings(name, sectionMgr.warnings); } outputHTML = sectionMgr.replace(outputHTML); diff --git a/src/libs/ConsoleColors.js b/src/libs/ConsoleColors.js new file mode 100644 index 0000000..c304a33 --- /dev/null +++ b/src/libs/ConsoleColors.js @@ -0,0 +1,29 @@ +const HEX_ESC = '\x1b'; + +/* eslint-disable key-spacing */ +export const ColorTypes = Object.freeze({ + Reset: `${HEX_ESC}[0m`, +}); + +export const Colors = Object.freeze({ + // Text colors (foreground colors) + Black: `${HEX_ESC}[30m`, + Blue: `${HEX_ESC}[34m`, + CoolYellow: `${HEX_ESC}[38;2;175;175;151m`, + Cyan: `${HEX_ESC}[36m`, + Gray: `${HEX_ESC}[90m`, + Green: `${HEX_ESC}[32m`, + Magenta: `${HEX_ESC}[35m`, + Orange: `${HEX_ESC}[38;5;208m`, + Pink: `${HEX_ESC}[38;5;175m`, + Purple: `${HEX_ESC}[38;5;99m`, + Red: `${HEX_ESC}[31m`, + White: `${HEX_ESC}[37m`, + Yellow: `${HEX_ESC}[33m`, +}); +/* eslint-enable key-spacing */ + +export default { + ColorTypes, + Colors, +}; diff --git a/src/libs/__tests__/utils.test.js b/src/libs/__tests__/utils.test.js index 7989d82..2df9ad0 100644 --- a/src/libs/__tests__/utils.test.js +++ b/src/libs/__tests__/utils.test.js @@ -232,3 +232,35 @@ Dalgona coffee is a whipped coffee drink`, }); }); }); + +describe('titleCase', () => { + const { titleCase } = utils; + + it('should convert string to title case', () => { + const tests = [ + { value: 'hello world', expectedResult: 'Hello World' }, + { value: ' this is a test ', expectedResult: 'This Is A Test' }, + ]; + + tests.forEach(({ value, expectedResult }) => { + const result = titleCase(value); + expect(result).toBe(expectedResult); + }); + }); +}); + +describe('fileNameToTitleCase', () => { + const { fileNameToTitleCase } = utils; + + it('should convert file name to title case', () => { + const tests = [ + { value: 'hello-world.js', expectedResult: 'Hello World.Js' }, + { value: ' anotherexample html---my--pie.lady', expectedResult: 'Anotherexample Html My Pie.Lady' }, + ]; + + tests.forEach(({ value, expectedResult }) => { + const result = fileNameToTitleCase(value); + expect(result).toBe(expectedResult); + }); + }); +}); diff --git a/src/libs/fsUtils.js b/src/libs/fsUtils.js new file mode 100644 index 0000000..df2bef6 --- /dev/null +++ b/src/libs/fsUtils.js @@ -0,0 +1,18 @@ +import { + basename, + dirname, + extname, + resolve, +} from 'path'; +import { fileURLToPath } from 'url'; + +/** directory name of project root */ +export const __dirname = resolve(dirname(fileURLToPath(import.meta.url)), '../..'); + +export const filterByExtension = (fileList, basePath, allowedExtensions) => fileList + .filter((fileName) => allowedExtensions.includes(extname(fileName))) + .map((fileName) => ({ + file: resolve(basePath, fileName), + fileName, + name: basename(fileName, extname(fileName)), + })); diff --git a/src/libs/utils.js b/src/libs/utils.js index 3721f76..48e1283 100644 --- a/src/libs/utils.js +++ b/src/libs/utils.js @@ -92,8 +92,30 @@ export const shorten = (value) => value return domain; }); -/** brute-force approach: replaces `1/2` with `½` */ +/** + * brute-force approach: replaces `1/2` with `½` + */ export const replaceFractions = (value) => value .replace(RegExes.FRACTIONS, (m) => FractionsHash[m]); +/** + * Replaces "straight" quotes with HTML encoded "curly" quotes. Avoids replacing quotes in HTML tags. + */ export const replaceQuotes = (value) => value.replace(/(?]+)"(?=[\s<])/g, '“$1”'); + +/** + * Converts a string to title case by replacing the first character of each word with uppercase. + * @param {string} value - The string to convert to title case. + * @returns {string} The string in title case. + */ +export const titleCase = (value) => value.trim().replace(/\b\w/g, (word) => word.toUpperCase()); + +/** + * Replaces "-" with spaces, removes double spaces, and returns the title case of the result. + * @param {string} value - The string to process. + * @returns {string} The processed string in title case. + */ +export const fileNameToTitleCase = (value) => { + const processedString = value.trim().replace(/-/g, ' ').replace(/\s+/g, ' '); + return titleCase(processedString); +}; diff --git a/src/static/.htaccess b/src/static/.htaccess new file mode 100644 index 0000000..0da0f5a --- /dev/null +++ b/src/static/.htaccess @@ -0,0 +1,7 @@ +AddDefaultCharset UTF-8 + + AddCharset UTF-8 .txt .md + + + ForceType text/plain +