import { describe, it, expect } from 'vitest';
import { renderContent, cleanContent } from '../utils/content-renderer';
describe('renderContent', () => {
it('passes through plain text unchanged', () => {
expect(renderContent('hello world')).toBe('hello world');
});
it('passes through pre-rendered MathML unchanged', () => {
const preRendered = 'value here';
expect(renderContent(preRendered)).toBe(preRendered);
});
it('still converts italic in mixed pre-rendered MathML content', () => {
const preRendered = ' and *italic*';
expect(renderContent(preRendered)).toBe(
' and italic',
);
});
it('converts *text* to (italic) for non-pre-rendered content', () => {
expect(renderContent('some *italic* text')).toBe('some italic text');
});
it('converts ~text~ to (subscript)', () => {
expect(renderContent('H~2~O')).toBe('H2O');
});
it('converts bullet lines to - ', () => {
const result = renderContent('* first item\n\n* second item');
expect(result).toContain('
');
expect(result).toContain('- first item
');
expect(result).toContain('- second item
');
});
it('converts AsciiDoc pipe-delimited tables to ', () => {
const input = 'Intro text\n\n|===\n| a | b | c\n| d | e | f\n|===';
const result = renderContent(input);
expect(result).toContain('');
expect(result).toContain('| a | b | c |
');
expect(result).toContain('| d | e | f |
');
expect(result).not.toContain('|===');
});
it('strips AsciiDoc table attribute lines (cols, options)', () => {
const input = 'Intro\n\n[cols="3", options="noheader,unnumbered"]\n|===\n| a | b\n|===';
const result = renderContent(input);
expect(result).not.toContain('[cols=');
expect(result).not.toContain('noheader');
expect(result).toContain('');
});
it('strips cell-span modifiers (.2+, 2+) from cell content', () => {
const input = '|===\n.2+| cell-a | cell-b\n| cell-c\n|===';
const result = renderContent(input);
expect(result).not.toMatch(/\b\.2\+/);
expect(result).toContain('cell-a');
expect(result).toContain('cell-b');
});
it('joins + continuation lines into the same cell with
', () => {
const input = '|===\n| english line +\nfrench line | cell-b\n|===';
const result = renderContent(input);
expect(result).toContain('english line
french line');
expect(result).toContain('cell-b | ');
expect(result).not.toMatch(/\s\+\s*\|/);
});
it('groups cells of the same row when continuation is used', () => {
// VIM-style bilingual layout: each `|` after a `+` is a new cell in
// the SAME row, not a new row.
const input = '|===\n.2+| length +\nlongueur | radius +\nrayon\n|===';
const result = renderContent(input);
expect(result).toContain('length');
expect(result).toContain('longueur');
expect(result).toContain('radius');
expect(result).toContain('rayon');
expect(result).toContain('length
longueur');
expect(result).toContain('radius
rayon');
});
it('preserves rendered math spans inside table cells (no double-escaping)', () => {
const input = '|===\n| length, stem:[l] | radius, stem:[r]\n|===';
const result = renderContent(input);
expect(result).toContain('data-expr="l"');
expect(result).toContain('data-expr="r"');
expect(result).not.toContain('<span');
});
it('resolves URN inline refs via xrefResolver', () => {
const resolver = (uri: string, term: string) => `[${term}→${uri}]`;
const result = renderContent(
'a {{urn:iso:std:iso:14812:3.1.1.1,entity}} reference',
resolver,
);
expect(result).toBe('a [entity→urn:iso:std:iso:14812:3.1.1.1] reference');
});
it('resolves single-braced URN inline refs', () => {
const resolver = (uri: string, term: string) => `[${term}→${uri}]`;
const result = renderContent(
'a {urn:iso:std:iso:14812:3.1.1.1,entity} reference',
resolver,
);
expect(result).toBe('a [entity→urn:iso:std:iso:14812:3.1.1.1] reference');
});
it('shows term without resolver', () => {
const result = renderContent('a {{urn:iso:std:iso:14812:3.1.1.1,entity}} ref');
expect(result).toBe('a entity ref');
});
it('uses display text from three-part URN refs', () => {
const result = renderContent('a {{urn:iso:std:iso:14812:3.1.1.6,person,Person}} ref');
expect(result).toBe('a Person ref');
});
it('resolves three-part URN refs with display text via xrefResolver', () => {
const resolver = (uri: string, term: string) => `[${term}→${uri}]`;
const result = renderContent(
'{{urn:iso:std:iso:14812:3.1.1.6,person,Person}}, object, event',
resolver,
);
expect(result).toBe('[Person→urn:iso:std:iso:14812:3.1.1.6], object, event');
});
it('resolves single-braced three-part URN refs', () => {
const resolver = (uri: string, term: string) => `[${term}→${uri}]`;
const result = renderContent(
'{urn:iso:std:iso:14812:3.5.3.4,user,users} are people',
resolver,
);
expect(result).toBe('[users→urn:iso:std:iso:14812:3.5.3.4] are people');
});
it('displays render term (second part) for unresolved two-arg mentions', () => {
const result = renderContent('see {{some term, unknown ref}}');
// Per spec: {{identifier,render term}} — identifier first, render term last
// Without a resolver, the render term is what should display
expect(result).toBe('see unknown ref');
});
it('resolves cross-refs even in pre-rendered content', () => {
const resolver = (uri: string, term: string) => `[${term}→${uri}]`;
const preRendered = ' and {{urn:iso:std:iso:14812:3.1.1.1,entity}}';
expect(renderContent(preRendered, resolver)).toBe(
' and [entity→urn:iso:std:iso:14812:3.1.1.1]',
);
});
it('handles empty input', () => {
expect(renderContent('')).toBe('');
});
it('handles null-ish input', () => {
expect(renderContent(null as any)).toBe('');
expect(renderContent(undefined as any)).toBe('');
});
// stem:[...] placeholder tests
it('outputs math-pending placeholder for stem:[expr]', () => {
const result = renderContent('value stem:[x^2] here');
expect(result).toContain('class="math-pending"');
expect(result).toContain('data-expr="x^2"');
expect(result).toContain('data-format="asciimath"');
expect(result).toContain('x^2');
});
it('outputs math-pending with math-bold for *stem:[expr]', () => {
const result = renderContent('*stem:[alpha]');
expect(result).toContain('class="math-pending math-bold"');
expect(result).toContain('data-expr="alpha"');
});
it('outputs math-pending placeholder for latexmath:[expr]', () => {
const result = renderContent('equation latexmath:[\\frac{a}{b}] end');
expect(result).toContain('class="math-pending"');
expect(result).toContain('data-expr="\\frac{a}{b}"');
expect(result).toContain('data-format="latex"');
});
it('handles multiple stem: expressions in one string', () => {
const result = renderContent('stem:[m] out of stem:[n] redundancy');
const matches = result.match(/class="math-pending"/g);
expect(matches).toHaveLength(2);
expect(result).toContain('data-expr="m"');
expect(result).toContain('data-expr="n"');
});
it('handles nested brackets in stem:', () => {
const result = renderContent('stem:[a_[i]]');
expect(result).toContain('data-expr="a_[i]"');
});
it('handles stem: in designation text', () => {
const result = renderContent('stem:[n]-ary digit');
expect(result).toContain('data-expr="n"');
expect(result).toContain('-ary digit');
});
it('escapes special HTML in stem: expressions', () => {
const result = renderContent('stem:[a', () => {
expect(renderContent('some **bold** text')).toBe('some bold text');
});
it('distinguishes **bold** from *italic*', () => {
expect(renderContent('**bold** and *italic*')).toBe('bold and italic');
});
// cite:key rendering
it('renders {{cite:key}} as bib-ref span', () => {
const result = renderContent('see {{cite:iso-10303-2}} for details');
expect(result).toBe('see iso-10303-2 for details');
});
it('renders {{cite:key,label}} using label', () => {
const result = renderContent('see {{cite:vim-2.2,entity data type}} for details');
expect(result).toBe('see entity data type for details');
});
it('invokes citeResolver for cite:key mentions', () => {
const resolver = (key: string, label: string | null) => `[CITE:${key}:${label}]`;
const result = renderContent('{{cite:foo,bar}}', { citeResolver: resolver });
expect(result).toBe('[CITE:foo:bar]');
});
it('invokes citeResolver for cite:key without label', () => {
const resolver = (key: string, label: string | null) => `[CITE:${key}]`;
const result = renderContent('{{cite:foo}}', { citeResolver: resolver });
expect(result).toBe('[CITE:foo]');
});
it('escapes HTML in cite:key fallback rendering', () => {
const result = renderContent('{{cite:foo}}');
expect(result).toContain('foo<b>');
expect(result).not.toContain('');
});
});
describe('cleanContent', () => {
it('strips pre-rendered HTML/MathML tags', () => {
expect(cleanContent('value here'))
.toBe('value x here');
});
it('strips *text* to plain text', () => {
expect(cleanContent('some *italic* text')).toBe('some italic text');
});
it('strips **text** to plain text', () => {
expect(cleanContent('some **bold** text')).toBe('some bold text');
});
it('strips {{cite:key}} completely', () => {
expect(cleanContent('see {{cite:foo}} here')).toBe('see here');
});
it('strips {{cite:key,label}} to label', () => {
expect(cleanContent('see {{cite:foo,entity data type}} here')).toBe('see entity data type here');
});
it('converts ~text~ to _text', () => {
expect(cleanContent('H~2~O')).toBe('H_2O');
});
it('strips list markers', () => {
const result = cleanContent('items:\n* one\n* two');
expect(result).toContain('one');
expect(result).toContain('two');
expect(result).not.toContain('* ');
});
it('strips URN refs to just the term', () => {
expect(cleanContent('a {{urn:iso:std:iso:14812:3.1.1.1,entity}} ref')).toBe('a entity ref');
});
it('strips three-part URN refs to the linked term (not display text)', () => {
expect(cleanContent('{{urn:iso:std:iso:14812:3.1.1.6,person,Person}}, object')).toBe('person, object');
});
it('handles empty input', () => {
expect(cleanContent('')).toBe('');
expect(cleanContent(null as any)).toBe('');
});
it('strips stem: notation to plain expression', () => {
expect(cleanContent('stem:[x^2]')).toBe('x^2');
});
it('strips *stem: bold notation', () => {
expect(cleanContent('*stem:[alpha]')).toBe('alpha');
});
it('strips latexmath: notation', () => {
expect(cleanContent('latexmath:[\\frac{a}{b}]')).toBe('\\frac{a}{b}');
});
it('strips stem: in running text', () => {
expect(cleanContent('value stem:[m] out of stem:[n]')).toBe('value m out of n');
});
});
describe('renderContent — cite-ref edge cases', () => {
it('renders cite with special characters in key', () => {
const result = renderContent('{{cite:iso-10303-2-ed1}}');
expect(result).toBe('iso-10303-2-ed1');
});
it('renders cite with numeric-only key', () => {
const result = renderContent('{{cite:42}}');
expect(result).toBe('42');
});
it('falls back to key when no label in citeResolver', () => {
const result = renderContent('{{cite:foo-bar}}');
expect(result).toBe('foo-bar');
});
});
describe('cleanContent — cite-ref edge cases', () => {
it('strips cite:key with special characters', () => {
expect(cleanContent('see {{cite:iso-10303-2-ed1}}')).toBe('see ');
});
it('preserves label with comma in cite:key,label', () => {
expect(cleanContent('see {{cite:foo,entity data type}}')).toBe('see entity data type');
});
it('handles multiple cite refs in sequence', () => {
expect(cleanContent('{{cite:a}} and {{cite:b,Label B}}'))
.toBe(' and Label B');
});
});