import { AndRule, OrRule, RepetitionRule, type Rule, type RuleTestResponse, } from '@os-team/lexical-rules'; import type { DataWithOptionsRef, Parser } from './Parser.js'; import CharDataRule from '../rules/CharDataRule.js'; import ElementParser, { type Element } from './ElementParser.js'; import createLazyParser from './createLazyParser.js'; import ReferenceRule from '../rules/ReferenceRule.js'; import CDSectParser from './CDSectParser.js'; import PIParser, { type PI } from './PIParser.js'; import CommentRule from '../rules/CommentRule.js'; export type Primitive = string | number | boolean | null | undefined; export type ContentWithElement = Element & { '#content'?: Primitive; }; export type Content = Primitive | ContentWithElement; const escapeValue = (value: string) => value.replace( /[<>]/g, (c) => ({ '<': '<', '>': '>', })[c] || c ); const parseValue = (value: string): Exclude => { if (/^[0-9]+(\.[0-9]+)?$/.test(value)) return Number(value); if (['true', 'false'].includes(value)) return value === 'true'; if (value === 'null') return null; if (value === 'undefined') return ''; return value; }; const buildValue = (value: Primitive): string => { if (typeof value === 'string') return escapeValue(value); if (typeof value === 'number') return value.toString(); if (typeof value === 'boolean') return value.toString(); if (value === null) return 'null'; return ''; }; /** * The content of an element. * See https://www.w3.org/TR/xml/#NT-content */ class ContentParser implements Parser { private readonly elementParser: ElementParser; private readonly cdSectParser: CDSectParser; private readonly piParser: PIParser; private readonly rule: Rule< [string, Array<[Element | string | PI | undefined, string]>] >; public constructor() { const charDataRule = new CharDataRule(); // CharData? this.elementParser = createLazyParser( ElementParser ); const referenceRule = new ReferenceRule(); // Reference this.cdSectParser = new CDSectParser(); // CDSect this.piParser = new PIParser(); // PI const commentRule = new CommentRule(); // Comment const entityRule = new OrRule([ this.elementParser, referenceRule, this.cdSectParser, this.piParser, commentRule, ]); // element | Reference | CDSect | PI | Comment const entityAndCharDataRule = new AndRule([entityRule, charDataRule]); // (element | Reference | CDSect | PI | Comment) CharData? const anyEntityAndCharDataRule = new RepetitionRule( entityAndCharDataRule, 0 ); // ((element | Reference | CDSect | PI | Comment) CharData?)* this.rule = new AndRule([charDataRule, anyEntityAndCharDataRule]); // CharData? ((element | Reference | CDSect | PI | Comment) CharData?)* } public test(ref: DataWithOptionsRef, pos: number): RuleTestResponse { const { isArray = () => false, include = '' } = ref.options || {}; const isArrayFn = Array.isArray(isArray) ? (name) => isArray.includes(name) : isArray; const [isValid, nextPos, res] = this.rule.test(ref, pos); if (!isValid || res === undefined) return [false, nextPos]; const [firstCharData, moreItems] = res; let element: Element = {}; let strContent = firstCharData; moreItems.forEach(([entity, charData]) => { if (typeof entity === 'string') { strContent = `${strContent}${entity}`; } else if (Array.isArray(entity)) { if (['PROLOG', 'ALL'].includes(include)) { const [, target, content] = entity; element = { ...element, [`?${target}`]: content || '' }; } } else if (entity !== undefined) { const [key, value] = Object.entries(entity)[0]; const elementValue = element[key]; if (Array.isArray(elementValue)) { if (Array.isArray(value)) elementValue.push(...value); else elementValue.push(value); } else if (elementValue !== undefined) { if (Array.isArray(value)) element = { ...element, [key]: [elementValue, ...value] }; else element = { ...element, [key]: [elementValue, value] }; } else if (isArrayFn(key) && !Array.isArray(value)) { element = { ...element, [key]: [value] }; } else { element = { ...element, [key]: value }; } } strContent = `${strContent}${charData}`; }); let content: Content; if (Object.keys(element).length > 0) { content = element; if (strContent) content['#content'] = parseValue(strContent); } else { content = parseValue(strContent); } return [true, nextPos, content]; } public build(data: Content) { if (typeof data !== 'object' || data === null) return buildValue(data); const { '#content': content, ...element } = data; const strContent = buildValue(content); const strElement = Object.entries(element).reduce((acc, [key, value]) => { let item: string; if (key[0] === '?' && typeof value === 'string') { item = this.piParser.build(['PI', key.slice(1), value]); } else if (Array.isArray(value)) { item = value.reduce( (itemAcc, itemValue) => `${itemAcc}${this.elementParser.build({ [key]: itemValue })}`, '' ); } else { item = this.elementParser.build({ [key]: value }); } return `${acc}${item}`; }, ''); return `${strContent}${strElement}`; } } export default ContentParser;