All checks were successful
Forgejo Android APK / Build signed APK (push) Successful in 1m58s
The earlier fix corrected two of the repair rules and left the rest with the same flaw, and the punctuation boundary it introduced let the table case back in. A review found the remainder. Every repair rule inserted newlines with no idea whether it was inside a markdown table row. A dose row such as "| Dexamethasone | 0.6 mg/kg PO (max 16 mg) - single dose | 1 |" was split mid-row, which drops that row's citation and every row below it out of the table. A "#" column destroyed the table outright. The rules now skip table rows, dividers and headers entirely. Ranges were still split in three other places: in a heading, so "### Dexamethasone 0.15 - 0.6 mg/kg" rendered as a heading reading "Dexamethasone 0.15"; after a citation; and beside a hash, where "Room # 4" became a heading. A hyphen now starts a list item only when a number does not follow, and a run-together heading is recognised by the capital letter after the hashes rather than by position. Adjacent citation merging could also cross a paragraph break, turning "[1]\n\n[2] Vancomycin only if MRSA" into "[1, 2]Vancomycin only if MRSA" -- joining two paragraphs and moving a citation onto a claim it never supported. Merging is now limited to citations on the same line. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
338 lines
20 KiB
JavaScript
338 lines
20 KiB
JavaScript
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs/promises');
|
|
const path = require('node:path');
|
|
const MarkdownIt = require('markdown-it');
|
|
|
|
let modulePromise;
|
|
|
|
async function loadCitationModule() {
|
|
if (!modulePromise) {
|
|
modulePromise = fs.readFile(path.join(__dirname, '..', 'public', 'js', 'assistant', 'citations.js'), 'utf8')
|
|
.then((source) => import('data:text/javascript;charset=utf-8,' + encodeURIComponent(source)));
|
|
}
|
|
return modulePromise;
|
|
}
|
|
|
|
const sources = [
|
|
{ title: 'Nelson Textbook of Pediatrics' },
|
|
{ title: 'Pediatric Asthma Guideline' },
|
|
{ resource: 'Emergency Medicine Review' }
|
|
];
|
|
|
|
test('renders citation clusters as links to matching source cards', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const html = renderAssistantMarkdown('Give magnesium for severe exacerbation [1, 2].', sources);
|
|
assert.match(html, /href="#assistant-source-1"/);
|
|
assert.match(html, /href="#assistant-source-2"/);
|
|
assert.match(html, /title="Source 1: Nelson Textbook of Pediatrics"/);
|
|
assert.match(html, /data-source-number="1"/);
|
|
assert.match(html, /data-source-number="2"/);
|
|
assert.match(html, /<a class="assistant-cite"[^>]*>src<\/a> <a class="assistant-cite"[^>]*>src<\/a>/);
|
|
});
|
|
|
|
test('can render citation labels as numbers for PDF export', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const html = renderAssistantMarkdown('Give magnesium for severe exacerbation [1, 2].', sources, { citationLabel: 'number' });
|
|
assert.match(html, /<a class="assistant-cite"[^>]*>1<\/a> <a class="assistant-cite"[^>]*>2<\/a>/);
|
|
});
|
|
|
|
test('leaves unknown citation clusters untouched rather than guessing', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const html = renderAssistantMarkdown('Dose statement [4].', sources);
|
|
assert.match(html, /\[4\]/);
|
|
assert.doesNotMatch(html, /assistant-source-4/);
|
|
});
|
|
|
|
test('does not merge separate citations across separate claims', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const html = renderAssistantMarkdown('Use oxygen [1]. Give bronchodilator [2].', sources);
|
|
assert.match(html, /oxygen <a class="assistant-cite"[^>]*data-source-number="1"[^>]*>src<\/a>/);
|
|
assert.match(html, /bronchodilator <a class="assistant-cite"[^>]*data-source-number="2"[^>]*>src<\/a>/);
|
|
assert.doesNotMatch(html, /data-source-number="1"[^>]*>src<\/a><a class="assistant-cite"[^>]*data-source-number="2"/);
|
|
});
|
|
|
|
test('sorts adjacent citation tokens into one safe Vancouver cluster', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const html = renderAssistantMarkdown('Deteriorating course [1][4][2][3].', [
|
|
{ title: 'A' }, { title: 'B' }, { title: 'C' }, { title: 'D' }
|
|
]);
|
|
assert.match(html, /data-source-number="1"[^>]*>src<\/a> <a class="assistant-cite"[^>]*data-source-number="2"[^>]*>src<\/a> <a class="assistant-cite"[^>]*data-source-number="3"[^>]*>src<\/a> <a class="assistant-cite"[^>]*data-source-number="4"[^>]*>src<\/a>/);
|
|
assert.doesNotMatch(html, /\]<span|\]\[/);
|
|
});
|
|
|
|
test('repairs a clearly adjacent trailing citation missing its closing bracket', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const html = renderAssistantMarkdown('Deteriorating course [1][4][2][3', [
|
|
{ title: 'A' }, { title: 'B' }, { title: 'C' }, { title: 'D' }
|
|
]);
|
|
assert.match(html, /data-source-number="1"[^>]*>src<\/a> <a class="assistant-cite"[^>]*data-source-number="2"[^>]*>src<\/a> <a class="assistant-cite"[^>]*data-source-number="3"[^>]*>src<\/a> <a class="assistant-cite"[^>]*data-source-number="4"[^>]*>src<\/a>/);
|
|
});
|
|
|
|
test('does not normalize adjacent citations if any source number is unknown', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const html = renderAssistantMarkdown('Unsupported claim [1][9].', sources);
|
|
assert.match(html, /\[9\]/);
|
|
assert.doesNotMatch(html, /assistant-source-9/);
|
|
});
|
|
|
|
test('escapes source titles inside citation link titles', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const html = renderAssistantMarkdown('Unsafe title [1].', [{ title: 'Bad "Title" <script>' }]);
|
|
assert.match(html, /title="Source 1: Bad "Title" <script>"/);
|
|
assert.doesNotMatch(html, /<script>/);
|
|
});
|
|
|
|
test('does not strip sentences that mention available sources', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const html = renderAssistantMarkdown('Pneumonia can cause parapneumonic effusion [1]. However, the available sources do not specifically identify adenovirus as a typical cause [2]. In summary, evidence is insufficient [3].', sources);
|
|
assert.match(html, /available sources do not specifically identify adenovirus/);
|
|
assert.match(html, /In summary, evidence is insufficient/);
|
|
});
|
|
|
|
test('normalizes inline markdown headings before rendering', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const html = renderAssistantMarkdown('Intro ### Management\n- oxygen', sources);
|
|
assert.match(html, /<h3>Management<\/h3>/);
|
|
assert.match(html, /<li>oxygen<\/li>/);
|
|
});
|
|
|
|
test('normalizes old saved assistant answer formatting', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const html = renderAssistantMarkdown('Comparison of Bronchiolitis and Asthma Management in Infants ### Bronchiolitis Management - Mainstay: Supportive care [1, 2]. - Medications: bronchodilators are not routine [2]. ### Key Differences | Aspect | Bronchiolitis | Asthma | |---|---|---| | Therapy | Supportive | SABA | --- References: [1] duplicate', sources);
|
|
assert.match(html, /<h3>Bronchiolitis Management<\/h3>/);
|
|
assert.match(html, /<li>Mainstay: Supportive care/);
|
|
assert.match(html, /Key Differences/);
|
|
});
|
|
|
|
test('repairs smashed admission bullet list after citations', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const html = renderAssistantMarkdown('Admission is indicated for: Apnoea [1, 2]- **Persistent oxygen saturation <90%** [1, 2]- **Severe respiratory distress** [1]', sources);
|
|
assert.match(html, /<li><strong>Persistent oxygen saturation <90%<\/strong>/);
|
|
assert.match(html, /<li><strong>Severe respiratory distress<\/strong>/);
|
|
assert.doesNotMatch(html, /\]-/);
|
|
});
|
|
|
|
test('repairs smashed head injury red flag bullets after citation clusters', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const html = renderAssistantMarkdown('CT is indicated for: [1, 2, 3]- Focal neurologic deficits [1]- Signs of skull fracture [2]- Recurrent vomiting [3]', sources);
|
|
assert.match(html, /<li>Focal neurologic deficits/);
|
|
assert.match(html, /<li>Signs of skull fracture/);
|
|
assert.match(html, /<li>Recurrent vomiting/);
|
|
assert.doesNotMatch(html, /\]-/);
|
|
});
|
|
|
|
test('preserves code block contents without creating citation links inside code', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const html = renderAssistantMarkdown('```js\nconst ref = "[1]";\n```', sources);
|
|
assert.match(html, /<pre><code>const ref = "\[1\]"/);
|
|
assert.doesNotMatch(html, /assistant-source-1/);
|
|
});
|
|
|
|
test('does not repair source tables inside code blocks', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const html = renderAssistantMarkdown('```md\n| Feature | Source(s) |\n|---|---|\n| Example | 1, 2 |\n```', sources);
|
|
assert.match(html, /\| Example \| 1, 2 \|/);
|
|
assert.doesNotMatch(html, /assistant-source-1/);
|
|
});
|
|
|
|
test('does not repair model-split markdown tables', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const html = renderAssistantMarkdown('Summary Table\n| Indication for Use | Dose (IV) | Max Dose\n\n| Infusion Time | Monitoring/Precautions |\n|-----------------------------------------|----------------------------|----------|--------------|---------------------------------------|\n| Severe exacerbation unresponsive to SABA/anticholinergic | 25-75 mg/kg | 2 g\n| 20 min | BP, reflexes, respiratory status[1, 2] |', sources);
|
|
assert.doesNotMatch(html, /Max Dose Infusion Time/);
|
|
assert.match(html, /Infusion Time/);
|
|
assert.match(html, /assistant-cite/);
|
|
});
|
|
|
|
test('breaks inline numbered clinical sections onto new lines', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const html = renderAssistantMarkdown('Monitor with serial abdominal exams [1]. 2. Imaging\nImmediate abdominal X-ray for obstruction [1, 2]. 3. Laboratory Studies\nAssess electrolytes if ill-appearing [3]. 4. Surgical Consultation\nUrgent pediatric surgical consultation [1].', sources);
|
|
assert.match(html, /2\. Imaging/);
|
|
assert.match(html, /3\. Laboratory Studies/);
|
|
assert.match(html, /4\. Surgical Consultation/);
|
|
assert.doesNotMatch(html, /exams[^<]*2\. Imaging/);
|
|
assert.doesNotMatch(html, /obstruction[^<]*3\. Laboratory Studies/);
|
|
});
|
|
|
|
test('strips orphan trailing markdown emphasis markers', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const html = renderAssistantMarkdown('Summary Table\n\nAge Group\tEmpiric Antibiotics\n<1 month\tAmpicillin + cefotaxime\n**', sources);
|
|
assert.match(html, /Summary Table/);
|
|
assert.doesNotMatch(html, /\*\*/);
|
|
});
|
|
|
|
test('strips orphan trailing markdown marker after tab-separated text', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const markdownIt = new MarkdownIt({ html: false, linkify: true, typographer: true, breaks: true });
|
|
const input = 'Summary Table\nStep\tRecommendation\nBlood culture\tObtain before antibiotics\nHospitalization\tYes, for all febrile neonates <=28 days old\n**';
|
|
const html = renderAssistantMarkdown(input, sources, { markdownIt });
|
|
assert.doesNotMatch(html, /<table>/);
|
|
assert.match(html, /Hospitalization/);
|
|
assert.doesNotMatch(html, /\*\*/);
|
|
});
|
|
|
|
test('keeps summary paragraph outside tab-separated text', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const markdownIt = new MarkdownIt({ html: false, linkify: true, typographer: true, breaks: true });
|
|
const input = 'Summary Table\nStep/Indication\tRecommendation/Threshold\tCitation\nInitial assessment\tABCs and focused exam\t[1]\nActivated charcoal window\tWithin 1 hour\t[2]\nIn summary: rapidly assess ABCs and consider activated charcoal when appropriate [1, 2].';
|
|
const html = renderAssistantMarkdown(input, sources, { markdownIt });
|
|
assert.doesNotMatch(html, /<table>/);
|
|
assert.match(html, /In summary:/);
|
|
assert.doesNotMatch(html, /<td>In summary:/);
|
|
});
|
|
|
|
test('strips trailing marker after notes followed by tab-separated text', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const markdownIt = new MarkdownIt({ html: false, linkify: true, typographer: true, breaks: true });
|
|
const input = 'Additional Notes\nListeria coverage: Ampicillin should be added for infants <3 months [1].\nVancomycin: Added for resistant pneumococcus [2].\nAge Group\tEmpiric Antibiotics\n<1 month\tAmpicillin + Cefotaxime\n1-23 months\tVancomycin + Cefotaxime or Ceftriaxone\n>=24 months\tVancomycin + Cefotaxime or Ceftriaxone\n**';
|
|
const html = renderAssistantMarkdown(input, sources, { markdownIt });
|
|
assert.match(html, /Additional Notes/);
|
|
assert.doesNotMatch(html, /<table>/);
|
|
assert.doesNotMatch(html, /\*\*/);
|
|
});
|
|
|
|
test('links citation-only cells in markdown pipe tables', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const markdownIt = new MarkdownIt({ html: false, linkify: true, typographer: true, breaks: true });
|
|
const input = '| Step | Purpose | Source(s) |\n|---|---|---|\n| Surgical consult | Urgent assessment | [1, 2] |\n| X-ray | Assess obstruction | [3] |';
|
|
const html = renderAssistantMarkdown(input, sources, { markdownIt });
|
|
assert.match(html, /<table>/);
|
|
assert.match(html, /data-source-number="1"/);
|
|
assert.match(html, /data-source-number="2"/);
|
|
assert.match(html, /data-source-number="3"/);
|
|
assert.doesNotMatch(html, /<td>\[1, 2\]<\/td>/);
|
|
});
|
|
|
|
test('repairs bare source numbers in markdown table source columns', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const markdownIt = new MarkdownIt({ html: false, linkify: true, typographer: true, breaks: true });
|
|
const input = '| Feature | Possible Surgical Cause(s) | Source(s) |\n|---|---|---|\n| Bilious vomiting | Malrotation with volvulus | 1, 2, 3 |\n| Projectile vomiting | Pyloric stenosis | 2 |';
|
|
const html = renderAssistantMarkdown(input, sources, { markdownIt });
|
|
assert.match(html, /<table>/);
|
|
assert.match(html, /data-source-number="1"/);
|
|
assert.match(html, /data-source-number="2"/);
|
|
assert.match(html, /data-source-number="3"/);
|
|
assert.doesNotMatch(html, /<td>1, 2, 3<\/td>/);
|
|
});
|
|
|
|
test('uses markdown-it stack for GFM-style tables when provided', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const markdownIt = new MarkdownIt({ html: false, linkify: true, typographer: true, breaks: true });
|
|
const html = renderAssistantMarkdown('| Age | Antibiotics |\n|---|---|\n| <1 month | Ampicillin + cefotaxime [1] |', sources, { markdownIt });
|
|
assert.match(html, /<table>/);
|
|
assert.match(html, /<td><1 month<\/td>/);
|
|
assert.match(html, /assistant-cite/);
|
|
});
|
|
|
|
test('renders pipe summary tables and removes trailing emphasis junk', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const markdownIt = new MarkdownIt({ html: false, linkify: true, typographer: true, breaks: true });
|
|
const input = 'Summary Table\n\n| Situation | Imaging Recommended | Timing/Notes |\n|---|---|---|\n| First febrile UTI | Renal/bladder ultrasound | After recovery |\n| Routine use of DMSA scan | Not recommended | |\n**';
|
|
const html = renderAssistantMarkdown(input, sources, { markdownIt });
|
|
assert.match(html, /<table>/);
|
|
assert.match(html, /<th>Situation<\/th>/);
|
|
assert.match(html, /<td>Routine use of DMSA scan<\/td>/);
|
|
assert.doesNotMatch(html, /\*\*/);
|
|
});
|
|
|
|
test('does not repair malformed tab-separated clinical summary rows into a table', async () => {
|
|
const { renderAssistantMarkdown } = await loadCitationModule();
|
|
const markdownIt = new MarkdownIt({ html: false, linkify: true, typographer: true, breaks: true });
|
|
const input = 'Summary Table\nStep\tDetails\tSource\nRed Flags\tShock, peritonitis, distension\t3 4 7\nInitial Imaging\tAbdominal X-ray if perforation suspected, then ultrasound\n2 3 7\t\tDiagnostic Gold Std\nAbdominal ultrasound target sign\t2 3 7 Nonoperative Reduction\tAir or contrast enema if stable\nSurgical Indications\tShock, peritonitis, perforation, failed reduction\n1 3 7\t\tRecurrence Rate\n~10% after nonsurgical reduction\t5 6 7\tIf you need details on recurrence management, please specify.';
|
|
const html = renderAssistantMarkdown(input, sources, { markdownIt, citationLabel: 'number' });
|
|
assert.doesNotMatch(html, /<table>/);
|
|
assert.match(html, /Initial Imaging/);
|
|
assert.match(html, /If you need details/);
|
|
});
|
|
|
|
test('clinical assistant streams long table answers as lightweight text before final render', async () => {
|
|
const source = await fs.readFile(path.join(__dirname, '..', 'public', 'js', 'clinicalAssistant.js'), 'utf8');
|
|
assert.match(source, /STREAM_MARKDOWN_LIMIT/);
|
|
assert.match(source, /renderStreamingAnswerHtml\(partial, streamSources\)/);
|
|
assert.match(source, /assistant-streaming-text/);
|
|
assert.match(source, /pipeRows >= 8/);
|
|
});
|
|
|
|
test('numeric ranges written with spaced hyphens are not turned into bullets', async () => {
|
|
const { normalizeMarkdownText } = await loadCitationModule();
|
|
// A dose range split across a line break reads as a different dose.
|
|
for (const text of [
|
|
'Give dexamethasone 0.15 - 0.6 mg/kg orally.',
|
|
'Target SpO2 92 - 96% on room air.',
|
|
'Aim for pH 7.35 - 7.45.',
|
|
'Ages 2 - 5 years.',
|
|
'Use 5 - 10 mL/kg boluses.'
|
|
]) {
|
|
assert.equal(normalizeMarkdownText(text), text, text);
|
|
}
|
|
});
|
|
|
|
test('a hyphen after sentence punctuation still starts a list item', async () => {
|
|
const { normalizeMarkdownText } = await loadCitationModule();
|
|
assert.equal(normalizeMarkdownText('Treatment options. - Dexamethasone first.'), 'Treatment options.\n- Dexamethasone first.');
|
|
assert.equal(normalizeMarkdownText('Options: - Dexamethasone'), 'Options:\n- Dexamethasone');
|
|
assert.equal(normalizeMarkdownText('Works well [1] - Dexamethasone'), 'Works well [1]\n- Dexamethasone');
|
|
assert.equal(normalizeMarkdownText('Intro\n- one\n- two'), 'Intro\n- one\n- two');
|
|
});
|
|
|
|
test('a bold phrase at the end of an answer keeps its closing marker', async () => {
|
|
const { stripOrphanMarkdownMarkers } = await loadCitationModule();
|
|
assert.equal(stripOrphanMarkdownMarkers('Give oxygen. **Monitor closely**'), 'Give oxygen. **Monitor closely**');
|
|
assert.equal(stripOrphanMarkdownMarkers('Note the *caveat*'), 'Note the *caveat*');
|
|
// A genuinely unpaired marker is still removed.
|
|
assert.equal(stripOrphanMarkdownMarkers('Ends with bold **'), 'Ends with bold');
|
|
});
|
|
|
|
test('dollar amounts are not rendered as mathematics', async () => {
|
|
const { renderLatexText } = await loadCitationModule();
|
|
const katex = { renderToString: (expr) => '<KATEX>' + expr + '</KATEX>' };
|
|
assert.equal(renderLatexText('Costs $5 to $10 per dose', katex), 'Costs $5 to $10 per dose');
|
|
// Real mathematics still renders.
|
|
assert.equal(renderLatexText('SpO$_2$ target', katex), 'SpO<KATEX>_2</KATEX> target');
|
|
assert.equal(renderLatexText('Use $\\frac{1}{2}$ dose', katex), 'Use <KATEX>\\frac{1}{2}</KATEX> dose');
|
|
});
|
|
|
|
test('table rows are never rewritten by the bullet and heading repairs', async () => {
|
|
const { normalizeMarkdownText } = await loadCitationModule();
|
|
// A newline inserted inside a row destroys that row and every row after it.
|
|
for (const text of [
|
|
'| Dexamethasone | 0.6 mg/kg PO (max 16 mg) - single dose | 1 |',
|
|
'| Item # | Dose |',
|
|
'| Hypoxia | SpO2 <90 \\| escalate | 1 |',
|
|
'| Drug | Dose |\n|---|---|\n| Dex | 0.6 mg/kg (max 16 mg) - single dose |'
|
|
]) {
|
|
assert.equal(normalizeMarkdownText(text), text, text);
|
|
}
|
|
});
|
|
|
|
test('ranges survive in headings, after citations, and beside a hash', async () => {
|
|
const { normalizeMarkdownText } = await loadCitationModule();
|
|
for (const text of [
|
|
'### Dexamethasone 0.15 - 0.6 mg/kg PO once',
|
|
'## Target SpO2 92 - 96%',
|
|
'Magnesium 40 mg/kg IV over 20 min [1] - 50 mg/kg is an alternative.',
|
|
'Room # 4 and bed # 2'
|
|
]) {
|
|
assert.equal(normalizeMarkdownText(text), text, text);
|
|
}
|
|
});
|
|
|
|
test('the loose-formatting repairs still fire where they were meant to', async () => {
|
|
const { normalizeMarkdownText } = await loadCitationModule();
|
|
assert.equal(normalizeMarkdownText('Treatment options. - Dexamethasone first.'), 'Treatment options.\n- Dexamethasone first.');
|
|
assert.equal(normalizeMarkdownText('Options: - Dexamethasone'), 'Options:\n- Dexamethasone');
|
|
assert.equal(normalizeMarkdownText('Works well [1] - Dexamethasone helps'), 'Works well [1]\n- Dexamethasone helps');
|
|
assert.equal(normalizeMarkdownText('That is all. ## Management'), 'That is all.\n\n## Management');
|
|
assert.equal(normalizeMarkdownText('## Management - Dexamethasone first'), '## Management\n\n- Dexamethasone first');
|
|
});
|
|
|
|
test('adjacent citations merge only on the same line', async () => {
|
|
const { normalizeAdjacentCitationClusters } = await loadCitationModule();
|
|
const sources = [{ title: 'A' }, { title: 'B' }, { title: 'C' }];
|
|
assert.equal(normalizeAdjacentCitationClusters('Works [1] [2] [3].', sources), 'Works [1, 2, 3].');
|
|
// Merging across a break moved a citation onto a claim it never supported.
|
|
assert.equal(
|
|
normalizeAdjacentCitationClusters('Give ceftriaxone 50 mg/kg [1]\n\n[2] Vancomycin only if MRSA.', sources),
|
|
'Give ceftriaxone 50 mg/kg [1]\n\n[2] Vancomycin only if MRSA.'
|
|
);
|
|
});
|