fix: stop the markdown normaliser mangling doses, bold and dollar amounts
All checks were successful
Forgejo Android APK / Build signed APK (push) Successful in 1m52s
All checks were successful
Forgejo Android APK / Build signed APK (push) Successful in 1m52s
Three rendering defects in the clinical assistant, all confirmed by running the
code rather than reading it.
A spaced hyphen anywhere in a sentence was rewritten as a list item, so every
numeric range written that way was split in two:
"Give dexamethasone 0.15 - 0.6 mg/kg orally."
-> "Give dexamethasone 0.15\n- 0.6 mg/kg orally."
which renders as a truncated sentence followed by a bullet, and a dose range
therefore reads as a different dose. The same applied to SpO2 targets, pH ranges
and age ranges. A hyphen now starts a list item only at the beginning of a line
or after sentence punctuation, which still catches the case the rule was written
for.
An answer ending in a bold phrase lost its closing marker, because trailing
emphasis was stripped unconditionally and the strip ran twice. Only an unpaired
marker is removed now.
Inline maths swallowed dollar amounts: "Costs $5 to $10 per dose" rendered the
text between the signs as an equation and dropped both signs. A $...$ span is
now treated as maths only when it contains something mathematical, so subscripts
and fractions still render.
Citation handling is unchanged and covered by the same tests: clusters still
merge and sort, unknown source numbers stay literal, links still resolve by index,
and bare numbers in a table's Source column still become bracketed tokens.
The state before these fixes is tagged pre-citation-fixes-20260828.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
c88cc6a547
commit
126d7928a2
2 changed files with 56 additions and 7 deletions
|
|
@ -102,8 +102,8 @@ export function normalizeMarkdownText(text) {
|
|||
.replace(/([^\n])\s+(#{1,4}\s+)/g, '$1\n\n$2')
|
||||
.replace(/(#{1,4}\s+[^\n]+?)\s+(-\s+)/g, '$1\n\n$2')
|
||||
.replace(/(#{1,4}\s+[^\n]+)\n(-\s+)/g, '$1\n\n$2')
|
||||
.replace(/([^\n])\s+(-\s+(?:Mainstay|Medications|Hospitalization|Other therapies|Prevention|Short-acting|Anticholinergics|Systemic|Adjuncts|Long-term|Infants|Differentiating|Persistent|Severe|Need for|Inadequate)\b)/g, '$1\n$2')
|
||||
.replace(/([^\n])\s+(-\s+[^\n])/g, '$1\n$2')
|
||||
.replace(/([.!?:;)\]])\s+(-\s+(?:Mainstay|Medications|Hospitalization|Other therapies|Prevention|Short-acting|Anticholinergics|Systemic|Adjuncts|Long-term|Infants|Differentiating|Persistent|Severe|Need for|Inadequate)\b)/g, '$1\n$2')
|
||||
.replace(/([.!?:;)\]])\s+(-\s+[^\n])/g, '$1\n$2')
|
||||
.trim()));
|
||||
}
|
||||
|
||||
|
|
@ -150,10 +150,18 @@ function normalizeBareCitationCell(cell) {
|
|||
}
|
||||
|
||||
export function stripOrphanMarkdownMarkers(text) {
|
||||
return String(text || '')
|
||||
.replace(/\s*(?:\*\*|__|\*|_)\s*$/g, '')
|
||||
.replace(/\s*(?:\*\*|__)?\s*(?:Figure|Fig\.)\s*(?:\*\*|__)?\s*$/i, '')
|
||||
.trim();
|
||||
var out = String(text || '')
|
||||
.replace(/\s*(?:\*\*|__)?\s*(?:Figure|Fig\.)\s*(?:\*\*|__)?\s*$/i, '');
|
||||
// Remove a trailing marker only when it has no partner, so "…**Monitor
|
||||
// closely**" keeps its closing pair while a dangling "**" is still cleaned up.
|
||||
var trailing = out.match(/(\*\*|__|\*|_)\s*$/);
|
||||
if (trailing) {
|
||||
var marker = trailing[1];
|
||||
var body = out.slice(0, out.length - trailing[0].length);
|
||||
var occurrences = body.split(marker).length - 1;
|
||||
if (occurrences % 2 === 0) out = body;
|
||||
}
|
||||
return out.trim();
|
||||
}
|
||||
|
||||
export function fallbackMarkdown(text) {
|
||||
|
|
@ -229,7 +237,9 @@ export function renderLatexText(text, katex) {
|
|||
return String(text || '')
|
||||
.replace(/\$\$([\s\S]+?)\$\$/g, function(_, expr) { return safeKatex(katex, expr, true); })
|
||||
.replace(/\\\[([\s\S]+?)\\\]/g, function(_, expr) { return safeKatex(katex, expr, true); })
|
||||
.replace(/\$([^$\n]+?)\$/g, function(_, expr) { return safeKatex(katex, expr, false); })
|
||||
.replace(/\$([^$\n]+?)\$/g, function(match, expr) {
|
||||
return /[\\^_{}]|\\frac|\\times|\\le|\\ge/.test(expr) ? safeKatex(katex, expr, false) : match;
|
||||
})
|
||||
.replace(/\\\((.+?)\\\)/g, function(_, expr) { return safeKatex(katex, expr, false); });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -252,3 +252,42 @@ test('clinical assistant streams long table answers as lightweight text before f
|
|||
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');
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue