fix: convert Substack footnotes to Obsidian [^N] syntax via Turndown rules

This commit is contained in:
@gapmiss 2026-06-19 20:39:51 -05:00
parent 83402ad77d
commit 8dacb96863
3 changed files with 35 additions and 13 deletions

View file

@ -95,6 +95,39 @@ export function createConverter(downloadedUrls: Set<string>): 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;
}

View file

@ -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 = '';

View file

@ -1,4 +1,4 @@
export function postprocessMarkdown(md: string, domain: string, downloadedUrls: Set<string>): string {
export function postprocessMarkdown(md: string, downloadedUrls: Set<string>): 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,