diff --git a/public/js/assistant/citations.js b/public/js/assistant/citations.js
index 117ec3d..5502457 100644
--- a/public/js/assistant/citations.js
+++ b/public/js/assistant/citations.js
@@ -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); });
}
diff --git a/test/assistant-citations.test.js b/test/assistant-citations.test.js
index bb3d943..989e307 100644
--- a/test/assistant-citations.test.js
+++ b/test/assistant-citations.test.js
@@ -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) => '' + expr + '' };
+ 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_2 target');
+ assert.equal(renderLatexText('Use $\\frac{1}{2}$ dose', katex), 'Use \\frac{1}{2} dose');
+});