diff --git a/eleventy.config.js b/eleventy.config.js index 22882c0..20539fc 100644 --- a/eleventy.config.js +++ b/eleventy.config.js @@ -40,15 +40,12 @@ const extractTags = (content, mdInstance) => { return [...new Set(tags)]; } -const getPostTags = (post, mdInstance) => { +const getPostTags = (post, tagMdInstance) => { const filePath = post.inputPath; try { const content = fs.readFileSync(filePath, 'utf-8'); - const tags = extractTags(content, mdInstance); - return tags.map(tag => { - const normalizedTag = tag.toLowerCase(); - return normalizedTag - }); + const tags = extractTags(content, tagMdInstance); + return tags.map(tag => tag.toLowerCase()); } catch (e) { // Skip if file can't be read return [] @@ -60,13 +57,13 @@ const isReleased = (post) => { } const markdownItHashtag = (md) => { - const hashtagRegex = /^#([a-zA-Z][a-zA-Z0-9_\/]*)(?![a-zA-Z0-9_-])/; + 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 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; @@ -99,42 +96,94 @@ const markdownItHashtag = (md) => { const slug = tagName.toLowerCase(); return `#${md.utils.escapeHtml(tagName)}`; }; -} +}; -const md = markdownIt({ - html: true, - breaks: true, - linkify: true -}); +// 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; + }); +}; -md.use(markdownItContainer, 'details', { - validate: function (params) { - return params.trim().match(/^(.*)$/); - }, - render: function (tokens, idx) { - const m = tokens[idx].info.trim().match(/^(.*)$/); - if (tokens[idx].nesting === 1) { - const title = md.utils.escapeHtml(m[1]); - return `\n'; +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 `\\n'; } - } -}); + }); +}; -md.use(markdownItFootnote); -md.use(markdownItHashtag); -md.use(markdownItMermaid); -md.use(markdownItTaskLists, { enabled: true, label: true, labelAfter: true }); +const sharedPlugins = [ + markdownItFootnote, + markdownItHashtag, + markdownItMermaid, + [markdownItTaskLists, { enabled: true, label: true, labelAfter: true }], + markdownItDetails, +]; + +const applyPlugins = (md, plugins) => + plugins.reduce((instance, plugin) => { + const [pluginFn, options] = Array.isArray(plugin) ? plugin : [plugin]; + return instance.use(pluginFn, options), instance; + }, md); + +const createMarkdownInstance = (extraPlugins = []) => { + const md = markdownIt({ html: true, breaks: true, linkify: true }); + applyPlugins(md, [...sharedPlugins, ...extraPlugins]); + return md; +}; + +const md = createMarkdownInstance([markdownItStripTrailingHashtags]); + +const tagExtractorMd = createMarkdownInstance(); module.exports = (eleventyConfig) => { eleventyConfig.addPlugin(syntaxHighlight); - eleventyConfig.addFilter("extractTags", (content) => extractTags(content, md)); + eleventyConfig.addFilter("extractTags", (content) => extractTags(content, tagExtractorMd)); eleventyConfig.addFilter("extractTagsFromFile", (filePath) => { try { const content = fs.readFileSync(filePath, 'utf-8'); - return extractTags(content, md); + return extractTags(content, tagExtractorMd); } catch (e) { return []; } @@ -216,7 +265,7 @@ module.exports = (eleventyConfig) => { const tagMap = {}; posts.forEach(post => { - const tags = getPostTags(post, md) + const tags = getPostTags(post, tagExtractorMd) tags.forEach((tag) => { tagMap[tag] = { name: tag, @@ -275,7 +324,7 @@ module.exports = (eleventyConfig) => { const filePath = recipe.inputPath; try { const content = fs.readFileSync(filePath, 'utf-8'); - const tags = extractTags(content, md); + const tags = extractTags(content, tagExtractorMd); return tags.map(tag => tag.toLowerCase()); } catch (e) { return []; @@ -287,7 +336,7 @@ module.exports = (eleventyConfig) => { const recipes = collectionApi.getFilteredByGlob("recipes/**/*.md") .filter(r => r.data.isNewestVersion && r.data.draft !== true); - const postTags = posts.flatMap(post => getPostTags(post, md)); + const postTags = posts.flatMap(post => getPostTags(post, tagExtractorMd)); const recipeTags = recipes.flatMap(recipe => getRecipeTags(recipe)); return [...new Set([...postTags, ...recipeTags])].sort(); @@ -306,7 +355,7 @@ module.exports = (eleventyConfig) => { // Build tag map from posts const postTagMap = posts.reduce((acc, post) => { - const tags = getPostTags(post, md); + const tags = getPostTags(post, tagExtractorMd); return tags.reduce((innerAcc, tag) => ({ ...innerAcc, [tag]: { diff --git a/posts/drafts/hyper-logLog-tombstone-garbage-collection.md b/posts/drafts/hyper-logLog-tombstone-garbage-collection.md index a4169fb..e073227 100644 --- a/posts/drafts/hyper-logLog-tombstone-garbage-collection.md +++ b/posts/drafts/hyper-logLog-tombstone-garbage-collection.md @@ -254,4 +254,6 @@ The HyperLogLog approach trades exactness (~3% error) for dramatic scalability ## References -A working simulation is available at [simulations/hyperloglog-tombstone/simulation.ts](/simulations/hyperloglog-tombstone/simulation.ts). \ No newline at end of file +A working simulation is available at [simulations/hyperloglog-tombstone/simulation.ts](/simulations/hyperloglog-tombstone/simulation.ts). + +#algorithm #computer #distributed #peer_to_peer \ No newline at end of file