Compare commits
No commits in common. "2cbd931e99a3699d541b8065447e4678cf2d5a17" and "a212df96ddc640d11857cb58f7c30ca4ef3b72ce" have entirely different histories.
2cbd931e99
...
a212df96dd
10 changed files with 241 additions and 266 deletions
|
|
@ -30,7 +30,7 @@ pageType: recipe
|
||||||
<div class="measurement-toggles" style="display: none;"></div>
|
<div class="measurement-toggles" style="display: none;"></div>
|
||||||
|
|
||||||
<div class="recipe-content">
|
<div class="recipe-content">
|
||||||
{{ content | renderRecipeMarkdown | safe }}
|
{{ content | safe }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% set recipeTags = page.inputPath | extractTagsFromFile %}
|
{% set recipeTags = page.inputPath | extractTagsFromFile %}
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,179 @@
|
||||||
const markdownIt = require("markdown-it");
|
const markdownIt = require("markdown-it");
|
||||||
const markdownItFootnoteCustom = require('./lib/footnotes/plugin');
|
const markdownItContainer = require("markdown-it-container");
|
||||||
|
const markdownItFootnote = require("markdown-it-footnote");
|
||||||
const markdownItMermaid = require('markdown-it-mermaid').default
|
const markdownItMermaid = require('markdown-it-mermaid').default
|
||||||
const markdownItTaskLists = require('markdown-it-task-lists');
|
const markdownItTaskLists = require('markdown-it-task-lists');
|
||||||
const markdownItMeasurements = require('./lib/measurements/plugin');
|
const markdownItMeasurements = require('./lib/measurements/plugin');
|
||||||
const markdownItHashtag = require('./lib/hashtags/plugin');
|
|
||||||
const markdownItStripTrailingHashtags = require('./lib/strip-trailing-hashtags/plugin');
|
|
||||||
const markdownItDetails = require('./lib/details/plugin');
|
|
||||||
const { extractTags, expandHierarchicalTags, getPostTags, getRecipeTags } = require('./lib/tags/plugin');
|
|
||||||
const { cacheBustingPlugin } = require('./lib/cache-busting/plugin');
|
|
||||||
const syntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight");
|
const syntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
const crypto = require("crypto");
|
||||||
|
const path = require("path");
|
||||||
const { DateTime } = require("luxon");
|
const { DateTime } = require("luxon");
|
||||||
const siteConfig = require("./_data/site.js");
|
const siteConfig = require("./_data/site.js");
|
||||||
|
|
||||||
|
const fileHashCache = {};
|
||||||
|
const getFileHash = (file, dir = "css") => {
|
||||||
|
const cacheKey = `${dir}/${file}`;
|
||||||
|
if (fileHashCache[cacheKey]) return fileHashCache[cacheKey];
|
||||||
|
|
||||||
|
const filePath = path.join(__dirname, dir, file);
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(filePath, "utf-8");
|
||||||
|
const hash = crypto.createHash("md5").update(content).digest("hex").slice(0, 8);
|
||||||
|
fileHashCache[cacheKey] = hash;
|
||||||
|
return hash;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Could not hash file: ${file} in ${dir}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractTags = (content, mdInstance) => {
|
||||||
|
if (!content) return [];
|
||||||
|
|
||||||
|
const collectHashtags = (tokens) =>
|
||||||
|
tokens.flatMap(token => [
|
||||||
|
...(token.type === 'hashtag' ? [token.content] : []),
|
||||||
|
...(token.children ? collectHashtags(token.children) : [])
|
||||||
|
]);
|
||||||
|
|
||||||
|
const tokens = mdInstance.parse(content, {});
|
||||||
|
const tags = collectHashtags(tokens);
|
||||||
|
return [...new Set(tags)];
|
||||||
|
}
|
||||||
|
|
||||||
|
const expandHierarchicalTags = (tags) => {
|
||||||
|
const expanded = new Set();
|
||||||
|
tags.forEach(tag => {
|
||||||
|
expanded.add(tag);
|
||||||
|
const parts = tag.split('/');
|
||||||
|
for (let i = 1; i < parts.length; i++) {
|
||||||
|
const parentTag = parts.slice(0, i).join('/');
|
||||||
|
expanded.add(parentTag);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return [...expanded];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPostTags = (post, tagMdInstance) => {
|
||||||
|
const filePath = post.inputPath;
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
const tags = extractTags(content, tagMdInstance);
|
||||||
|
return tags.map(tag => tag.toLowerCase());
|
||||||
|
} catch (e) {
|
||||||
|
// Skip if file can't be read
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const isReleased = (post) => {
|
const isReleased = (post) => {
|
||||||
return post.data.released !== false;
|
return post.data.released !== false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const markdownItHashtag = (md) => {
|
||||||
|
const hashtagRegex = /^#([a-zA-Z][a-zA-Z0-9_\\\\/\-]*)(?![a-zA-Z0-9_-])/;
|
||||||
|
|
||||||
|
const HASH_CODE = '#'.charCodeAt(0);
|
||||||
|
const SPACE_CODE = ' '.charCodeAt(0);
|
||||||
|
const TAB_CODE = '\\\\t'.charCodeAt(0);
|
||||||
|
const NEWLINE_CODE = '\\\\n'.charCodeAt(0);
|
||||||
|
const CARRIAGE_RETURN_CODE = '\\\\r'.charCodeAt(0);
|
||||||
|
|
||||||
|
md.inline.ruler.push('hashtag', function(state, silent) {
|
||||||
|
const pos = state.pos;
|
||||||
|
const ch = state.src.charCodeAt(pos);
|
||||||
|
|
||||||
|
if (ch !== HASH_CODE) return false;
|
||||||
|
|
||||||
|
if (pos > 0) {
|
||||||
|
const prevCh = state.src.charCodeAt(pos - 1);
|
||||||
|
if (prevCh !== SPACE_CODE && prevCh !== TAB_CODE && prevCh !== NEWLINE_CODE && prevCh !== CARRIAGE_RETURN_CODE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = state.src.slice(pos).match(hashtagRegex);
|
||||||
|
if (!match) return false;
|
||||||
|
|
||||||
|
if (!silent) {
|
||||||
|
const token = state.push('hashtag', 'a', 0);
|
||||||
|
token.content = match[1];
|
||||||
|
token.markup = '#';
|
||||||
|
}
|
||||||
|
|
||||||
|
state.pos += match[0].length;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
md.renderer.rules.hashtag = function(tokens, idx) {
|
||||||
|
const tagName = tokens[idx].content;
|
||||||
|
const slug = tagName.toLowerCase();
|
||||||
|
return `<a href="/tags/${slug}/" class="inline-tag">#${md.utils.escapeHtml(tagName)}</a>`;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Plugin: Strip trailing hashtag-only paragraphs from rendered output
|
||||||
|
// Must be applied AFTER footnote plugin since footnotes are moved to the end
|
||||||
|
const markdownItStripTrailingHashtags = (md) => {
|
||||||
|
md.core.ruler.push('strip_trailing_hashtags', function(state) {
|
||||||
|
const tokens = state.tokens;
|
||||||
|
|
||||||
|
const isHashtagOnlyParagraph = (inlineToken) =>
|
||||||
|
inlineToken?.type === 'inline' &&
|
||||||
|
inlineToken.children?.every(child =>
|
||||||
|
child.type === 'hashtag' ||
|
||||||
|
(child.type === 'text' && child.content.trim() === '') ||
|
||||||
|
child.type === 'softbreak'
|
||||||
|
) &&
|
||||||
|
inlineToken.children?.some(child => child.type === 'hashtag');
|
||||||
|
|
||||||
|
const isHashtagParagraphAt = (idx) =>
|
||||||
|
tokens[idx]?.type === 'paragraph_open' &&
|
||||||
|
tokens[idx + 1]?.type === 'inline' &&
|
||||||
|
tokens[idx + 2]?.type === 'paragraph_close' &&
|
||||||
|
isHashtagOnlyParagraph(tokens[idx + 1]);
|
||||||
|
|
||||||
|
const footnoteIdx = tokens.findIndex(t => t.type === 'footnote_block_open');
|
||||||
|
const footnoteSectionStart = footnoteIdx === -1 ? tokens.length : footnoteIdx;
|
||||||
|
|
||||||
|
const hashtagSectionStart = Array.from(
|
||||||
|
{ length: Math.floor(footnoteSectionStart / 3) },
|
||||||
|
(_, i) => footnoteSectionStart - 3 * (i + 1)
|
||||||
|
).reduce(
|
||||||
|
(start, idx) => isHashtagParagraphAt(idx) ? idx : start,
|
||||||
|
footnoteSectionStart
|
||||||
|
);
|
||||||
|
|
||||||
|
state.tokens = tokens.filter((_, idx) =>
|
||||||
|
idx < hashtagSectionStart || idx >= footnoteSectionStart
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const markdownItDetails = (md) => {
|
||||||
|
md.use(markdownItContainer, 'details', {
|
||||||
|
validate: (params) => params.trim().match(/^(.*)$/),
|
||||||
|
render: (tokens, idx) => {
|
||||||
|
const m = tokens[idx].info.trim().match(/^(.*)$/);
|
||||||
|
if (tokens[idx].nesting === 1) {
|
||||||
|
const title = md.utils.escapeHtml(m[1]);
|
||||||
|
return `<details class="expandable">\\n<summary>${title}</summary>\\n`;
|
||||||
|
}
|
||||||
|
return '</details>\\n';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const sharedPlugins = [
|
const sharedPlugins = [
|
||||||
markdownItFootnoteCustom,
|
markdownItFootnote,
|
||||||
markdownItHashtag,
|
markdownItHashtag,
|
||||||
markdownItMermaid,
|
markdownItMermaid,
|
||||||
[markdownItTaskLists, { enabled: true, label: true, labelAfter: false }],
|
[markdownItTaskLists, { enabled: true, label: true, labelAfter: false }],
|
||||||
markdownItDetails,
|
markdownItDetails,
|
||||||
|
markdownItMeasurements,
|
||||||
];
|
];
|
||||||
|
|
||||||
const applyPlugins = (md, plugins) =>
|
const applyPlugins = (md, plugins) =>
|
||||||
|
|
@ -38,13 +189,24 @@ const createMarkdownInstance = (extraPlugins = []) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const md = createMarkdownInstance([markdownItStripTrailingHashtags]);
|
const md = createMarkdownInstance([markdownItStripTrailingHashtags]);
|
||||||
const recipeMd = createMarkdownInstance([markdownItStripTrailingHashtags, markdownItMeasurements]);
|
|
||||||
|
// Customize footnote rendering to remove brackets
|
||||||
|
md.renderer.rules.footnote_ref = (tokens, idx, options, env, slf) => {
|
||||||
|
const id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);
|
||||||
|
const n = Number(tokens[idx].meta.id + 1).toString();
|
||||||
|
let refid = id;
|
||||||
|
|
||||||
|
if (tokens[idx].meta.subId > 0) {
|
||||||
|
refid += ':' + tokens[idx].meta.subId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '<sup class="footnote-ref"><a href="#fn' + id + '" id="fnref' + refid + '">' + n + '</a></sup>';
|
||||||
|
};
|
||||||
|
|
||||||
const tagExtractorMd = createMarkdownInstance();
|
const tagExtractorMd = createMarkdownInstance();
|
||||||
|
|
||||||
module.exports = (eleventyConfig) => {
|
module.exports = (eleventyConfig) => {
|
||||||
eleventyConfig.addPlugin(syntaxHighlight);
|
eleventyConfig.addPlugin(syntaxHighlight);
|
||||||
eleventyConfig.addPlugin(cacheBustingPlugin, { rootDir: __dirname });
|
|
||||||
|
|
||||||
eleventyConfig.addFilter("extractTags", (content) => extractTags(content, tagExtractorMd));
|
eleventyConfig.addFilter("extractTags", (content) => extractTags(content, tagExtractorMd));
|
||||||
eleventyConfig.addFilter("extractTagsFromFile", (filePath) => {
|
eleventyConfig.addFilter("extractTagsFromFile", (filePath) => {
|
||||||
|
|
@ -56,11 +218,6 @@ module.exports = (eleventyConfig) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
eleventyConfig.addFilter("renderRecipeMarkdown", (content) => {
|
|
||||||
if (!content) return '';
|
|
||||||
return recipeMd.render(content);
|
|
||||||
});
|
|
||||||
|
|
||||||
eleventyConfig.setLibrary("md", md);
|
eleventyConfig.setLibrary("md", md);
|
||||||
|
|
||||||
eleventyConfig.addFilter("dateTimeFormat", (date) => {
|
eleventyConfig.addFilter("dateTimeFormat", (date) => {
|
||||||
|
|
@ -201,13 +358,25 @@ module.exports = (eleventyConfig) => {
|
||||||
}, {});
|
}, {});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get tags from recipes (only from newest non-draft versions)
|
||||||
|
const getRecipeTags = (recipe) => {
|
||||||
|
const filePath = recipe.inputPath;
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
const tags = extractTags(content, tagExtractorMd);
|
||||||
|
return tags.map(tag => tag.toLowerCase());
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
eleventyConfig.addCollection("contentTags", (collectionApi) => {
|
eleventyConfig.addCollection("contentTags", (collectionApi) => {
|
||||||
const posts = collectionApi.getFilteredByGlob("posts/**/*.md").filter(isReleased);
|
const posts = collectionApi.getFilteredByGlob("posts/**/*.md").filter(isReleased);
|
||||||
const recipes = collectionApi.getFilteredByGlob("recipes/**/*.md")
|
const recipes = collectionApi.getFilteredByGlob("recipes/**/*.md")
|
||||||
.filter(r => r.data.isNewestVersion && r.data.draft !== true);
|
.filter(r => r.data.isNewestVersion && r.data.draft !== true);
|
||||||
|
|
||||||
const postTags = posts.flatMap(post => getPostTags(post, tagExtractorMd));
|
const postTags = posts.flatMap(post => getPostTags(post, tagExtractorMd));
|
||||||
const recipeTags = recipes.flatMap(recipe => getRecipeTags(recipe, tagExtractorMd));
|
const recipeTags = recipes.flatMap(recipe => getRecipeTags(recipe));
|
||||||
|
|
||||||
const allTags = expandHierarchicalTags([...postTags, ...recipeTags]);
|
const allTags = expandHierarchicalTags([...postTags, ...recipeTags]);
|
||||||
|
|
||||||
|
|
@ -239,7 +408,7 @@ module.exports = (eleventyConfig) => {
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
const tagMap = recipes.reduce((acc, recipe) => {
|
const tagMap = recipes.reduce((acc, recipe) => {
|
||||||
const rawTags = getRecipeTags(recipe, tagExtractorMd);
|
const rawTags = getRecipeTags(recipe);
|
||||||
const tags = expandHierarchicalTags(rawTags);
|
const tags = expandHierarchicalTags(rawTags);
|
||||||
return tags.reduce((innerAcc, tag) => ({
|
return tags.reduce((innerAcc, tag) => ({
|
||||||
...innerAcc,
|
...innerAcc,
|
||||||
|
|
@ -261,6 +430,60 @@ module.exports = (eleventyConfig) => {
|
||||||
}), {});
|
}), {});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Cache busting filter: returns hashed filename
|
||||||
|
eleventyConfig.addFilter("fileHash", (file, dir = "css") => {
|
||||||
|
const hash = getFileHash(file, dir);
|
||||||
|
const ext = path.extname(file);
|
||||||
|
const base = path.basename(file, ext);
|
||||||
|
return `/${dir}/${base}.${hash}${ext}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
eleventyConfig.on("eleventy.before", async () => {
|
||||||
|
// Copy CSS files with hashes
|
||||||
|
const cssDir = path.join(__dirname, "css");
|
||||||
|
const outputCssDir = path.join(__dirname, "_site", "css");
|
||||||
|
|
||||||
|
if (!fs.existsSync(outputCssDir)) {
|
||||||
|
fs.mkdirSync(outputCssDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const cssFiles = fs.readdirSync(cssDir).filter(f => f.endsWith(".css"));
|
||||||
|
for (const cssFile of cssFiles) {
|
||||||
|
const hash = getFileHash(cssFile, "css");
|
||||||
|
const ext = path.extname(cssFile);
|
||||||
|
const base = path.basename(cssFile, ext);
|
||||||
|
const hashedName = `${base}${hash == null ? '' : `.${hash}`}${ext}`;
|
||||||
|
|
||||||
|
fs.copyFileSync(
|
||||||
|
path.join(cssDir, cssFile),
|
||||||
|
path.join(outputCssDir, hashedName)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy assets files with hashes
|
||||||
|
const assetsDir = path.join(__dirname, "assets");
|
||||||
|
const outputAssetsDir = path.join(__dirname, "_site", "assets");
|
||||||
|
|
||||||
|
if (fs.existsSync(assetsDir)) {
|
||||||
|
if (!fs.existsSync(outputAssetsDir)) {
|
||||||
|
fs.mkdirSync(outputAssetsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const assetFiles = fs.readdirSync(assetsDir);
|
||||||
|
for (const assetFile of assetFiles) {
|
||||||
|
const hash = getFileHash(assetFile, "assets");
|
||||||
|
const ext = path.extname(assetFile);
|
||||||
|
const base = path.basename(assetFile, ext);
|
||||||
|
const hashedName = `${base}${hash == null ? '' : `.${hash}`}${ext}`;
|
||||||
|
|
||||||
|
fs.copyFileSync(
|
||||||
|
path.join(assetsDir, assetFile),
|
||||||
|
path.join(outputAssetsDir, hashedName)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
eleventyConfig.addPassthroughCopy("robots.txt");
|
eleventyConfig.addPassthroughCopy("robots.txt");
|
||||||
eleventyConfig.addPassthroughCopy("simulations");
|
eleventyConfig.addPassthroughCopy("simulations");
|
||||||
eleventyConfig.addPassthroughCopy("js");
|
eleventyConfig.addPassthroughCopy("js");
|
||||||
|
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
const fs = require("fs");
|
|
||||||
const crypto = require("crypto");
|
|
||||||
const path = require("path");
|
|
||||||
|
|
||||||
const fileHashCache = {};
|
|
||||||
const getFileHash = (file, dir = "css") => {
|
|
||||||
const cacheKey = `${dir}/${file}`;
|
|
||||||
if (fileHashCache[cacheKey]) return fileHashCache[cacheKey];
|
|
||||||
|
|
||||||
const filePath = path.join(__dirname, '..', '..', dir, file);
|
|
||||||
try {
|
|
||||||
const content = fs.readFileSync(filePath, "utf-8");
|
|
||||||
const hash = crypto.createHash("md5").update(content).digest("hex").slice(0, 8);
|
|
||||||
fileHashCache[cacheKey] = hash;
|
|
||||||
return hash;
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(`Could not hash file: ${file} in ${dir}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyHashedFiles = (sourceDir, outputDir) => {
|
|
||||||
if (!fs.existsSync(sourceDir)) return;
|
|
||||||
|
|
||||||
if (!fs.existsSync(outputDir)) {
|
|
||||||
fs.mkdirSync(outputDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const dirName = path.basename(sourceDir);
|
|
||||||
const files = fs.readdirSync(sourceDir);
|
|
||||||
for (const file of files) {
|
|
||||||
const hash = getFileHash(file, dirName);
|
|
||||||
const ext = path.extname(file);
|
|
||||||
const base = path.basename(file, ext);
|
|
||||||
const hashedName = `${base}${hash == null ? '' : `.${hash}`}${ext}`;
|
|
||||||
|
|
||||||
fs.copyFileSync(
|
|
||||||
path.join(sourceDir, file),
|
|
||||||
path.join(outputDir, hashedName)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const cacheBustingPlugin = (eleventyConfig, { rootDir }) => {
|
|
||||||
eleventyConfig.addFilter("fileHash", (file, dir = "css") => {
|
|
||||||
const hash = getFileHash(file, dir);
|
|
||||||
const ext = path.extname(file);
|
|
||||||
const base = path.basename(file, ext);
|
|
||||||
return `/${dir}/${base}.${hash}${ext}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
eleventyConfig.on("eleventy.before", async () => {
|
|
||||||
// Copy CSS files with hashes
|
|
||||||
const cssDir = path.join(rootDir, "css");
|
|
||||||
const outputCssDir = path.join(rootDir, "_site", "css");
|
|
||||||
copyHashedFiles(cssDir, outputCssDir);
|
|
||||||
|
|
||||||
// Copy assets files with hashes
|
|
||||||
const assetsDir = path.join(rootDir, "assets");
|
|
||||||
const outputAssetsDir = path.join(rootDir, "_site", "assets");
|
|
||||||
copyHashedFiles(assetsDir, outputAssetsDir);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
cacheBustingPlugin,
|
|
||||||
getFileHash,
|
|
||||||
};
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
const markdownItContainer = require("markdown-it-container");
|
|
||||||
|
|
||||||
const markdownItDetails = (md) => {
|
|
||||||
md.use(markdownItContainer, 'details', {
|
|
||||||
validate: (params) => params.trim().match(/^(.*)$/),
|
|
||||||
render: (tokens, idx) => {
|
|
||||||
const m = tokens[idx].info.trim().match(/^(.*)$/);
|
|
||||||
if (tokens[idx].nesting === 1) {
|
|
||||||
const title = md.utils.escapeHtml(m[1]);
|
|
||||||
return `<details class="expandable">\n<summary>${title}</summary>\n`;
|
|
||||||
}
|
|
||||||
return '</details>\n';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = markdownItDetails;
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
const markdownItFootnote = require("markdown-it-footnote");
|
|
||||||
|
|
||||||
// Wraps markdown-it-footnote and customizes rendering to remove brackets
|
|
||||||
const markdownItFootnoteCustom = (md) => {
|
|
||||||
md.use(markdownItFootnote);
|
|
||||||
|
|
||||||
md.renderer.rules.footnote_ref = (tokens, idx, options, env, slf) => {
|
|
||||||
const id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);
|
|
||||||
const n = Number(tokens[idx].meta.id + 1).toString();
|
|
||||||
let refid = id;
|
|
||||||
|
|
||||||
if (tokens[idx].meta.subId > 0) {
|
|
||||||
refid += ':' + tokens[idx].meta.subId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return '<sup class="footnote-ref"><a href="#fn' + id + '" id="fnref' + refid + '">' + n + '</a></sup>';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = markdownItFootnoteCustom;
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
const hashtagRegex = /^#([a-zA-Z][a-zA-Z0-9_\\\/\-]*)(?![a-zA-Z0-9_-])/;
|
|
||||||
|
|
||||||
const HASH_CODE = '#'.charCodeAt(0);
|
|
||||||
const SPACE_CODE = ' '.charCodeAt(0);
|
|
||||||
const TAB_CODE = '\t'.charCodeAt(0);
|
|
||||||
const NEWLINE_CODE = '\n'.charCodeAt(0);
|
|
||||||
const CARRIAGE_RETURN_CODE = '\r'.charCodeAt(0);
|
|
||||||
|
|
||||||
const markdownItHashtag = (md) => {
|
|
||||||
md.inline.ruler.push('hashtag', function(state, silent) {
|
|
||||||
const pos = state.pos;
|
|
||||||
const ch = state.src.charCodeAt(pos);
|
|
||||||
|
|
||||||
if (ch !== HASH_CODE) return false;
|
|
||||||
|
|
||||||
if (pos > 0) {
|
|
||||||
const prevCh = state.src.charCodeAt(pos - 1);
|
|
||||||
if (prevCh !== SPACE_CODE && prevCh !== TAB_CODE && prevCh !== NEWLINE_CODE && prevCh !== CARRIAGE_RETURN_CODE) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const match = state.src.slice(pos).match(hashtagRegex);
|
|
||||||
if (!match) return false;
|
|
||||||
|
|
||||||
if (!silent) {
|
|
||||||
const token = state.push('hashtag', 'a', 0);
|
|
||||||
token.content = match[1];
|
|
||||||
token.markup = '#';
|
|
||||||
}
|
|
||||||
|
|
||||||
state.pos += match[0].length;
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
md.renderer.rules.hashtag = function(tokens, idx) {
|
|
||||||
const tagName = tokens[idx].content;
|
|
||||||
const slug = tagName.toLowerCase();
|
|
||||||
return `<a href="/tags/${slug}/" class="inline-tag">#${md.utils.escapeHtml(tagName)}</a>`;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = markdownItHashtag;
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
// Plugin: Strip trailing hashtag-only paragraphs from rendered output
|
|
||||||
// Must be applied AFTER footnote plugin since footnotes are moved to the end
|
|
||||||
const markdownItStripTrailingHashtags = (md) => {
|
|
||||||
md.core.ruler.push('strip_trailing_hashtags', function(state) {
|
|
||||||
const tokens = state.tokens;
|
|
||||||
|
|
||||||
const isHashtagOnlyParagraph = (inlineToken) =>
|
|
||||||
inlineToken?.type === 'inline' &&
|
|
||||||
inlineToken.children?.every(child =>
|
|
||||||
child.type === 'hashtag' ||
|
|
||||||
(child.type === 'text' && child.content.trim() === '') ||
|
|
||||||
child.type === 'softbreak'
|
|
||||||
) &&
|
|
||||||
inlineToken.children?.some(child => child.type === 'hashtag');
|
|
||||||
|
|
||||||
const isHashtagParagraphAt = (idx) =>
|
|
||||||
tokens[idx]?.type === 'paragraph_open' &&
|
|
||||||
tokens[idx + 1]?.type === 'inline' &&
|
|
||||||
tokens[idx + 2]?.type === 'paragraph_close' &&
|
|
||||||
isHashtagOnlyParagraph(tokens[idx + 1]);
|
|
||||||
|
|
||||||
const footnoteIdx = tokens.findIndex(t => t.type === 'footnote_block_open');
|
|
||||||
const footnoteSectionStart = footnoteIdx === -1 ? tokens.length : footnoteIdx;
|
|
||||||
|
|
||||||
const hashtagSectionStart = Array.from(
|
|
||||||
{ length: Math.floor(footnoteSectionStart / 3) },
|
|
||||||
(_, i) => footnoteSectionStart - 3 * (i + 1)
|
|
||||||
).reduce(
|
|
||||||
(start, idx) => isHashtagParagraphAt(idx) ? idx : start,
|
|
||||||
footnoteSectionStart
|
|
||||||
);
|
|
||||||
|
|
||||||
state.tokens = tokens.filter((_, idx) =>
|
|
||||||
idx < hashtagSectionStart || idx >= footnoteSectionStart
|
|
||||||
);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = markdownItStripTrailingHashtags;
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const extractTags = (content, mdInstance) => {
|
|
||||||
if (!content) return [];
|
|
||||||
|
|
||||||
const collectHashtags = (tokens) =>
|
|
||||||
tokens.flatMap(token => [
|
|
||||||
...(token.type === 'hashtag' ? [token.content] : []),
|
|
||||||
...(token.children ? collectHashtags(token.children) : [])
|
|
||||||
]);
|
|
||||||
|
|
||||||
const tokens = mdInstance.parse(content, {});
|
|
||||||
const tags = collectHashtags(tokens);
|
|
||||||
return [...new Set(tags)];
|
|
||||||
}
|
|
||||||
|
|
||||||
const expandHierarchicalTags = (tags) => {
|
|
||||||
const expanded = new Set();
|
|
||||||
tags.forEach(tag => {
|
|
||||||
expanded.add(tag);
|
|
||||||
const parts = tag.split('/');
|
|
||||||
for (let i = 1; i < parts.length; i++) {
|
|
||||||
const parentTag = parts.slice(0, i).join('/');
|
|
||||||
expanded.add(parentTag);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return [...expanded];
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPostTags = (post, tagMdInstance) => {
|
|
||||||
const filePath = post.inputPath;
|
|
||||||
try {
|
|
||||||
const content = fs.readFileSync(filePath, 'utf-8');
|
|
||||||
const tags = extractTags(content, tagMdInstance);
|
|
||||||
return tags.map(tag => tag.toLowerCase());
|
|
||||||
} catch (e) {
|
|
||||||
// Skip if file can't be read
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getRecipeTags = (recipe, tagMdInstance) => {
|
|
||||||
const filePath = recipe.inputPath;
|
|
||||||
try {
|
|
||||||
const content = fs.readFileSync(filePath, 'utf-8');
|
|
||||||
const tags = extractTags(content, tagMdInstance);
|
|
||||||
return tags.map(tag => tag.toLowerCase());
|
|
||||||
} catch (e) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
extractTags,
|
|
||||||
expandHierarchicalTags,
|
|
||||||
getPostTags,
|
|
||||||
getRecipeTags,
|
|
||||||
};
|
|
||||||
|
|
@ -29,7 +29,7 @@ eleventyComputed:
|
||||||
<div class="measurement-toggles" style="display: none;"></div>
|
<div class="measurement-toggles" style="display: none;"></div>
|
||||||
|
|
||||||
<div class="recipe-content">
|
<div class="recipe-content">
|
||||||
{{ recipe.content | renderRecipeMarkdown | safe }}
|
{{ recipe.content | safe }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% set recipeTags = recipe.inputPath | extractTagsFromFile %}
|
{% set recipeTags = recipe.inputPath | extractTagsFromFile %}
|
||||||
|
|
|
||||||
|
|
@ -218,7 +218,6 @@ const isNewestVersion = (filePath) => {
|
||||||
module.exports = {
|
module.exports = {
|
||||||
layout: "recipe.njk",
|
layout: "recipe.njk",
|
||||||
tags: ["recipe"],
|
tags: ["recipe"],
|
||||||
templateEngineOverride: "njk",
|
|
||||||
eleventyComputed: {
|
eleventyComputed: {
|
||||||
recipeSlug: (data) => {
|
recipeSlug: (data) => {
|
||||||
return getSlugFromPath(data.page.inputPath);
|
return getSlugFromPath(data.page.inputPath);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue