refac: sources and citations

This commit is contained in:
Timothy Jaeryang Baek
2025-11-23 18:27:57 -05:00
parent b0491886bc
commit ec45d77ce9
11 changed files with 201 additions and 62 deletions
+1 -25
View File
@@ -32,7 +32,7 @@ function escapeRegExp(string: string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
export const replaceTokens = (content, sourceIds, char, user) => {
export const replaceTokens = (content, char, user) => {
const tokens = [
{ regex: /{{char}}/gi, replacement: char },
{ regex: /{{user}}/gi, replacement: user },
@@ -67,30 +67,6 @@ export const replaceTokens = (content, sourceIds, char, user) => {
}
});
if (Array.isArray(sourceIds)) {
// Match both [1], [2], and [1,2,3] forms
const multiRefRegex = /\[([\d,\s]+)\]/g;
segment = segment.replace(multiRefRegex, (match, group) => {
// Extract numbers like 1,2,3
const indices = group
.split(',')
.map((n) => parseInt(n.trim(), 10))
.filter((n) => !isNaN(n));
// Replace each index with a <source_id> tag
const sources = indices
.map((idx) => {
const sourceId = sourceIds[idx - 1];
return sourceId
? `<source_id data="${idx}" title="${encodeURIComponent(sourceId)}" />`
: `[${idx}]`;
})
.join('');
return sources;
});
}
return segment;
});
@@ -0,0 +1,55 @@
export function citationExtension() {
return {
name: 'citation',
level: 'inline' as const,
start(src: string) {
// Trigger on any [number]
return src.search(/\[(\d[\d,\s]*)\]/);
},
tokenizer(src: string) {
// Avoid matching footnotes
if (/^\[\^/.test(src)) return;
// Match ONE OR MORE adjacent [1] or [1,2] blocks
// Example matched: "[1][2,3][4]"
const rule = /^(\[(?:\d[\d,\s]*)\])+/;
const match = rule.exec(src);
if (!match) return;
const raw = match[0];
// Extract ALL bracket groups inside the big match
const groupRegex = /\[([\d,\s]+)\]/g;
const ids: number[] = [];
let m: RegExpExecArray | null;
while ((m = groupRegex.exec(raw))) {
const parsed = m[1]
.split(',')
.map((n) => parseInt(n.trim(), 10))
.filter((n) => !isNaN(n));
ids.push(...parsed);
}
return {
type: 'citation',
raw,
ids // merged list
};
},
renderer(token: any) {
// e.g. "1,2,3"
return token.ids.join(',');
}
};
}
export default function () {
return {
extensions: [citationExtension()]
};
}
@@ -0,0 +1,38 @@
// footnote-extension.ts
// Simple extension for marked to support footnote references like [^1], [^note]
function escapeHtml(s: string) {
return s.replace(
/[&<>"']/g,
(c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]!
);
}
export function footnoteExtension() {
return {
name: 'footnote',
level: 'inline' as const,
start(src: string) {
return src.search(/\[\^\s*[a-zA-Z0-9_-]+\s*\]/);
},
tokenizer(src: string) {
const rule = /^\[\^\s*([a-zA-Z0-9_-]+)\s*\]/;
const match = rule.exec(src);
if (match) {
const escapedText = escapeHtml(match[1]);
return {
type: 'footnote',
raw: match[0],
text: match[1],
escapedText: escapedText
};
}
}
};
}
export default function () {
return {
extensions: [footnoteExtension()]
};
}