diff --git a/inline_parser_test.ts b/inline_parser_test.ts index 6d18264..dc6d003 100644 --- a/inline_parser_test.ts +++ b/inline_parser_test.ts @@ -19,8 +19,8 @@ import { import { tokenize } from './tokenizer.ts'; import { blockEvents } from './block_parser.ts'; import { inlineEvents } from './inline_parser.ts'; -import { textEvent } from './events.ts'; -import type { EnterEvent, ExitEvent, WikitextEvent } from './events.ts'; +import { DiagnosticCode, textEvent } from './events.ts'; +import type { EnterEvent, ErrorEvent, ExitEvent, WikitextEvent } from './events.ts'; function parse(input: string): WikitextEvent[] { return [...inlineEvents(input, blockEvents(input, tokenize(input)))]; @@ -57,6 +57,13 @@ function textValues(events: WikitextEvent[], source: string): string[] { }); } +/** Collect machine-readable codes from recovery events. */ +function errorCodes(events: WikitextEvent[]): string[] { + return events + .filter((event): event is ErrorEvent => event.kind === 'error') + .map((event) => event.code ?? ''); +} + describe('inlineEvents — text-group boundaries', () => { it('merges contiguous neighboring text events into one scan group', () => { const source = 'Hello [[Mars|planet]] world'; @@ -389,6 +396,48 @@ describe('inlineEvents — templates and special inline nodes', () => { }); }); + it('keeps malformed-but-closed tag openers structurally real once `>` is reached', () => { + const input = `>hi`; + const events = parse(input); + const tag = expectEnter(events, 'html-tag'); + + expect(tag.props).toEqual({ + tag_name: 'span', + self_closing: false, + attributes: { foo: '', div: '' }, + }); + expect(textValues(events, input)).toContain('hi'); + }); + + it('recovers a missing close tag after a complete reference opener', () => { + const input = `''quoted''`; + const events = parse(input); + const reference = expectEnter(events, 'reference'); + + expect(reference.props).toEqual({ name: 'cite-1' }); + expect(structure(events)).toContainEqual(['enter', 'italic']); + expect(errorCodes(events)).toContain(DiagnosticCode.INLINE_TAG_MISSING_CLOSE); + }); + + it('recovers a missing close tag after a complete nowiki opener', () => { + const input = `[[literal]]`; + const events = parse(input); + const nowiki = expectEnter(events, 'nowiki'); + + expect(nowiki.props).toEqual({ value: '[[literal]]' }); + expect(firstEnter(events, 'wikilink')).toBeUndefined(); + expect(errorCodes(events)).toContain(DiagnosticCode.INLINE_TAG_MISSING_CLOSE); + }); + + it('preserves an unterminated opener as text when `>` never appears', () => { + const input = ` { const input = `[[File:Example.jpg|thumb|{{Card|name=[[Main Page|home]]}} ''quoted'' &]]`; const events = parse(input); @@ -599,7 +648,7 @@ describe('inlineEvents — invariants', () => { it('keeps enter and exit events balanced on pathological mixed input', () => { fc.assert( fc.property(pathological_wikitext_string(), (input) => { - // Inline recovery can choose many conservative outputs, but it must not + // Inline recovery can choose many strict outputs, but it must not // produce broken nesting. Balance is the core contract that downstream // consumers rely on. const events = parse(input);