diff --git a/src/converter.ts b/src/converter.ts index 45c9b06..8dd3982 100644 --- a/src/converter.ts +++ b/src/converter.ts @@ -95,6 +95,39 @@ export function createConverter(downloadedUrls: Set): TurndownService { }, }); + turndown.addRule('footnoteRef', { + filter: (node) => { + if (node.nodeName !== 'A') return false; + return /^#footnote-\d+$/.test(node.getAttribute('href') ?? ''); + }, + replacement: (content) => `[^${content.trim()}]`, + }); + + turndown.addRule('footnoteBackLink', { + filter: (node) => { + if (node.nodeName !== 'A') return false; + return /^#footnote-anchor-\d+$/.test(node.getAttribute('href') ?? ''); + }, + replacement: () => '', + }); + + turndown.addRule('footnoteDef', { + filter: (node) => { + if (node.nodeName !== 'DIV') return false; + const backLink = node.querySelector('a[href^="#footnote-anchor-"]'); + return backLink !== null && backLink.parentElement === node; + }, + replacement: (content, node) => { + const anchor = node.querySelector('a[href^="#footnote-anchor-"]'); + const num = (anchor?.textContent ?? '').trim(); + const text = content.trim(); + const indented = text.split('\n').map((line, i) => + i === 0 || line === '' ? line : ' ' + line + ).join('\n'); + return `\n\n[^${num}]: ${indented}\n\n`; + }, + }); + return turndown; } diff --git a/src/main.ts b/src/main.ts index c7625e6..c2e2b14 100644 --- a/src/main.ts +++ b/src/main.ts @@ -98,7 +98,7 @@ export default class SubstackClipperPlugin extends Plugin { } } - let processedMd = postprocessMarkdown(markdown, domain, downloadResult.downloaded); + let processedMd = postprocessMarkdown(markdown, downloadResult.downloaded); // Build podcast/video/transcript sections let podcastVideoPlayer = ''; diff --git a/src/postprocess.ts b/src/postprocess.ts index 1a82c6b..9ed83d8 100644 --- a/src/postprocess.ts +++ b/src/postprocess.ts @@ -1,4 +1,4 @@ -export function postprocessMarkdown(md: string, domain: string, downloadedUrls: Set): string { +export function postprocessMarkdown(md: string, downloadedUrls: Set): string { let out = md; // iframe spacing @@ -11,17 +11,6 @@ export function postprocessMarkdown(md: string, domain: string, downloadedUrls: // fix headings glued to links out = out.replace(/(\))(#{1,6})/g, '$1\n\n$2'); - // footnotes - const escapedDomain = domain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - out = out.replace( - new RegExp(`\\[(\\d)\\]\\(${escapedDomain}/p/([a-zA-Z0-9-]+)#footnote-\\d-\\d+\\)`, 'g'), - '[^$1]', - ); - out = out.replace( - new RegExp(`\\[(\\d)\\]\\(${escapedDomain}/p/([a-zA-Z0-9-]+)#footnote-anchor-\\d-\\d+\\)`, 'g'), - '[^$1]: ', - ); - // truncated post previews out = out.replace( /\[(.*)\n(\n)?-+\]\([a-z0-9-:/.]+\)(.*)(\n\n)?\[Read full story\]\(([a-z0-9-]+)\)/g,