- "frontend/src/api/public-client.ts" - "frontend/src/pages/TechniquePage.tsx" - "frontend/src/components/TableOfContents.tsx" - "frontend/src/utils/citations.tsx" - "frontend/src/App.css" GSD-Task: S05/T01
76 lines
2.1 KiB
TypeScript
76 lines
2.1 KiB
TypeScript
/**
|
|
* Parse [N] and [N,M] citation markers in text and replace with React anchor links.
|
|
*
|
|
* Citations are 1-based indices into the key_moments array.
|
|
* Each marker becomes a superscript link to #km-{momentId}.
|
|
*/
|
|
|
|
import React from "react";
|
|
import type { KeyMomentSummary } from "../api/public-client";
|
|
|
|
// Matches [1], [2,3], [1,2,3], etc.
|
|
const CITATION_RE = /\[(\d+(?:,\s*\d+)*)\]/g;
|
|
|
|
/**
|
|
* Convert a text string containing [N] or [N,M] markers into an array of
|
|
* React nodes — plain strings interleaved with citation anchor elements.
|
|
*/
|
|
export function parseCitations(
|
|
text: string,
|
|
keyMoments: KeyMomentSummary[],
|
|
): React.ReactNode[] {
|
|
const nodes: React.ReactNode[] = [];
|
|
let lastIndex = 0;
|
|
|
|
for (const match of text.matchAll(CITATION_RE)) {
|
|
const matchStart = match.index ?? 0;
|
|
// Push text before this match
|
|
if (matchStart > lastIndex) {
|
|
nodes.push(text.slice(lastIndex, matchStart));
|
|
}
|
|
|
|
// Parse the indices from the match group
|
|
const rawGroup = match[1];
|
|
if (!rawGroup) continue;
|
|
const indices = rawGroup.split(",").map((s) => parseInt(s.trim(), 10));
|
|
const links: React.ReactNode[] = [];
|
|
|
|
for (let i = 0; i < indices.length; i++) {
|
|
const idx = indices[i]!;
|
|
// Citation indices are 1-based; key_moments array is 0-based
|
|
const moment = keyMoments[idx - 1];
|
|
if (moment) {
|
|
if (i > 0) links.push(", ");
|
|
links.push(
|
|
<a
|
|
key={`${matchStart}-${idx}`}
|
|
href={`#km-${moment.id}`}
|
|
className="citation-link"
|
|
title={moment.title}
|
|
>
|
|
{idx}
|
|
</a>,
|
|
);
|
|
} else {
|
|
// Invalid index — render as plain text
|
|
if (i > 0) links.push(", ");
|
|
links.push(String(idx));
|
|
}
|
|
}
|
|
|
|
nodes.push(
|
|
<sup key={`cite-${matchStart}`} className="citation-group">
|
|
[{links}]
|
|
</sup>,
|
|
);
|
|
|
|
lastIndex = matchStart + match[0].length;
|
|
}
|
|
|
|
// Push remaining text after the last match
|
|
if (lastIndex < text.length) {
|
|
nodes.push(text.slice(lastIndex));
|
|
}
|
|
|
|
return nodes.length > 0 ? nodes : [text];
|
|
}
|