diff --git a/build.js b/build.js
index a565c32..80aa681 100644
--- a/build.js
+++ b/build.js
@@ -156,7 +156,7 @@ function main(configs) {
readFile(resolve(templatesPath, 'index.html'), { encoding: 'utf8' }),
readFile(resolve(templatesPath, 'recipe.html'), { encoding: 'utf8' }),
])
- .then(([markdownFiles, images, indexTemplate, recipeTemplate]) => {
+ .then(async ([markdownFiles, images, indexTemplate, recipeTemplate]) => {
markdownFiles = filterByExtension(markdownFiles, recipesPath, ['.md']);
images = filterByExtension(images, imagesPath, ['.jpg', '.jpeg', '.png', '.webp', '.avif']);
@@ -165,8 +165,8 @@ function main(configs) {
makeThumbnails(outputPath, images);
images = swapJpegForAvif(images);
- buildRecipes(recipeTemplate, options, markdownFiles, images);
- buildRecipeIndex(indexTemplate, options, markdownFiles, images);
+ const recipeInfo = await buildRecipes(recipeTemplate, options, markdownFiles, images);
+ buildRecipeIndex(indexTemplate, options, markdownFiles, images, recipeInfo);
const endTime = new Date();
console.log(`Processed ${markdownFiles.length} recipes in ${endTime - startTime}ms`);
diff --git a/config.js b/config.js
index c720134..c17a59c 100644
--- a/config.js
+++ b/config.js
@@ -24,6 +24,18 @@ module.exports = {
*/
useSmartQuotes: true,
+ /**
+ * **experimental**
+ * when enabled wraps `` tags in link to open in new tab
+ */
+ addImageLinks: true,
+
+ /**
+ * **experimental**
+ * when enabled looks in raw file for author's name
+ */
+ findAuthor: true,
+
defaultTheme: 'default',
/**
diff --git a/src/buildRecipeIndex.js b/src/buildRecipeIndex.js
index 20e37b9..e6c17f0 100644
--- a/src/buildRecipeIndex.js
+++ b/src/buildRecipeIndex.js
@@ -9,14 +9,16 @@ const Substitutions = {
// Head's meta-tags
META_FAVICON: '{{__favIcon__}}',
META_DATE: '{{__metaDateGenerated__}}',
- META_OG_IMG: '{{__metaOGImage__}}',
+ META_OG_IMG: '{{__metaOGImage__}}',
THEME_CSS: '{{__theme__}}',
// kickoff the on-page script
STARTUP_JS: '{{__startup__}}',
};
const Styles = Object.freeze({
+ AUTHOR: 'recipe-list__author',
ITEM: 'recipe-list__item',
+ LINK: 'recipe-list__item-link',
NAME: 'recipe-list__name',
PHOTO: 'recipe-list__photo',
});
@@ -30,7 +32,7 @@ const RegExes = {
* and generate table of contents, plus a quick-nav list
* at the top
*/
-function buildRecipeIndex(indexTemplate, { defaultTheme, favicon, outputPath, initialIndexView }, fileList, images) {
+function buildRecipeIndex(indexTemplate, { defaultTheme, favicon, outputPath, initialIndexView }, fileList, images, recipeInfo) {
// create anchor and name from url
let lettersIndex = '';
// create list of recipes
@@ -55,14 +57,17 @@ function buildRecipeIndex(indexTemplate, { defaultTheme, favicon, outputPath, in
if (!ogImgURL && image) {
ogImgURL = `images/${image.fileName}`;
}
+ const author = recipeInfo.find(({ name: infoName }) => infoName === name)?.author || '';
const imgPath = image
? `images/thumbnails/${image.name}.jpg`
: 'images/placeholder.svg';
recipeItems += `
-
+
- ${displayName}
+ ${displayName}
+ ${author ? `${author}` : ''}
+
`.replace(RegExes.END_SPACES, '');
diff --git a/src/buildRecipes.js b/src/buildRecipes.js
index cb90545..b625ca7 100644
--- a/src/buildRecipes.js
+++ b/src/buildRecipes.js
@@ -2,7 +2,7 @@ const { resolve } = require('path');
const { readFile, writeFile } = require('fs');
const showdown = require('showdown');
const prettyHtml = require('pretty');
-const { linkify, shorten, replaceFractions, replaceQuotes } = require('./libs/utils');
+const { linkify, shorten, replaceFractions, replaceQuotes, linkifyImages, getAuthor } = require('./libs/utils');
const SectionMgr = require('./libs/SectionManager');
/* eslint-disable key-spacing */
@@ -132,7 +132,7 @@ const getInlineCss = heroImgURL => !heroImgURL
: `
`;
@@ -169,10 +169,12 @@ function getHelpSection(helpURLs, name) {
`;
}
-function convertRecipe(outputHTML, recipeHTML, config, name, image) {
+function convertRecipe(outputHTML, recipeHTML, opts) {
const {
+ name,
+ heroImgURL: image,
autoUrlSections, defaultTheme, favicon, useFractionSymbols, helpURLs, includeHelpLinks, shortenURLs, titleSuffix,
- } = config;
+ } = opts;
let recipeName = '';
const sectionMgr = new SectionMgr({ definedTypes: SectionTypes, defaultType: SectionTypes.NOTES });
@@ -235,24 +237,37 @@ function convertRecipe(outputHTML, recipeHTML, config, name, image) {
}
function buildRecipes(recipeTemplate, options, fileList, images) {
- const { outputPath, useSmartQuotes } = options;
-
- const converter = new showdown.Converter();
-
- fileList.forEach(({ file: path, name }) => {
- readFile(path, { encoding: 'utf8' }, (err, markdown) => {
- if (err) {
- // eslint-disable-next-line no-console
- console.error(err);
- return;
- }
- const heroImgURL = images.find(i => i.name === name);
- if (useSmartQuotes) {
- markdown = replaceQuotes(markdown);
- }
- let html = converter.makeHtml(markdown);
- html = prettyHtml(convertRecipe(recipeTemplate, html, options, name, heroImgURL), { ocd: true });
- writeFile(resolve(outputPath, `${name}.html`), html, { encoding: 'utf8'}, () => null);
+ return new Promise((promResolve) => {
+ let fileCount = 0;
+ const { addImageLinks, findAuthor, outputPath, useSmartQuotes } = options;
+ const converter = new showdown.Converter();
+ const recipeInfo = [];
+ fileList.forEach(({ file: path, name }) => {
+ readFile(path, { encoding: 'utf8' }, (err, markdown) => {
+ if (err) {
+ // eslint-disable-next-line no-console
+ console.error(err);
+ return;
+ }
+ const heroImgURL = images.find(i => i.name === name);
+ if (useSmartQuotes) {
+ markdown = replaceQuotes(markdown);
+ }
+ const author = findAuthor ? getAuthor(markdown) : '';
+ if (author) {
+ recipeInfo.push({ name, author });
+ }
+ let html = converter.makeHtml(markdown);
+ if (addImageLinks) {
+ html = linkifyImages(html);
+ }
+ html = prettyHtml(convertRecipe(recipeTemplate, html, { ...options, name, heroImgURL }), { ocd: true });
+ writeFile(resolve(outputPath, `${name}.html`), html, { encoding: 'utf8'}, () => {
+ if (fileList.length === ++fileCount) {
+ promResolve(recipeInfo);
+ }
+ });
+ });
});
});
}
diff --git a/src/libs/__tests__/utils.test.js b/src/libs/__tests__/utils.test.js
index de9565e..31cbc37 100644
--- a/src/libs/__tests__/utils.test.js
+++ b/src/libs/__tests__/utils.test.js
@@ -127,4 +127,82 @@ describe('buildRecipes', () => {
expect(result).toBe(expectedResult);
});
});
+
+ describe('getAuthor', () => {
+ const { getAuthor } = utils;
+
+ it('should happy path', () => {
+ const Tests = [{
+ value: ``,
+ expectedResult: '',
+ }, {
+ value: `
+ by Amanda Berry
+ `,
+ expectedResult: 'Amanda Berry',
+ }, {
+ value: `
+photo by Demples
+This is from the kitchen of Harriet McCormmick hereself
+from the kitchen of Aunt Bertha (my favorite auntie)
+
+ `,
+ expectedResult: 'Aunt Bertha',
+ }, {
+ value: `
+ courtesy of: Jenny
+ `,
+ expectedResult: 'Jenny',
+ }, {
+ value: `
+ courtesy of Derek
+ `,
+ expectedResult: 'Derek',
+ }, {
+ value: `BY Todd`,
+ expectedResult: 'Todd',
+ }, {
+ value: `BY the New York Times Staff`,
+ expectedResult: '',
+ }, {
+ value: `From the time my Unkle James was paroled`,
+ expectedResult: '',
+ }, {
+ value: `
+ courtesy of : Gavin
+
+ `,
+ expectedResult: 'Gavin',
+ }, {
+ value: `
+ courtesy of:Auntie Jim
+
+ `,
+ expectedResult: 'Auntie Jim',
+ }, {
+ value: `
+from Mellisa Clark at the New York Times
+ `,
+ expectedResult: 'Mellisa Clark at the New York Times',
+ }, {
+ value: `
+by Jeff "Handsy" Smith aka "The Frugal Gourmet" (WBEZ Chicago)
+ `,
+ expectedResult: 'Jeff "Handsy" Smith aka "The Frugal Gourmet"',
+ }, {
+ value: `
+# Positively-the-Absolutely-Best-Chocolate-Chip Cookies
+### From Maida Heatter
+
+* Yield: **50** cookies.
+`,
+ expectedResult: 'Maida Heatter'
+ }];
+
+ Tests.forEach(({ value, expectedResult}) => {
+ const result = getAuthor(value);
+ expect(result).toBe(expectedResult);
+ });
+ });
+ });
});
diff --git a/src/libs/utils.js b/src/libs/utils.js
index 723f68f..b73704e 100644
--- a/src/libs/utils.js
+++ b/src/libs/utils.js
@@ -8,6 +8,11 @@ const RegExes = {
EMAIL: /(([a-zA-Z0-9\-_.])+@[a-zA-Z_]+?(\.[a-zA-Z]{2,6})+)/gim,
// #endregion
+ // #region LInkify Images
+ IMG_TAG: /]+?>/,
+ IMG_ALT_PROP: /\balt="([^"]+?)"/,
+ // #endregion
+
// #region Shorten
textReg: /(?<=>)(https?:\/\/.*?)(?=<\/a>)/,
simpleDomain: /((?:[\w-]+\.)+[\w-]+)/,
@@ -16,8 +21,17 @@ const RegExes = {
// #region replaceFractions
FRACTIONS: /(1\/[2-9]|2\/[35]|3\/[458]|4\/5|5\/[68])|7\/8/g,
// #endregion
+
+ // #region Find Author Credit
+ AUTHOR: /^(?:#{3,6})?\s*(?:by|courtesy of|from(?: the)? kitchen of|from)\s*[ :-]\s*([A-Z][\w "]+)/im
+ // #endregion
};
+const Styles = Object.freeze({
+ LINKED_IMG: 'img-link',
+ JS_LINKED_IMG: 'js-img-link',
+});
+
const FractionsHash = Object.freeze({
'1/2': '½',
'1/3': '⅓',
@@ -46,6 +60,25 @@ const linkify = value => value
.replace(RegExes.URL_WITH_WWW, '$1$2')
.replace(RegExes.EMAIL, '$1');
+/**
+ * Violating one of my goals to keep this "pure" -- all front-end
+ * aganostic, but should apply a class for future me to tinker.
+ */
+const linkifyImages = text => text
+ .replace(new RegExp(RegExes.IMG_TAG, 'gm'), (imgTag) => {
+ const [, src] = imgTag.match(RegExes.IMG_TAG);
+ const [, alt] = imgTag.match(RegExes.IMG_ALT_PROP) || [];
+ return `${imgTag}`;
+ });
+
+const getAuthor = (text) => {
+ let [, author] = `${text}`.match(RegExes.AUTHOR) || [];
+ if (!author) {
+ return '';
+ }
+ author = author.trim();
+ return /[A-Z]/.test(author[0]) ? author : '';
+};
/**
* Replaces complete URL with only the domain, i.e. strips
* off path & protocol.
@@ -65,7 +98,9 @@ const replaceFractions = value => value
const replaceQuotes = value => value.replace(/(?]+)"(?=[\s<])/g, '“$1”');
module.exports = {
+ getAuthor,
linkify,
+ linkifyImages,
replaceFractions,
replaceQuotes,
shorten,
diff --git a/src/static/styles/recipesIndex.css b/src/static/styles/recipesIndex.css
index b5a1a25..decfb18 100644
--- a/src/static/styles/recipesIndex.css
+++ b/src/static/styles/recipesIndex.css
@@ -4,9 +4,9 @@
--color-view-btn-selected: var(--color-primary);
--color-view-svg-selected: var(--color-neutral-00);
- --color-filter-inactive: var(--color-neutral-25);
- --color-filter-hover: var(--color-neutral-80);
- --color-filter-focus: var(--color-neutral-50);
+ --color-filter-inactive: var(--color-neutral-25);
+ --color-filter-hover: var(--color-neutral-80);
+ --color-filter-focus: var(--color-neutral-50);
--search-input-height: 40px;
--search-icon-length: 18px;
@@ -192,11 +192,17 @@ body .recipe-list .recipe-list__item--hidden {
text-transform: capitalize;
}
-.recipe-list__item a {
+.recipe-list__item-link {
display: inline-block;
+ text-decoration: none;
width: 100%;
}
+.recipe-list__item-link:link,
+.recipe-list__item-link:visited {
+ text-decoration: inherit;
+}
+
.recipe-list__photo img {
object-fit: cover;
width: 100%;
@@ -207,6 +213,14 @@ body .recipe-list .recipe-list__item--hidden {
display: inline-block;
}
+.recipe-list__author {
+ color: var(--color-neutral-50);
+ display: block;
+ font-size: 90%;
+ font-style: normal;
+ font-weight: 300;
+}
+
/* Shared */
.view--content .recipe-list__item,
.view--compact-list .recipe-list__item {
@@ -219,8 +233,8 @@ body .recipe-list .recipe-list__item--hidden {
break-inside: avoid-column;
}
-
/* view: Compact List */
+.view--compact-list .recipe-list__author,
.view--compact-list .recipe-list__photo {
display: none;
}
@@ -254,13 +268,13 @@ body .recipe-list .recipe-list__item--hidden {
.view--grid .recipe-list__item a {
height: 260px;
- border: solid 1px transparent;
+ border: solid 1px var(--color-neutral-15);
border-radius: 6px;
overflow: hidden;
}
.view--grid .recipe-list__item a:hover {
- border-color: black;
+ border-color: var(--color-neutral-100);
}
.view--grid .recipe-list__photo {
@@ -270,10 +284,12 @@ body .recipe-list .recipe-list__item--hidden {
}
.view--grid .recipe-list__name {
- padding: 0 1em;
+ -webkit-line-clamp: 2;
font-size: 110%;
font-weight: 700;
line-height: 1.4;
+ overflow: hidden;
+ padding: 0 1em;
text-overflow: ellipsis;
}
diff --git a/src/static/styles/stylesheet.css b/src/static/styles/stylesheet.css
index 911095b..dc38a18 100644
--- a/src/static/styles/stylesheet.css
+++ b/src/static/styles/stylesheet.css
@@ -252,6 +252,10 @@ a:hover {
background-color: var(--color-link-bg-hover);
}
+img {
+ max-width: 100%;
+}
+
/* specific sections/objects */
.wrapper {
width: 90%;
diff --git a/src/static/styles/theme-default.css b/src/static/styles/theme-default.css
index 765cfc5..1e9445b 100644
--- a/src/static/styles/theme-default.css
+++ b/src/static/styles/theme-default.css
@@ -1,7 +1,7 @@
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;1,300&family=PT+Sans+Narrow:wght@700&display=swap');
:root {
- --font-stack-default-body: 'Open Sans', Franklin, Arial, Helvetica, sans-serif;
+ --font-stack-default-body: Franklin, Arial, Helvetica, sans-serif;
--font-stack-default-headline: 'PT Sans Narrow', sans-serif;
--font-family-body: var(--font-stack-default-body);
diff --git a/src/static/styles/vars.css b/src/static/styles/vars.css
index 0a832d0..2defb63 100644
--- a/src/static/styles/vars.css
+++ b/src/static/styles/vars.css
@@ -2,6 +2,7 @@
--font-stack-sans-serif: Georgia, serif;
--color-neutral-00: rgb(255, 255, 255);
+ --color-neutral-15: rgb(230, 230, 230);
--color-neutral-10: rgb(211, 211, 211);
--color-neutral-20: rgb(200, 200, 200);
--color-neutral-25: rgb(189, 189, 189);
@@ -12,6 +13,7 @@
--color-neutral-60: rgb(102, 102, 102);
--color-neutral-80: rgb(35, 35, 35);
--color-neutral-90: rgb(27, 27, 27);
+ --color-neutral-100: rgb(0, 0, 0);
/* --color-neutral-78: rgb(42, 42, 42); */
--color-peach-30: rgb(236, 169, 111);