From 5b1f5f40edae8b0af4667eb52696cbf29cf79bb4 Mon Sep 17 00:00:00 2001 From: sss Date: Wed, 19 Feb 2025 00:36:11 +0100 Subject: [PATCH 01/11] inline footnotes --- quartz/plugins/transformers/ofm.ts | 67 +++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index b0b0a42ef..383210043 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -7,6 +7,7 @@ import { DefinitionContent, Paragraph, Code, + FootnoteDefinition, } from "mdast" import { Element, Literal, Root as HtmlRoot } from "hast" import { ReplaceFunction, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace" @@ -38,6 +39,7 @@ export interface Options { enableYouTubeEmbed: boolean enableVideoEmbed: boolean enableCheckbox: boolean + inlineFootnotes: boolean } const defaultOptions: Options = { @@ -52,7 +54,8 @@ const defaultOptions: Options = { enableInHtmlEmbed: false, enableYouTubeEmbed: true, enableVideoEmbed: true, - enableCheckbox: false, + enableCheckbox: true, + inlineFootnotes: true, } const calloutMapping = { @@ -143,6 +146,22 @@ const wikilinkImageEmbedRegex = new RegExp( /^(?(?!^\d*x?\d*$).*?)?(\|?\s*?(?\d+)(x(?\d+))?)?$/, ) +const inlineFootnoteRegex = /\^\[([\s\S]*?)\]/g + +// Helper function to match balanced brackets +function matchBalancedBrackets(text: string, startIndex: number): number { + let depth = 1; + let i = startIndex; + + while (i < text.length && depth > 0) { + if (text[i] === '[') depth++; + if (text[i] === ']') depth--; + i++; + } + + return depth === 0 ? i - 1 : -1; +} + export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin> = (userOpts) => { const opts = { ...defaultOptions, ...userOpts } @@ -213,6 +232,52 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin> }) } + if (opts.inlineFootnotes) { + let footnoteCounter = 1 + const footnotes: Record = {} + let result = "" + let currentIndex = 0 + + while (true) { + // Find next inline footnote start + const startMatch = src.indexOf("^[", currentIndex) + if (startMatch === -1) break + + // Add text before the footnote + result += src.slice(currentIndex, startMatch) + + // Find the matching closing bracket + const contentStart = startMatch + 2 + const contentEnd = matchBalancedBrackets(src, contentStart) + + if (contentEnd === -1) { + // No matching bracket found, treat as normal text + result += src.slice(startMatch, contentStart) + currentIndex = contentStart + } else { + // Extract footnote content + const content = src.slice(contentStart, contentEnd) + const id = `inline${Math.random().toString(36).substring(2, 8)}` + footnotes[id] = content.trim() + result += `[^${id}]` + currentIndex = contentEnd + 1 + } + } + + // Add remaining text + result += src.slice(currentIndex) + + // Append footnote definitions + if (Object.keys(footnotes).length > 0) { + result += "\n\n" + Object.entries(footnotes).forEach(([id, content]) => { + result += `[^${id}]: ${content}\n` + }) + } + + return result + } + return src }, markdownPlugins(_ctx) { From 781352f01a4d2063755c00244cbbbba116b519d8 Mon Sep 17 00:00:00 2001 From: sss Date: Wed, 19 Feb 2025 00:40:40 +0100 Subject: [PATCH 02/11] no checkbox --- quartz/plugins/transformers/ofm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index 383210043..b3c7ca92e 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -54,7 +54,7 @@ const defaultOptions: Options = { enableInHtmlEmbed: false, enableYouTubeEmbed: true, enableVideoEmbed: true, - enableCheckbox: true, + enableCheckbox: false, inlineFootnotes: true, } From bd83712be5de676312cf44b946b575027409f57f Mon Sep 17 00:00:00 2001 From: sss Date: Fri, 21 Feb 2025 00:56:34 +0100 Subject: [PATCH 03/11] use regex --- quartz/plugins/transformers/ofm.ts | 66 ++++++------------------------ 1 file changed, 13 insertions(+), 53 deletions(-) diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index b3c7ca92e..9dfd0dfd7 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -7,7 +7,6 @@ import { DefinitionContent, Paragraph, Code, - FootnoteDefinition, } from "mdast" import { Element, Literal, Root as HtmlRoot } from "hast" import { ReplaceFunction, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace" @@ -146,21 +145,7 @@ const wikilinkImageEmbedRegex = new RegExp( /^(?(?!^\d*x?\d*$).*?)?(\|?\s*?(?\d+)(x(?\d+))?)?$/, ) -const inlineFootnoteRegex = /\^\[([\s\S]*?)\]/g - -// Helper function to match balanced brackets -function matchBalancedBrackets(text: string, startIndex: number): number { - let depth = 1; - let i = startIndex; - - while (i < text.length && depth > 0) { - if (text[i] === '[') depth++; - if (text[i] === ']') depth--; - i++; - } - - return depth === 0 ? i - 1 : -1; -} +const inlineFootnoteRegex = /\^\[((?:[^\[\]]|\[(?:[^\[\]]|\[[^\[\]]*\])*\])*)\]/g export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin> = (userOpts) => { const opts = { ...defaultOptions, ...userOpts } @@ -233,46 +218,21 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin> } if (opts.inlineFootnotes) { - let footnoteCounter = 1 + // Replaces ^[inline] footnotes with regular footnotes [^1]: const footnotes: Record = {} - let result = "" - let currentIndex = 0 + + // Replace inline footnotes with references and collect definitions + const result = src.replace(inlineFootnoteRegex, (_match, content) => { + const id = `inline-${Math.random().toString(36).substring(2, 8)}` + footnotes[id] = content.trim() + return `[^${id}]` + }) - while (true) { - // Find next inline footnote start - const startMatch = src.indexOf("^[", currentIndex) - if (startMatch === -1) break - - // Add text before the footnote - result += src.slice(currentIndex, startMatch) - - // Find the matching closing bracket - const contentStart = startMatch + 2 - const contentEnd = matchBalancedBrackets(src, contentStart) - - if (contentEnd === -1) { - // No matching bracket found, treat as normal text - result += src.slice(startMatch, contentStart) - currentIndex = contentStart - } else { - // Extract footnote content - const content = src.slice(contentStart, contentEnd) - const id = `inline${Math.random().toString(36).substring(2, 8)}` - footnotes[id] = content.trim() - result += `[^${id}]` - currentIndex = contentEnd + 1 - } - } - - // Add remaining text - result += src.slice(currentIndex) - - // Append footnote definitions + // Append footnote definitions if we found any if (Object.keys(footnotes).length > 0) { - result += "\n\n" - Object.entries(footnotes).forEach(([id, content]) => { - result += `[^${id}]: ${content}\n` - }) + return result + "\n\n" + Object.entries(footnotes) + .map(([id, content]) => `[^${id}]: ${content}`) + .join("\n") + "\n" } return result From 1ebe312d09ba1623e0c5554729ca0cddbe61d588 Mon Sep 17 00:00:00 2001 From: sss Date: Fri, 21 Feb 2025 01:11:49 +0100 Subject: [PATCH 04/11] documentation for inlineFootnotes --- docs/plugins/ObsidianFlavoredMarkdown.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/plugins/ObsidianFlavoredMarkdown.md b/docs/plugins/ObsidianFlavoredMarkdown.md index 414f743b8..f02425f74 100644 --- a/docs/plugins/ObsidianFlavoredMarkdown.md +++ b/docs/plugins/ObsidianFlavoredMarkdown.md @@ -23,6 +23,7 @@ This plugin accepts the following configuration options: - `enableYouTubeEmbed`: If `true` (default), enables the embedding of YouTube videos and playlists using external image Markdown syntax. - `enableVideoEmbed`: If `true` (default), enables the embedding of video files. - `enableCheckbox`: If `true`, adds support for interactive checkboxes in content. Defaults to `false`. +- `inlineFootnotes`: If `true` (default), enables parsing of inline footnotes. > [!warning] > Don't remove this plugin if you're using [[Obsidian compatibility|Obsidian]] to author the content! From aae897a0d57441cdfcc570db7809d55c134e8fc8 Mon Sep 17 00:00:00 2001 From: sss Date: Fri, 21 Feb 2025 01:16:13 +0100 Subject: [PATCH 05/11] fix ts errors --- quartz/plugins/transformers/ofm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index 9dfd0dfd7..f34f0a195 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -222,7 +222,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin> const footnotes: Record = {} // Replace inline footnotes with references and collect definitions - const result = src.replace(inlineFootnoteRegex, (_match, content) => { + const result = (src as string).replace(inlineFootnoteRegex, (_match: string, content: string) => { const id = `inline-${Math.random().toString(36).substring(2, 8)}` footnotes[id] = content.trim() return `[^${id}]` From eaac39444af9ad2bd9c363f7d3e60f5d9bbcfd47 Mon Sep 17 00:00:00 2001 From: sss Date: Fri, 21 Feb 2025 01:19:38 +0100 Subject: [PATCH 06/11] prettier --- quartz/plugins/transformers/ofm.ts | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index f34f0a195..58fb64616 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -220,19 +220,27 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin> if (opts.inlineFootnotes) { // Replaces ^[inline] footnotes with regular footnotes [^1]: const footnotes: Record = {} - + // Replace inline footnotes with references and collect definitions - const result = (src as string).replace(inlineFootnoteRegex, (_match: string, content: string) => { - const id = `inline-${Math.random().toString(36).substring(2, 8)}` - footnotes[id] = content.trim() - return `[^${id}]` - }) + const result = (src as string).replace( + inlineFootnoteRegex, + (_match: string, content: string) => { + const id = `inline-${Math.random().toString(36).substring(2, 8)}` + footnotes[id] = content.trim() + return `[^${id}]` + }, + ) // Append footnote definitions if we found any if (Object.keys(footnotes).length > 0) { - return result + "\n\n" + Object.entries(footnotes) - .map(([id, content]) => `[^${id}]: ${content}`) - .join("\n") + "\n" + return ( + result + + "\n\n" + + Object.entries(footnotes) + .map(([id, content]) => `[^${id}]: ${content}`) + .join("\n") + + "\n" + ) } return result From 5e2ec38a4b53b73a043d9ac38622bc73d857d648 Mon Sep 17 00:00:00 2001 From: sss Date: Fri, 21 Feb 2025 01:21:45 +0100 Subject: [PATCH 07/11] use passive lang --- quartz/plugins/transformers/ofm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index 58fb64616..fecbcb5d3 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -231,7 +231,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin> }, ) - // Append footnote definitions if we found any + // Append footnote definitions if any are found if (Object.keys(footnotes).length > 0) { return ( result + From 7e04bb4e6e42d4d7ac8c021b3a63c37e322a7300 Mon Sep 17 00:00:00 2001 From: sss Date: Mon, 24 Feb 2025 00:03:03 +0100 Subject: [PATCH 08/11] no random inline footnote ids --- quartz/plugins/transformers/ofm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index fecbcb5d3..dc337e9fe 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -225,7 +225,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin> const result = (src as string).replace( inlineFootnoteRegex, (_match: string, content: string) => { - const id = `inline-${Math.random().toString(36).substring(2, 8)}` + const id = `inline-${Object.keys(footnotes).length + 1}` footnotes[id] = content.trim() return `[^${id}]` }, From df3cdb18b83d8f4899879c0dd7b146379ad6fd5f Mon Sep 17 00:00:00 2001 From: sss Date: Mon, 3 Mar 2025 07:06:49 +0100 Subject: [PATCH 09/11] add counter, regex comments, change inlineFootnotes to enableInlineFootnotes --- quartz/plugins/transformers/ofm.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index dc337e9fe..c8c069798 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -38,7 +38,7 @@ export interface Options { enableYouTubeEmbed: boolean enableVideoEmbed: boolean enableCheckbox: boolean - inlineFootnotes: boolean + enableInlineFootnotes: boolean } const defaultOptions: Options = { @@ -54,7 +54,7 @@ const defaultOptions: Options = { enableYouTubeEmbed: true, enableVideoEmbed: true, enableCheckbox: false, - inlineFootnotes: true, + enableInlineFootnotes: true, } const calloutMapping = { @@ -145,7 +145,12 @@ const wikilinkImageEmbedRegex = new RegExp( /^(?(?!^\d*x?\d*$).*?)?(\|?\s*?(?\d+)(x(?\d+))?)?$/, ) -const inlineFootnoteRegex = /\^\[((?:[^\[\]]|\[(?:[^\[\]]|\[[^\[\]]*\])*\])*)\]/g +const inlineFootnoteRegex = new RegExp(/\^\[((?:[^\[\]]|\[(?:[^\[\]]|\[[^\[\]]*\])*\])*)\]/g) +// match inline footnotes where content can contain any properly nested brackets +// \^\[...\] -> matches ^[inline footnote's] brackets +// (?:...) -> does not capture any of the following: +// [^\[\]] -> any character that is not a bracket +// \[(?:[^\[\]]|\[[^\[\]]*\])*\] -> a properly nested set of brackets export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin> = (userOpts) => { const opts = { ...defaultOptions, ...userOpts } @@ -217,15 +222,16 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin> }) } - if (opts.inlineFootnotes) { + if (opts.enableInlineFootnotes) { // Replaces ^[inline] footnotes with regular footnotes [^1]: const footnotes: Record = {} + let counter = 1 // Replace inline footnotes with references and collect definitions const result = (src as string).replace( inlineFootnoteRegex, (_match: string, content: string) => { - const id = `inline-${Object.keys(footnotes).length + 1}` + const id = `inline-${counter++}` footnotes[id] = content.trim() return `[^${id}]` }, From 2752c3a62dd1e86126b43451f057e37bcfc121ed Mon Sep 17 00:00:00 2001 From: sss Date: Mon, 3 Mar 2025 07:09:45 +0100 Subject: [PATCH 10/11] Change inlineFootnotes name in docs --- docs/plugins/ObsidianFlavoredMarkdown.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/ObsidianFlavoredMarkdown.md b/docs/plugins/ObsidianFlavoredMarkdown.md index f02425f74..6de9cac18 100644 --- a/docs/plugins/ObsidianFlavoredMarkdown.md +++ b/docs/plugins/ObsidianFlavoredMarkdown.md @@ -23,7 +23,7 @@ This plugin accepts the following configuration options: - `enableYouTubeEmbed`: If `true` (default), enables the embedding of YouTube videos and playlists using external image Markdown syntax. - `enableVideoEmbed`: If `true` (default), enables the embedding of video files. - `enableCheckbox`: If `true`, adds support for interactive checkboxes in content. Defaults to `false`. -- `inlineFootnotes`: If `true` (default), enables parsing of inline footnotes. +- `enableInlineFootnotes`: If `true` (default), enables parsing of inline footnotes. > [!warning] > Don't remove this plugin if you're using [[Obsidian compatibility|Obsidian]] to author the content! From 84349af6dd128f8da7a29febcc86f4939f328ec9 Mon Sep 17 00:00:00 2001 From: Aaron Pham Date: Mon, 3 Mar 2025 01:45:28 -0500 Subject: [PATCH 11/11] Apply suggestions from code review --- quartz/plugins/transformers/ofm.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index c8c069798..43848cda6 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -225,13 +225,13 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin> if (opts.enableInlineFootnotes) { // Replaces ^[inline] footnotes with regular footnotes [^1]: const footnotes: Record = {} - let counter = 1 + let counter = 0 // Replace inline footnotes with references and collect definitions const result = (src as string).replace( inlineFootnoteRegex, (_match: string, content: string) => { - const id = `inline-${counter++}` + const id = `generated-inline-footnote-${counter++}` footnotes[id] = content.trim() return `[^${id}]` },