@endevops/effect-codec-xml 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/LICENSE-is-entities +21 -0
- package/LICENSE-is-xml-naming +21 -0
- package/README.md +415 -0
- package/dist/codec.d.ts +48 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +63 -0
- package/dist/codec.js.map +1 -0
- package/dist/conventions.d.ts +88 -0
- package/dist/conventions.d.ts.map +1 -0
- package/dist/conventions.js +113 -0
- package/dist/conventions.js.map +1 -0
- package/dist/entities/entity-decoder.d.ts +333 -0
- package/dist/entities/entity-decoder.d.ts.map +1 -0
- package/dist/entities/entity-decoder.js +841 -0
- package/dist/entities/entity-decoder.js.map +1 -0
- package/dist/entities/entity-tables.js +16 -0
- package/dist/entities/entity-tables.js.map +1 -0
- package/dist/errors.d.ts +49 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +48 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/namespaces.d.ts +101 -0
- package/dist/namespaces.d.ts.map +1 -0
- package/dist/namespaces.js +663 -0
- package/dist/namespaces.js.map +1 -0
- package/dist/naming.d.ts +149 -0
- package/dist/naming.d.ts.map +1 -0
- package/dist/naming.js +296 -0
- package/dist/naming.js.map +1 -0
- package/dist/parse.d.ts +75 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +437 -0
- package/dist/parse.js.map +1 -0
- package/dist/render.d.ts +99 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +509 -0
- package/dist/render.js.map +1 -0
- package/dist/xml-error.d.ts +172 -0
- package/dist/xml-error.d.ts.map +1 -0
- package/dist/xml-error.js +157 -0
- package/dist/xml-error.js.map +1 -0
- package/dist/xml-value.d.ts +42 -0
- package/dist/xml-value.d.ts.map +1 -0
- package/dist/xml-value.js +79 -0
- package/dist/xml-value.js.map +1 -0
- package/package.json +69 -0
- package/src/codec.ts +136 -0
- package/src/conventions.ts +145 -0
- package/src/entities/entity-decoder.ts +1248 -0
- package/src/entities/entity-tables.ts +18 -0
- package/src/errors.ts +55 -0
- package/src/index.ts +79 -0
- package/src/namespaces.ts +968 -0
- package/src/naming.ts +519 -0
- package/src/parse.ts +597 -0
- package/src/render.ts +708 -0
- package/src/xml-error.ts +168 -0
- package/src/xml-value.ts +108 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.js","names":[],"sources":["../src/render.ts"],"sourcesContent":["// Rendering: an `XmlValue` to XML text.\n//\n// This is the hot path for an application that serializes often, so it is built\n// around three things: one pass over each record's keys rather than one per\n// role a key can play, one pass over each character rather than one per\n// character class, and one array of chunks joined once rather than a growing\n// string.\n//\n// The walk itself is plain synchronous functions rather than a chain of\n// `yield*`es. Publicly `renderXml` is still an `Effect` — it suspends the walk so\n// it runs lazily, and folds the one failure the walk can report into the typed\n// error channel — but inside a document there is no effect boundary per element\n// or per attribute. A 500-row report is thousands of elements, and a fiber step\n// for each of them was most of what the `render 500 rows` row measured. The\n// typed failure survives: the walk throws an {@link XmlRenderError} and\n// `renderXml` catches it into `Effect.fail`.\n//\n// Escaping is the part that scales with the size of the document rather than\n// with its structure, and it is written out here rather than delegated, for a\n// measured reason. The entity encoder that used to live beside this package\n// escaped by applying five sequential global replacements, one per character,\n// so a document with a single `&` in twenty thousand characters was scanned\n// five times over to change one byte -- which is what the `render 20k` rows in\n// `bench/codec.bench.ts` measure. The table below covers the same five\n// characters that encoder escaped, and the explicit expectations in\n// `test/render.spec.ts` pin the fast path.\n\nimport { Effect, Predicate, Result } from 'effect';\n\nimport type { NameMode } from './conventions.ts';\nimport type { XmlVersion } from './naming.ts';\nimport type { XmlRecord, XmlValue } from './xml-value.ts';\n\nimport { attributeName, DEFAULT_ITEM_NAME, DEFAULT_ROOT_NAME, isAttributeKey, isTextKey, resolveNameSync, TEXT_KEY } from './conventions.ts';\nimport { XmlRenderError } from './errors.ts';\n\n/**\n * @description The five characters XML predefines an entity for, and the names to write for them. Written out rather than referenced from the entity decoder\n * because the table is indexed by character code below.\n */\nconst XML_PREDEFINED = { 34: '"', 38: '&', 39: ''', 60: '<', 62: '>' } as const;\n\n/**\n * @description The character references for the whitespace XML normalizes inside an attribute value. A parser replaces a literal newline, carriage return or tab\n * in an attribute with a space, so a value that has to survive a round trip has to spell them as references. They are in the attribute table and not\n * the text one: in character data they are content, and only an attribute value is normalized.\n */\nconst ATTRIBUTE_WHITESPACE = { 9: '	', 10: ' ', 13: ' ' } as const;\n\n/**\n * @description The replacement for each ASCII character that needs one, and `undefined` for the ones that do not. Indexed by character code and 128 long, so the\n * check is one comparison and one array read with no string search in it.\n *\n * @param extra - Characters to escape in addition to the five predefines.\n *\n * @returns The lookup table.\n */\nconst buildTable = (extra: Record<number, string>): ReadonlyArray<string | undefined> => {\n const table = Array.from<string | undefined>({ length: 128 }).fill(undefined);\n for (const [code, entity] of Object.entries({ ...XML_PREDEFINED, ...extra })) table[Number(code)] = entity;\n return table;\n};\n\nconst TEXT_TABLE = buildTable({});\nconst ATTRIBUTE_TABLE = buildTable(ATTRIBUTE_WHITESPACE);\n\n/**\n * @description The characters each table escapes, as a pattern rather than as a set of replacement passes. Finding the first one with a pattern is what makes\n * clean text cheap: V8 compiles a single character class into a scan that is several times faster than a JavaScript loop reading the same string a\n * code unit at a time, and clean text is most text. `render 20k of clean text` in `bench/codec.bench.ts` is the row that says so — a hand-written\n * loop over the same twenty thousand characters is roughly two and a half times slower. Neither pattern is global, so `exec` ignores `lastIndex` and\n * always starts at the beginning. One module-level instance of each is therefore safe to reuse, and nothing has to be reset between calls.\n */\nconst TEXT_UNSAFE = /[<>&\"']/;\nconst ATTRIBUTE_UNSAFE = /[<>&\"'\\n\\r\\t]/;\n\n/**\n * @description Options for {@link renderXml}.\n */\nexport interface XmlRenderOptions {\n /**\n * @description Name of the root element.\n *\n * @default 'root'\\\n * A codec passes the name it took from the schema's `identifier` annotation when the caller did not set one.\n */\n readonly rootName?: string | undefined;\n\n /**\n * @description Element name used for the members of a document whose root value is an array.\n *\n * @default 'item'\n */\n readonly itemName?: string | undefined;\n\n /**\n * @description Indent nested elements on their own lines.\n *\n * @default false\n */\n readonly format?: boolean | undefined;\n\n /**\n * @description The string one indent level is made of. Defaults to two spaces.\n */\n readonly indent?: string | undefined;\n\n /**\n * @description Write an element with no attributes, text or children as `<a/>` rather than `<a></a>`.\n *\n * @default true\n */\n readonly suppressEmptyNode?: boolean | undefined;\n\n /**\n * @description Sort an element's keys so the same value always renders to the same bytes.\n *\n * @default false\\\n * Which keeps declaration order.\\ Worth turning on for snapshot tests, where key order is otherwise the only thing that can make two equal values differ.\n */\n readonly sortKeys?: boolean | undefined;\n\n /**\n * @description What to do with a field name that is not a legal XML name.\n *\n * @default 'repair'.\n */\n readonly name?: NameMode | undefined;\n\n /**\n * @description XML version to validate names against.\n *\n * @default '1.0'\n */\n readonly xmlVersion?: XmlVersion | undefined;\n\n /**\n * @description How deep to nest before giving up. Guards against a value that nests without end taking the stack with it.\n *\n * @default 256\n */\n readonly maxDepth?: number | undefined;\n}\n\n/**\n * @description Options every render call needs, with the defaults already applied.\n */\ninterface ResolvedOptions {\n readonly rootName: string;\n readonly itemName: string;\n readonly format: boolean;\n readonly indent: string;\n readonly suppressEmptyNode: boolean;\n readonly sortKeys: boolean;\n readonly name: NameMode;\n readonly xmlVersion: XmlVersion;\n readonly maxDepth: number;\n\n /**\n * @description Resolves a field name to a legal XML name, in the mode this render was configured with. Synchronous and memoized; see {@link makeNamer}.\n */\n readonly namer: (name: string) => string;\n\n /**\n * @description The indent for a given depth, when pretty-printing. Built on first use at each depth and kept, so an indented document builds one string per\n * level rather than one per line.\n */\n readonly lineAt: (depth: number) => string;\n}\n\n/**\n * @description Builds the name resolver for one render. Every element and every attribute name goes through here, and a document repeats names: a thousand\n * `<item>` elements, or the same `id` on every row. A validator that runs a regex per occurrence pays that cost a thousand times for one answer, so\n * the first result is remembered and the rest are lookups. It also keeps the mode and version in one place, which is what stops a caller from\n * resolving a name with different settings than the render it is part of. A cache miss calls {@link resolveNameSync}, which throws an\n * {@link XmlParseError} in `'error'` mode; {@link renderXml} catches it and reports it as an {@link XmlRenderError}.\n *\n * @param options - Resolved render options.\n *\n * @returns A function from field name to the legal XML name.\n */\nconst makeNamer = (options: Omit<ResolvedOptions, 'namer' | 'lineAt'>): ((name: string) => string) => {\n const cache = new Map<string, string>();\n return (name: string): string => {\n const hit = cache.get(name);\n if (hit !== undefined) return hit;\n const resolved = resolveNameSync(name, { mode: options.name, xmlVersion: options.xmlVersion });\n cache.set(name, resolved);\n return resolved;\n };\n};\n\n/**\n * @description A boolean option's value, with an absent one read as the default. The three boolean options are spelled through here rather than through a `??` of\n * their own, so the table below reads as a list of what each option _is_ instead of a list of nine separate decisions about what an omitted option\n * means — and so a reader looking for \"which options are on by default\" finds three words rather than three mixes of `?? true` and `?? false` to\n * read.\n *\n * @param value - The option as the caller wrote it, or `undefined` when the caller left it out.\n * @param fallback - The value to use when the caller left it out.\n *\n * @returns The option's value.\n */\nconst flag = (value: boolean | undefined, fallback: boolean): boolean => value ?? fallback;\n\n/**\n * @description The indent for a given depth, built the first time a render reaches that depth and kept. A document of a few thousand elements on several lines\n * each would otherwise call `repeat` once per line and allocate the same handful of strings thousands of times over.\n *\n * @param indent - The string one level of indentation is made of.\n *\n * @returns A function from depth to the indent for that depth.\n */\nconst makeLineAt = (indent: string): ((depth: number) => string) => {\n const lines: Array<string> = [''];\n return depth => {\n const line = lines[depth];\n if (line !== undefined) return line;\n const built = indent.repeat(depth);\n lines[depth] = built;\n return built;\n };\n};\n\n/**\n * @description Applies the defaults to one call's options, and builds the two things a render needs that are not options: the memoized name resolver and the\n * memoized indent lines.\n *\n * @param options - The options as the caller wrote them.\n *\n * @returns Every option a render reads, defaulted, with the resolver and the indent lines attached.\n */\nconst resolveOptions = (options: XmlRenderOptions): ResolvedOptions => {\n const resolved = {\n rootName: options.rootName ?? DEFAULT_ROOT_NAME,\n itemName: options.itemName ?? DEFAULT_ITEM_NAME,\n format: flag(options.format, false),\n indent: options.indent ?? ' ',\n suppressEmptyNode: flag(options.suppressEmptyNode, true),\n sortKeys: flag(options.sortKeys, false),\n name: options.name ?? ('repair' as NameMode),\n xmlVersion: options.xmlVersion ?? ('1.0' as XmlVersion),\n maxDepth: options.maxDepth ?? 256,\n };\n return { ...resolved, namer: makeNamer(resolved), lineAt: makeLineAt(resolved.indent) };\n};\n\n/**\n * @description Escapes a value for use as character data.\n *\n * @param value - The text to escape.\n *\n * @returns The text with the XML-unsafe characters replaced by predefined entities, or the very same string when there is nothing to escape.\n */\nexport const escapeText = (value: string): string => escape(value, TEXT_UNSAFE, TEXT_TABLE);\n\n/**\n * @description Escapes a value for use inside a double-quoted attribute.\n *\n * @param value - The text to escape.\n *\n * @returns The escaped text, with the whitespace that XML would otherwise normalize spelled as character references.\n */\nexport const escapeAttribute = (value: string): string => escape(value, ATTRIBUTE_UNSAFE, ATTRIBUTE_TABLE);\n\n/**\n * @description Replaces every character the table has an entry for, in one pass over the string. The pattern finds the first character that needs replacing, and a\n * string with none is handed straight back — which is the common case, and the one the pattern is there to make fast. From there the rest of the\n * string is copied in runs between the replacements rather than a character at a time, so the cost is one pattern scan, one copy, and one\n * concatenation per replacement, rather than a whole pass per character class. Only ASCII is looked up. XML carries every other character natively,\n * and a code unit above 127 has no entity an XML parser is required to know.\n *\n * @param value - The text to escape.\n * @param pattern - Matches the first character that needs replacing.\n * @param table - The replacement for each ASCII character that needs one.\n *\n * @returns The escaped text, or `value` itself when there is nothing to escape.\n */\nconst escape = (value: string, pattern: RegExp, table: ReadonlyArray<string | undefined>): string => {\n const found = pattern.exec(value);\n if (found === null) return value; // nothing to escape: hand back the same string\n\n const length = value.length;\n const start = found.index;\n let out = value.slice(0, start);\n let copied = start;\n\n for (let index = start; index < length; index++) {\n const code = value.charCodeAt(index);\n const entity = code < 128 ? table[code] : undefined;\n if (entity !== undefined) {\n out += value.slice(copied, index) + entity;\n copied = index + 1;\n }\n }\n\n return copied === length ? out : out + value.slice(copied);\n};\n\n/**\n * @description Renders an {@link XmlValue} as an XML document.\\\n * A record becomes an element:\n *\n * - `@`-prefixed keys become attributes, the reserved `#text` key becomes character data, and every other key becomes a child element.\n * - An array repeats its name — a document whose root value is an array wraps it in the root element and names each member `itemName`.\n * - A string is character data. The walk is synchronous, and what can go wrong is reported by throwing an {@link XmlRenderError}; {@link renderXml}\n * folds that into the effect's typed error channel. A caller not already in an `Effect` runs it with `Effect.runSync`, which throws the failure it\n * produced.\n *\n * @param value - The value to render.\n * @param options - Root name, formatting, empty-element and name-resolution settings.\n *\n * @returns An effect producing the XML document as a string.\n */\nexport const renderXml = (value: XmlValue, options: XmlRenderOptions = {}): Effect.Effect<string, XmlRenderError> =>\n Effect.suspend(() => Effect.fromResult(renderResult(value, options)));\n\n/**\n * @description Runs the synchronous walk and folds the one failure it reports into a {@link Result}, which {@link renderXml} turns back into an `Effect`. Kept\n * separate so the walk itself can throw without the public API ever throwing.\n *\n * @param value - The value to render.\n * @param options - The options as the caller wrote them.\n *\n * @returns The document, or the failure to report.\n */\nconst renderResult = (value: XmlValue, options: XmlRenderOptions): Result.Result<string, XmlRenderError> => {\n try {\n return Result.succeed(render(value, options));\n } catch (cause) {\n return Result.fail(toRenderError(cause));\n }\n};\n\n/**\n * @description Reports a failure the synchronous walk threw in the render's own error type. The walk only throws an {@link XmlRenderError} of its own or an\n * {@link XmlParseError} from the name resolver; the latter carries the message the spec asserts on, so it is carried across rather than replaced.\n *\n * @param cause - Whatever was thrown.\n *\n * @returns The failure to report.\n */\nconst toRenderError = (cause: unknown): XmlRenderError => {\n if (cause instanceof XmlRenderError) return cause;\n if (Predicate.isError(cause)) return new XmlRenderError({ message: cause.message });\n return new XmlRenderError({ message: String(cause) });\n};\n\n/**\n * @description The synchronous walk behind {@link renderXml}.\n *\n * @param value - The value to render.\n * @param options - The options as the caller wrote them.\n *\n * @returns The XML document as a string.\n *\n * @throws {XmlRenderError} When the value nests past `maxDepth`, or the name resolver refuses a field name.\n */\nconst render = (value: XmlValue, options: XmlRenderOptions): string => {\n const resolved = resolveOptions(options);\n const out: Array<string> = [];\n\n // A document has exactly one root element, so a root value that is an array\n // is wrapped rather than emitted as several roots. Inside a named element an\n // array repeats that element's own name, so this wrapping is the only place\n // `itemName` is ever used.\n if (Array.isArray(value)) {\n const tag = resolved.namer(resolved.rootName);\n out.push('<', tag, '>');\n for (const member of value) renderElement(out, resolved.itemName, member, 1, resolved);\n if (resolved.format) out.push('\\n');\n out.push('</', tag, '>');\n } else {\n renderElement(out, resolved.rootName, value, 0, resolved);\n }\n\n if (resolved.format) out.push('\\n');\n return out.join('');\n};\n\n/**\n * @description Renders one named element and its subtree. The value an {@link XmlValue} holds decides which of the four shapes below it takes — a repeated run of\n * children, character data, an absent field, or a record — and each of those is written by a function of its own, so this one is the dispatch rather\n * than the document.\n *\n * @param out - The chunk buffer to append to.\n * @param name - The element name, not yet resolved.\n * @param value - The element's value.\n * @param depth - Current nesting depth, for indentation and the depth cap.\n * @param options - Resolved render options.\n */\nconst renderElement = (out: Array<string>, name: string, value: XmlValue, depth: number, options: ResolvedOptions): void => {\n assertWithinDepth(depth, options);\n\n if (Array.isArray(value)) {\n renderRepeated(out, name, value, depth, options);\n return;\n }\n\n // An element opens its own line rather than having its caller do it, which is\n // what keeps a repeated run of children on separate lines. The root is the\n // one element that has nothing in front of it.\n if (options.format && depth > 0) openLine(out, depth, options);\n\n const tag = options.namer(name);\n\n if (Predicate.isString(value) || Predicate.isUndefined(value)) {\n renderLeaf(out, tag, value, options);\n return;\n }\n\n // The array case returned above; `Predicate.isObject` narrows what is left to\n // a record, since `Array.isArray` alone leaves a `ReadonlyArray` in the union.\n if (!Predicate.isObject(value)) return;\n renderRecord(out, tag, value as XmlRecord, depth, options);\n};\n\n/**\n * @description Refuses to walk deeper than the render allows. A value can nest without end, and every one of those levels costs a stack frame here, so the cap is\n * checked on the way down rather than trusted to the caller.\n *\n * @param depth - The depth about to be written.\n * @param options - Resolved render options.\n *\n * @throws {XmlRenderError} When the depth is past the cap.\n */\nconst assertWithinDepth = (depth: number, options: ResolvedOptions): void => {\n if (depth > options.maxDepth) {\n throw new XmlRenderError({\n message: `XML nesting exceeded maxDepth (${options.maxDepth}). Raise the limit if the document is legitimately this deep.`,\n });\n }\n};\n\n/**\n * @description Renders a repeated run of children under one name: `tags: ['a', 'b']` renders `<tags>a</tags><tags>b</tags>`, not one element wrapping both. The\n * name is already the element's own, so a name only has to be supplied where no name is available. Checked before the line break and the name are\n * taken, because the array itself is not an element: opening a line for it as well as for each of its members would leave a blank line where the\n * array was.\n *\n * @param out - The chunk buffer to append to.\n * @param name - The element name, not yet resolved.\n * @param members - The children to write, one after another.\n * @param depth - The depth the run sits at.\n * @param options - Resolved render options.\n */\nconst renderRepeated = (out: Array<string>, name: string, members: ReadonlyArray<XmlValue>, depth: number, options: ResolvedOptions): void => {\n // An empty array still gets an element. Writing nothing would make a field\n // that was present and empty indistinguishable from one that was never\n // there, and a document that came from a schema is easier to trust when the\n // element it describes is actually in the output.\n if (members.length === 0) {\n if (options.format && depth > 0) openLine(out, depth, options);\n writeEmpty(out, options.namer(name), options);\n return;\n }\n for (const member of members) renderElement(out, name, member, depth, options);\n};\n\n/**\n * @description Renders an element whose value is character data, or nothing. An empty string is character data that happens to be empty, and an element holding\n * none of it is the same element as one holding nothing at all — as is an `undefined` element, which is an absent one. The renderer is handed values\n * that never went through the schema — a caller building a document by hand — so the absent case is reachable, and an empty element is the honest\n * rendering of both.\n *\n * @param out - The chunk buffer to append to.\n * @param tag - The element's name, already resolved.\n * @param value - The element's character data, or `undefined` for an absent element.\n * @param options - Resolved render options.\n */\nconst renderLeaf = (out: Array<string>, tag: string, value: string | undefined, options: ResolvedOptions): void => {\n if (Predicate.isUndefined(value) || value === '') {\n writeEmpty(out, tag, options);\n return;\n }\n out.push('<', tag, '>', escapeText(value), '</', tag, '>');\n};\n\n/**\n * @description Renders an element holding a record: the attributes gathered from its `@` keys, the character data from its `#text` key, and its remaining keys as\n * child elements.\n *\n * @param out - The chunk buffer to append to.\n * @param tag - The element's name, already resolved.\n * @param record - The element's value.\n * @param depth - The depth the element sits at.\n * @param options - Resolved render options.\n */\nconst renderRecord = (out: Array<string>, tag: string, record: XmlRecord, depth: number, options: ResolvedOptions): void => {\n const fields = collectFields(record, options);\n const text = textOf(record);\n const children = fields.children;\n\n // Self-closing is decided by whether the element has any *content*, not by\n // whether it has attributes: `<a id=\"1\"/>` is the same element as\n // `<a id=\"1\">` with nothing in it, and the short form is what every XML\n // writer produces.\n if (children === undefined && text === '') {\n writeEmpty(out, tag, options, fields.attributes);\n return;\n }\n\n out.push('<', tag, fields.attributes, '>');\n\n // Character data sits inline when it is all an element has, and on its own\n // line when the element also has children, so an indented document does not\n // end up with its first line of text glued to its opening tag.\n if (text !== '') {\n if (children !== undefined && options.format) openLine(out, depth + 1, options);\n out.push(escapeText(text));\n }\n\n if (children !== undefined) {\n writeChildren(out, record, children, depth, options);\n if (options.format) openLine(out, depth, options);\n }\n\n out.push('</', tag, '>');\n};\n\n/**\n * @description An element's keys resolved into the roles they play.\n */\ninterface Fields {\n /**\n * @description The element's rendered attributes, each with its leading space, or `''` when it has none.\n */\n readonly attributes: string;\n\n /**\n * @description The names of the child elements, in the order they will be written, or `undefined` when the element has none. `undefined` rather than an empty\n * array because \"has children\" is one of the two things the self-closing decision turns on, and the other is the text.\n */\n readonly children: Array<string> | undefined;\n}\n\n/**\n * @description One pass over a record's keys, collecting all three roles at once: the attributes are rendered as they are found, the child names are set aside for\n * the pass that writes them, and the text key is left to {@link textOf}. A pass for the attributes, a pass for the children and an index for the text\n * instead walks the keys three times and allocates the key array twice, which on a document of a few thousand elements is thousands of allocations\n * for nothing. Sorting is off by default, and the default path is the one that matters, so the attributes are built as they are found and there is\n * nothing to sort. When it is on, the attribute keys are collected instead and rendered afterwards in sorted order, which costs an array per element\n * and buys output that does not depend on the order the fields happened to be declared in.\n *\n * @param record - The element's value.\n * @param options - Resolved render options.\n *\n * @returns The element's rendered attributes and its child names.\n */\nconst collectFields = (record: XmlRecord, options: ResolvedOptions): Fields => {\n const keys = Object.keys(record);\n let attributes = '';\n let children: Array<string> | undefined;\n const sortAttributes = options.sortKeys ? ([] as Array<string>) : undefined;\n\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i] as string;\n const child = record[key];\n\n // An absent field is not written at all, which is what keeps an unset\n // optional attribute out of the document rather than in it as `a=\"\"`, and\n // an absent child out of it rather than in it as `<a/>`. The text key is\n // read by `textOf` either way, so skipping it here costs nothing.\n if (child === undefined) continue;\n\n if (isAttributeKey(key)) {\n // Only keys with a value are collected, so every name in `sortAttributes`\n // has one to read back out of.\n if (sortAttributes === undefined) attributes += renderAttribute(key, child, options);\n else sortAttributes.push(key);\n continue;\n }\n\n if (!isTextKey(key)) (children ??= []).push(key);\n }\n\n if (sortAttributes !== undefined) {\n sortAttributes.sort();\n attributes += sortedAttributes(sortAttributes, record, options);\n children?.sort();\n }\n\n return { attributes, children };\n};\n\n/**\n * @description The attributes named by `keys`, rendered in the order given. Only reached when the render was asked to sort keys, where the names are collected\n * during the key pass and written here so their order does not follow the order the fields were declared in.\n *\n * @param keys - The attribute keys to write, in the order to write them.\n * @param record - The element's value, to read the attribute values out of.\n * @param options - Resolved render options.\n *\n * @returns The rendered attributes, each with its leading space.\n */\nconst sortedAttributes = (keys: ReadonlyArray<string>, record: XmlRecord, options: ResolvedOptions): string => {\n let attributes = '';\n for (const key of keys) attributes += renderAttribute(key, record[key], options);\n return attributes;\n};\n\n/**\n * @description One attribute, written whole. The leading space is part of it so the caller can concatenate attributes and the opening tag without a separator of\n * its own.\n *\n * @param key - The attribute's key, with or without its `@` prefix.\n * @param value - The attribute's value.\n * @param options - Resolved render options.\n *\n * @returns The attribute, ready to write inside the opening tag.\n */\nconst renderAttribute = (key: string, value: XmlValue, options: ResolvedOptions): string => {\n const name = options.namer(attributeName(key));\n return ' ' + name + '=\"' + escapeAttribute(attributeText(value)) + '\"';\n};\n\n/**\n * @description Writes an element's children, by name and in the order their keys were found. The names are what the key pass kept; the values are read back out of\n * the record here, because keeping both would mean a second array per element.\n *\n * @param out - The chunk buffer to append to.\n * @param record - The element's value.\n * @param children - The child names, in the order to write them.\n * @param depth - The depth the parent sits at; its children are one deeper.\n * @param options - Resolved render options.\n */\nconst writeChildren = (out: Array<string>, record: XmlRecord, children: ReadonlyArray<string>, depth: number, options: ResolvedOptions): void => {\n for (let i = 0; i < children.length; i++) {\n const key = children[i] as string;\n const child = record[key];\n if (child === undefined) continue;\n renderElement(out, key, child, depth + 1, options);\n }\n};\n\n/**\n * @description Starts a new line at the given depth, when pretty-printing.\n *\n * @param out - The chunk buffer to append to.\n * @param depth - The depth the line sits at.\n * @param options - Resolved render options.\n */\nconst openLine = (out: Array<string>, depth: number, options: ResolvedOptions): void => {\n out.push('\\n');\n out.push(options.lineAt(depth));\n};\n\n/**\n * @description Writes an element with no content, in whichever of the two forms the options ask for. Every path that produces an element with nothing in it goes\n * through here, so the self-closing decision is made in exactly one place. That matters because \"nothing in it\" arrives four different ways — an\n * empty string, an absent value, an empty array, and a record whose fields are all absent — and four separate decisions are four chances for one of\n * them to write the long form by accident.\n *\n * @param out - The chunk buffer to append to.\n * @param tag - The element's name, already resolved.\n * @param options - Resolved render options.\n * @param attributes - The element's rendered attributes, if it has any. Defaults to none.\n */\nconst writeEmpty = (out: Array<string>, tag: string, options: ResolvedOptions, attributes = ''): void => {\n if (options.suppressEmptyNode) out.push('<', tag, attributes, '/>');\n else out.push('<', tag, attributes, '></', tag, '>');\n};\n\n/**\n * @description An element's character data, with the reserved text key read off its value.\n *\n * @param record - The element's value.\n *\n * @returns The text to write between the tags, or `''` when the element has none.\n */\nconst textOf = (record: XmlRecord): string => {\n const text = record[TEXT_KEY];\n if (Predicate.isUndefined(text)) return '';\n return Predicate.isString(text) ? text : renderScalar(text);\n};\n\n/**\n * @description An attribute value as the character data it is written as. A bare string is the only sensible shape, since an attribute holds nothing else. A\n * non-string is stringified rather than rejected: the schema is what enforces the field's type, and rejecting here would duplicate that check with a\n * different error and a message that names neither the field nor the document.\n *\n * @param value - The attribute's value.\n *\n * @returns The text to escape and write between the quotes.\n */\nconst attributeText = (value: XmlValue): string => {\n if (Predicate.isString(value)) return value;\n if (Predicate.isUndefined(value)) return '';\n return renderScalar(value);\n};\n\n/**\n * @description Renders a leaf that is not a string as the character data an XML document can hold. A schema-derived value never reaches here —\n * `Schema.toCodecStringTree` has already turned every scalar into a string — so this is for values a caller built by hand. A value with no sensible\n * text form is rendered as nothing rather than as `[object Object]`, which would silently write a document that parses back to something else.\n *\n * @param value - The leaf to render.\n *\n * @returns The leaf's textual form.\n */\nconst renderScalar = (value: Exclude<XmlValue, string | undefined>): string => {\n if (Predicate.isNull(value)) return 'null';\n try {\n return JSON.stringify(value) ?? '';\n } catch {\n return ''; // circular or otherwise not representable\n }\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
//#region src/xml-error.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* @description The specific cause of an {@link XmlError}, as a tagged union. The `_tag` on each member is the discriminant `Effect.catchReason` matches on, and
|
|
5
|
+
* the payload is what a handler needs in order to decide or to report. Every member is a case the decoder or the name validators actually raise —
|
|
6
|
+
* there is no catch-all member, so an exhaustive `match` stays exhaustive as causes are added.
|
|
7
|
+
*/
|
|
8
|
+
export declare const XmlErrorReason: Schema.TaggedUnion<{
|
|
9
|
+
readonly EntityRejected: Schema.TaggedStruct<"EntityRejected", {
|
|
10
|
+
/**
|
|
11
|
+
* @description Which registration was in progress. Both are runtime-injected, which is why they share a tier for limit accounting.
|
|
12
|
+
*/
|
|
13
|
+
readonly context: Schema.Literals<readonly ["external", "input"]>;
|
|
14
|
+
/**
|
|
15
|
+
* @description The entity name, without `&` or `;`.
|
|
16
|
+
*/
|
|
17
|
+
readonly name: Schema.String;
|
|
18
|
+
}>;
|
|
19
|
+
readonly ExpandedLengthLimitExceeded: Schema.TaggedStruct<"ExpandedLengthLimitExceeded", {
|
|
20
|
+
/**
|
|
21
|
+
* @description The accumulated growth that tripped the limit.
|
|
22
|
+
*/
|
|
23
|
+
readonly actual: Schema.Number;
|
|
24
|
+
/**
|
|
25
|
+
* @description The configured ceiling.
|
|
26
|
+
*/
|
|
27
|
+
readonly limit: Schema.Number;
|
|
28
|
+
}>;
|
|
29
|
+
readonly ExpansionLimitExceeded: Schema.TaggedStruct<"ExpansionLimitExceeded", {
|
|
30
|
+
/**
|
|
31
|
+
* @description The count that tripped the limit. Deliberately not reset on failure, so this is the real over-limit total rather than the ceiling.
|
|
32
|
+
*/
|
|
33
|
+
readonly actual: Schema.Number;
|
|
34
|
+
/**
|
|
35
|
+
* @description The configured ceiling. The check is `actual > limit`, so a limit of `2` allows two expansions.
|
|
36
|
+
*/
|
|
37
|
+
readonly limit: Schema.Number;
|
|
38
|
+
}>;
|
|
39
|
+
readonly InvalidEntityName: Schema.TaggedStruct<"InvalidEntityName", {
|
|
40
|
+
/**
|
|
41
|
+
* @description The rejected name, as it was passed in.
|
|
42
|
+
*/
|
|
43
|
+
readonly name: Schema.String;
|
|
44
|
+
/**
|
|
45
|
+
* @description The offending character, or `#` for a name that starts with one. A leading `#` is refused by position rather than by the character sweep,
|
|
46
|
+
* because a name starting with `#` is a numeric reference's token and would collide with the numeric pipeline.
|
|
47
|
+
*/
|
|
48
|
+
readonly character: Schema.String;
|
|
49
|
+
}>;
|
|
50
|
+
readonly InvalidProduction: Schema.TaggedStruct<"InvalidProduction", {
|
|
51
|
+
readonly production: Schema.String;
|
|
52
|
+
/**
|
|
53
|
+
* @description The productions that would have been accepted, comma-separated, as they appear in the message.
|
|
54
|
+
*/
|
|
55
|
+
readonly expected: Schema.String;
|
|
56
|
+
}>;
|
|
57
|
+
readonly MissingOptions: Schema.TaggedStruct<"MissingOptions", {
|
|
58
|
+
/**
|
|
59
|
+
* @description The parameter that was given nothing, named as it appears in the signature.
|
|
60
|
+
*/
|
|
61
|
+
readonly parameter: Schema.String;
|
|
62
|
+
}>;
|
|
63
|
+
readonly ProhibitedCharacterReference: Schema.TaggedStruct<"ProhibitedCharacterReference", {
|
|
64
|
+
/**
|
|
65
|
+
* @description The raw token without `&` and `;`, e.g. `#38` or `#x26`.
|
|
66
|
+
*/
|
|
67
|
+
readonly token: Schema.String;
|
|
68
|
+
/**
|
|
69
|
+
* @description The codepoint the reference resolved to, so a log can name the character that was refused.
|
|
70
|
+
*/
|
|
71
|
+
readonly codepoint: Schema.Number;
|
|
72
|
+
}>;
|
|
73
|
+
}>;
|
|
74
|
+
/**
|
|
75
|
+
* @description The reason a well-formedness or policy failure occurred.
|
|
76
|
+
*/
|
|
77
|
+
export type XmlErrorReason = typeof XmlErrorReason.Type;
|
|
78
|
+
declare const XmlError_base: Schema.Class<XmlError, Schema.TaggedStruct<"XmlError", {
|
|
79
|
+
/**
|
|
80
|
+
* @description The specific cause. Narrow on `_tag`, or recover with `Effect.catchReason`.
|
|
81
|
+
*/
|
|
82
|
+
readonly reason: Schema.TaggedUnion<{
|
|
83
|
+
readonly EntityRejected: Schema.TaggedStruct<"EntityRejected", {
|
|
84
|
+
/**
|
|
85
|
+
* @description Which registration was in progress. Both are runtime-injected, which is why they share a tier for limit accounting.
|
|
86
|
+
*/
|
|
87
|
+
readonly context: Schema.Literals<readonly ["external", "input"]>;
|
|
88
|
+
/**
|
|
89
|
+
* @description The entity name, without `&` or `;`.
|
|
90
|
+
*/
|
|
91
|
+
readonly name: Schema.String;
|
|
92
|
+
}>;
|
|
93
|
+
readonly ExpandedLengthLimitExceeded: Schema.TaggedStruct<"ExpandedLengthLimitExceeded", {
|
|
94
|
+
/**
|
|
95
|
+
* @description The accumulated growth that tripped the limit.
|
|
96
|
+
*/
|
|
97
|
+
readonly actual: Schema.Number;
|
|
98
|
+
/**
|
|
99
|
+
* @description The configured ceiling.
|
|
100
|
+
*/
|
|
101
|
+
readonly limit: Schema.Number;
|
|
102
|
+
}>;
|
|
103
|
+
readonly ExpansionLimitExceeded: Schema.TaggedStruct<"ExpansionLimitExceeded", {
|
|
104
|
+
/**
|
|
105
|
+
* @description The count that tripped the limit. Deliberately not reset on failure, so this is the real over-limit total rather than the ceiling.
|
|
106
|
+
*/
|
|
107
|
+
readonly actual: Schema.Number;
|
|
108
|
+
/**
|
|
109
|
+
* @description The configured ceiling. The check is `actual > limit`, so a limit of `2` allows two expansions.
|
|
110
|
+
*/
|
|
111
|
+
readonly limit: Schema.Number;
|
|
112
|
+
}>;
|
|
113
|
+
readonly InvalidEntityName: Schema.TaggedStruct<"InvalidEntityName", {
|
|
114
|
+
/**
|
|
115
|
+
* @description The rejected name, as it was passed in.
|
|
116
|
+
*/
|
|
117
|
+
readonly name: Schema.String;
|
|
118
|
+
/**
|
|
119
|
+
* @description The offending character, or `#` for a name that starts with one. A leading `#` is refused by position rather than by the character sweep,
|
|
120
|
+
* because a name starting with `#` is a numeric reference's token and would collide with the numeric pipeline.
|
|
121
|
+
*/
|
|
122
|
+
readonly character: Schema.String;
|
|
123
|
+
}>;
|
|
124
|
+
readonly InvalidProduction: Schema.TaggedStruct<"InvalidProduction", {
|
|
125
|
+
readonly production: Schema.String;
|
|
126
|
+
/**
|
|
127
|
+
* @description The productions that would have been accepted, comma-separated, as they appear in the message.
|
|
128
|
+
*/
|
|
129
|
+
readonly expected: Schema.String;
|
|
130
|
+
}>;
|
|
131
|
+
readonly MissingOptions: Schema.TaggedStruct<"MissingOptions", {
|
|
132
|
+
/**
|
|
133
|
+
* @description The parameter that was given nothing, named as it appears in the signature.
|
|
134
|
+
*/
|
|
135
|
+
readonly parameter: Schema.String;
|
|
136
|
+
}>;
|
|
137
|
+
readonly ProhibitedCharacterReference: Schema.TaggedStruct<"ProhibitedCharacterReference", {
|
|
138
|
+
/**
|
|
139
|
+
* @description The raw token without `&` and `;`, e.g. `#38` or `#x26`.
|
|
140
|
+
*/
|
|
141
|
+
readonly token: Schema.String;
|
|
142
|
+
/**
|
|
143
|
+
* @description The codepoint the reference resolved to, so a log can name the character that was refused.
|
|
144
|
+
*/
|
|
145
|
+
readonly codepoint: Schema.Number;
|
|
146
|
+
}>;
|
|
147
|
+
}>;
|
|
148
|
+
/**
|
|
149
|
+
* @description Human-readable description. The `[EntityReplacer]` and `[EntityDecoder]` prefixes are reproduced verbatim from the original throw sites, because
|
|
150
|
+
* they are documented as load-bearing for anything matching on them.
|
|
151
|
+
*/
|
|
152
|
+
readonly message: Schema.String;
|
|
153
|
+
}>, import("effect/Cause").YieldableError>;
|
|
154
|
+
/**
|
|
155
|
+
* @description Every failure this package can report, in the `E` channel of the effects that can fail. Carries both a `reason` — the typed, matchable cause — and
|
|
156
|
+
* a `message`, which is the human-readable form the package has always produced. Both are part of the schema, so an error survives a round-trip
|
|
157
|
+
* through a serialised boundary without losing either.
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```typescript
|
|
161
|
+
* import { Effect } from 'effect';
|
|
162
|
+
* import { EntityDecoder } from '@endevops/effect-xml-codec';
|
|
163
|
+
*
|
|
164
|
+
* const program = Effect.gen(function*() {
|
|
165
|
+
* const decoder = yield* EntityDecoder.make({});
|
|
166
|
+
* return yield* decoder.decode('a & b');
|
|
167
|
+
* });
|
|
168
|
+
* ```;
|
|
169
|
+
*/
|
|
170
|
+
export declare class XmlError extends XmlError_base {}
|
|
171
|
+
//#endregion
|
|
172
|
+
//# sourceMappingURL=xml-error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xml-error.d.ts","names":[],"sources":["../src/xml-error.ts"],"mappings":";;;;;;;qBAkCa,gBAAc,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;YAwGf,wBAAwB,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAkBtC,iBAAiB"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
//#region src/xml-error.ts
|
|
3
|
+
/**
|
|
4
|
+
* @description Every way the entity decoder and the name validators can fail, as one typed error. The package used to throw plain `Error` and `TypeError` from
|
|
5
|
+
* places spread across the entity and naming areas. A caller had to match on message text, and there was nothing to narrow on. All of it is now a
|
|
6
|
+
* single {@link XmlError} in the `E` channel of the effects that can fail, with the specific cause in a `reason` field rather than parsed back out of
|
|
7
|
+
* a string.
|
|
8
|
+
*
|
|
9
|
+
* ## Why one error with a `reason`, and not one error per cause
|
|
10
|
+
*
|
|
11
|
+
* The causes would mean a class each, and a caller handling "any of these" would need `catchTags` with all of them. The causes are not independent
|
|
12
|
+
* decisions a caller usually wants to make separately — they are all "this XML input was not acceptable", and the useful split is coarse: a bad
|
|
13
|
+
* _configuration_ versus a bad _document_. So there is one error, and `reason` narrows to the specific cause. Recovery is a `catchReason` away:
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { Effect } from 'effect';
|
|
18
|
+
* import { EntityDecoder, XmlError } from '@endevops/effect-xml-codec';
|
|
19
|
+
*
|
|
20
|
+
* const limited = new EntityDecoder({ limit: { maxTotalExpansions: 2 } }).decode('&&&').pipe(
|
|
21
|
+
* Effect.catchReason('XmlError', 'ExpansionLimitExceeded', reason => Effect.succeed(`gave up after ${reason.actual}`)),
|
|
22
|
+
* );
|
|
23
|
+
* ```;
|
|
24
|
+
*
|
|
25
|
+
* The message is kept alongside `reason` and is part of the schema, because the text is load-bearing: the
|
|
26
|
+
* `[EntityReplacer]` prefix in particular is documented as something callers match on, so it is reproduced exactly
|
|
27
|
+
* rather than reworded.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* @description The specific cause of an {@link XmlError}, as a tagged union. The `_tag` on each member is the discriminant `Effect.catchReason` matches on, and
|
|
31
|
+
* the payload is what a handler needs in order to decide or to report. Every member is a case the decoder or the name validators actually raise —
|
|
32
|
+
* there is no catch-all member, so an exhaustive `match` stays exhaustive as causes are added.
|
|
33
|
+
*/
|
|
34
|
+
const XmlErrorReason = Schema.TaggedUnion({
|
|
35
|
+
/**
|
|
36
|
+
* @description A required argument was `null` or another non-value where the package requires a real one. Raised by the factories that take caller-supplied
|
|
37
|
+
* input — {@link EntityDecoder.make} among them — when they are handed `null` for an options object that has no meaningful default. It is a
|
|
38
|
+
* distinct case from the rest because the argument is not _wrong_, it is _absent_, and a caller who wrote `make(null)` meant something the type
|
|
39
|
+
* system does not allow: a decoder with every default is `make({})`, and saying so here is more useful than silently producing one.
|
|
40
|
+
*/
|
|
41
|
+
MissingOptions: {
|
|
42
|
+
/**
|
|
43
|
+
* @description The parameter that was given nothing, named as it appears in the signature.
|
|
44
|
+
*/
|
|
45
|
+
parameter: Schema.String },
|
|
46
|
+
/**
|
|
47
|
+
* @description A name was checked against one of the five XML name productions and given a different one. Unreachable from TypeScript, where `Production` is a
|
|
48
|
+
* closed union — it is the guard for untyped JavaScript callers and for values that crossed a boundary as `unknown`.
|
|
49
|
+
*/
|
|
50
|
+
InvalidProduction: {
|
|
51
|
+
production: Schema.String,
|
|
52
|
+
/**
|
|
53
|
+
* @description The productions that would have been accepted, comma-separated, as they appear in the message.
|
|
54
|
+
*/
|
|
55
|
+
expected: Schema.String
|
|
56
|
+
},
|
|
57
|
+
/**
|
|
58
|
+
* @description An entity name cannot be written as a reference, so it is refused at registration.
|
|
59
|
+
*/
|
|
60
|
+
InvalidEntityName: {
|
|
61
|
+
/**
|
|
62
|
+
* @description The rejected name, as it was passed in.
|
|
63
|
+
*/
|
|
64
|
+
name: Schema.String,
|
|
65
|
+
/**
|
|
66
|
+
* @description The offending character, or `#` for a name that starts with one. A leading `#` is refused by position rather than by the character sweep,
|
|
67
|
+
* because a name starting with `#` is a numeric reference's token and would collide with the numeric pipeline.
|
|
68
|
+
*/
|
|
69
|
+
character: Schema.String
|
|
70
|
+
},
|
|
71
|
+
/**
|
|
72
|
+
* @description A registration hook refused an entity and asked for the registration to abort.
|
|
73
|
+
*/
|
|
74
|
+
EntityRejected: {
|
|
75
|
+
/**
|
|
76
|
+
* @description Which registration was in progress. Both are runtime-injected, which is why they share a tier for limit accounting.
|
|
77
|
+
*/
|
|
78
|
+
context: Schema.Literals(["external", "input"]),
|
|
79
|
+
/**
|
|
80
|
+
* @description The entity name, without `&` or `;`.
|
|
81
|
+
*/
|
|
82
|
+
name: Schema.String
|
|
83
|
+
},
|
|
84
|
+
/**
|
|
85
|
+
* @description A document expanded more tracked entity references than {@link EntityDecoderLimitOptions.maxTotalExpansions} allows. A document can define an
|
|
86
|
+
* entity that references another ten times over. Ten deep is a denial of service; a hundred is a fork bomb written in XML.
|
|
87
|
+
*/
|
|
88
|
+
ExpansionLimitExceeded: {
|
|
89
|
+
/**
|
|
90
|
+
* @description The count that tripped the limit. Deliberately not reset on failure, so this is the real over-limit total rather than the ceiling.
|
|
91
|
+
*/
|
|
92
|
+
actual: Schema.Number,
|
|
93
|
+
/**
|
|
94
|
+
* @description The configured ceiling. The check is `actual > limit`, so a limit of `2` allows two expansions.
|
|
95
|
+
*/
|
|
96
|
+
limit: Schema.Number
|
|
97
|
+
},
|
|
98
|
+
/**
|
|
99
|
+
* @description A document grew by more characters through entity expansion than {@link EntityDecoderLimitOptions.maxExpandedLength} allows. Only the surplus
|
|
100
|
+
* counts: a reference whose replacement is no longer than the `&token;` it replaces contributes nothing, so this bounds growth rather than document
|
|
101
|
+
* size.
|
|
102
|
+
*/
|
|
103
|
+
ExpandedLengthLimitExceeded: {
|
|
104
|
+
/**
|
|
105
|
+
* @description The accumulated growth that tripped the limit.
|
|
106
|
+
*/
|
|
107
|
+
actual: Schema.Number,
|
|
108
|
+
/**
|
|
109
|
+
* @description The configured ceiling.
|
|
110
|
+
*/
|
|
111
|
+
limit: Schema.Number
|
|
112
|
+
},
|
|
113
|
+
/**
|
|
114
|
+
* @description A numeric character reference was prohibited by the configured policy.
|
|
115
|
+
*/
|
|
116
|
+
ProhibitedCharacterReference: {
|
|
117
|
+
/**
|
|
118
|
+
* @description The raw token without `&` and `;`, e.g. `#38` or `#x26`.
|
|
119
|
+
*/
|
|
120
|
+
token: Schema.String,
|
|
121
|
+
/**
|
|
122
|
+
* @description The codepoint the reference resolved to, so a log can name the character that was refused.
|
|
123
|
+
*/
|
|
124
|
+
codepoint: Schema.Number
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
/**
|
|
128
|
+
* @description Every failure this package can report, in the `E` channel of the effects that can fail. Carries both a `reason` — the typed, matchable cause — and
|
|
129
|
+
* a `message`, which is the human-readable form the package has always produced. Both are part of the schema, so an error survives a round-trip
|
|
130
|
+
* through a serialised boundary without losing either.
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```typescript
|
|
134
|
+
* import { Effect } from 'effect';
|
|
135
|
+
* import { EntityDecoder } from '@endevops/effect-xml-codec';
|
|
136
|
+
*
|
|
137
|
+
* const program = Effect.gen(function*() {
|
|
138
|
+
* const decoder = yield* EntityDecoder.make({});
|
|
139
|
+
* return yield* decoder.decode('a & b');
|
|
140
|
+
* });
|
|
141
|
+
* ```;
|
|
142
|
+
*/
|
|
143
|
+
var XmlError = class extends Schema.TaggedError()("XmlError", {
|
|
144
|
+
/**
|
|
145
|
+
* @description The specific cause. Narrow on `_tag`, or recover with `Effect.catchReason`.
|
|
146
|
+
*/
|
|
147
|
+
reason: XmlErrorReason,
|
|
148
|
+
/**
|
|
149
|
+
* @description Human-readable description. The `[EntityReplacer]` and `[EntityDecoder]` prefixes are reproduced verbatim from the original throw sites, because
|
|
150
|
+
* they are documented as load-bearing for anything matching on them.
|
|
151
|
+
*/
|
|
152
|
+
message: Schema.String
|
|
153
|
+
}) {};
|
|
154
|
+
//#endregion
|
|
155
|
+
export { XmlError, XmlErrorReason };
|
|
156
|
+
|
|
157
|
+
//# sourceMappingURL=xml-error.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xml-error.js","names":[],"sources":["../src/xml-error.ts"],"sourcesContent":["/**\n * @description Every way the entity decoder and the name validators can fail, as one typed error. The package used to throw plain `Error` and `TypeError` from\n * places spread across the entity and naming areas. A caller had to match on message text, and there was nothing to narrow on. All of it is now a\n * single {@link XmlError} in the `E` channel of the effects that can fail, with the specific cause in a `reason` field rather than parsed back out of\n * a string.\n *\n * ## Why one error with a `reason`, and not one error per cause\n *\n * The causes would mean a class each, and a caller handling \"any of these\" would need `catchTags` with all of them. The causes are not independent\n * decisions a caller usually wants to make separately — they are all \"this XML input was not acceptable\", and the useful split is coarse: a bad\n * _configuration_ versus a bad _document_. So there is one error, and `reason` narrows to the specific cause. Recovery is a `catchReason` away:\n *\n * @example\n * ```typescript\n * import { Effect } from 'effect';\n * import { EntityDecoder, XmlError } from '@endevops/effect-xml-codec';\n *\n * const limited = new EntityDecoder({ limit: { maxTotalExpansions: 2 } }).decode('&&&').pipe(\n * Effect.catchReason('XmlError', 'ExpansionLimitExceeded', reason => Effect.succeed(`gave up after ${reason.actual}`)),\n * );\n * ```;\n *\n * The message is kept alongside `reason` and is part of the schema, because the text is load-bearing: the\n * `[EntityReplacer]` prefix in particular is documented as something callers match on, so it is reproduced exactly\n * rather than reworded.\n */\n\nimport { Schema } from 'effect';\n\n/**\n * @description The specific cause of an {@link XmlError}, as a tagged union. The `_tag` on each member is the discriminant `Effect.catchReason` matches on, and\n * the payload is what a handler needs in order to decide or to report. Every member is a case the decoder or the name validators actually raise —\n * there is no catch-all member, so an exhaustive `match` stays exhaustive as causes are added.\n */\nexport const XmlErrorReason = Schema.TaggedUnion({\n /**\n * @description A required argument was `null` or another non-value where the package requires a real one. Raised by the factories that take caller-supplied\n * input — {@link EntityDecoder.make} among them — when they are handed `null` for an options object that has no meaningful default. It is a\n * distinct case from the rest because the argument is not _wrong_, it is _absent_, and a caller who wrote `make(null)` meant something the type\n * system does not allow: a decoder with every default is `make({})`, and saying so here is more useful than silently producing one.\n */\n MissingOptions: {\n /**\n * @description The parameter that was given nothing, named as it appears in the signature.\n */\n parameter: Schema.String,\n },\n\n /**\n * @description A name was checked against one of the five XML name productions and given a different one. Unreachable from TypeScript, where `Production` is a\n * closed union — it is the guard for untyped JavaScript callers and for values that crossed a boundary as `unknown`.\n */\n InvalidProduction: {\n production: Schema.String,\n /**\n * @description The productions that would have been accepted, comma-separated, as they appear in the message.\n */\n expected: Schema.String,\n },\n\n /**\n * @description An entity name cannot be written as a reference, so it is refused at registration.\n */\n InvalidEntityName: {\n /**\n * @description The rejected name, as it was passed in.\n */\n name: Schema.String,\n /**\n * @description The offending character, or `#` for a name that starts with one. A leading `#` is refused by position rather than by the character sweep,\n * because a name starting with `#` is a numeric reference's token and would collide with the numeric pipeline.\n */\n character: Schema.String,\n },\n\n /**\n * @description A registration hook refused an entity and asked for the registration to abort.\n */\n EntityRejected: {\n /**\n * @description Which registration was in progress. Both are runtime-injected, which is why they share a tier for limit accounting.\n */\n context: Schema.Literals(['external', 'input']),\n /**\n * @description The entity name, without `&` or `;`.\n */\n name: Schema.String,\n },\n\n /**\n * @description A document expanded more tracked entity references than {@link EntityDecoderLimitOptions.maxTotalExpansions} allows. A document can define an\n * entity that references another ten times over. Ten deep is a denial of service; a hundred is a fork bomb written in XML.\n */\n ExpansionLimitExceeded: {\n /**\n * @description The count that tripped the limit. Deliberately not reset on failure, so this is the real over-limit total rather than the ceiling.\n */\n actual: Schema.Number,\n /**\n * @description The configured ceiling. The check is `actual > limit`, so a limit of `2` allows two expansions.\n */\n limit: Schema.Number,\n },\n\n /**\n * @description A document grew by more characters through entity expansion than {@link EntityDecoderLimitOptions.maxExpandedLength} allows. Only the surplus\n * counts: a reference whose replacement is no longer than the `&token;` it replaces contributes nothing, so this bounds growth rather than document\n * size.\n */\n ExpandedLengthLimitExceeded: {\n /**\n * @description The accumulated growth that tripped the limit.\n */\n actual: Schema.Number,\n /**\n * @description The configured ceiling.\n */\n limit: Schema.Number,\n },\n\n /**\n * @description A numeric character reference was prohibited by the configured policy.\n */\n ProhibitedCharacterReference: {\n /**\n * @description The raw token without `&` and `;`, e.g. `#38` or `#x26`.\n */\n token: Schema.String,\n /**\n * @description The codepoint the reference resolved to, so a log can name the character that was refused.\n */\n codepoint: Schema.Number,\n },\n});\n\n/**\n * @description The reason a well-formedness or policy failure occurred.\n */\nexport type XmlErrorReason = typeof XmlErrorReason.Type;\n\n/**\n * @description Every failure this package can report, in the `E` channel of the effects that can fail. Carries both a `reason` — the typed, matchable cause — and\n * a `message`, which is the human-readable form the package has always produced. Both are part of the schema, so an error survives a round-trip\n * through a serialised boundary without losing either.\n *\n * @example\n * ```typescript\n * import { Effect } from 'effect';\n * import { EntityDecoder } from '@endevops/effect-xml-codec';\n *\n * const program = Effect.gen(function*() {\n * const decoder = yield* EntityDecoder.make({});\n * return yield* decoder.decode('a & b');\n * });\n * ```;\n */\nexport class XmlError extends Schema.TaggedError<XmlError>()('XmlError', {\n /**\n * @description The specific cause. Narrow on `_tag`, or recover with `Effect.catchReason`.\n */\n reason: XmlErrorReason,\n\n /**\n * @description Human-readable description. The `[EntityReplacer]` and `[EntityDecoder]` prefixes are reproduced verbatim from the original throw sites, because\n * they are documented as load-bearing for anything matching on them.\n */\n message: Schema.String,\n}) {}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,MAAa,iBAAiB,OAAO,YAAY;;;;;;;CAO/C,gBAAgB;;;;AAId,WAAW,OAAO,OACpB;;;;;CAMA,mBAAmB;EACjB,YAAY,OAAO;;;;EAInB,UAAU,OAAO;CACnB;;;;CAKA,mBAAmB;;;;EAIjB,MAAM,OAAO;;;;;EAKb,WAAW,OAAO;CACpB;;;;CAKA,gBAAgB;;;;EAId,SAAS,OAAO,SAAS,CAAC,YAAY,OAAO,CAAC;;;;EAI9C,MAAM,OAAO;CACf;;;;;CAMA,wBAAwB;;;;EAItB,QAAQ,OAAO;;;;EAIf,OAAO,OAAO;CAChB;;;;;;CAOA,6BAA6B;;;;EAI3B,QAAQ,OAAO;;;;EAIf,OAAO,OAAO;CAChB;;;;CAKA,8BAA8B;;;;EAI5B,OAAO,OAAO;;;;EAId,WAAW,OAAO;CACpB;AACF,CAAC;;;;;;;;;;;;;;;;;AAuBD,IAAa,WAAb,cAA8B,OAAO,YAAsB,CAAC,CAAC,YAAY;;;;CAIvE,QAAQ;;;;;CAMR,SAAS,OAAO;AAClB,CAAC,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
//#region src/xml-value.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* @description A record of child elements, attributes and character data. A key starting with `@` is an attribute, the reserved `#text` key is character data, and
|
|
5
|
+
* every other key is a child element name. An `undefined` value means the field is absent, which is how an absent optional field stays
|
|
6
|
+
* distinguishable from an empty one.
|
|
7
|
+
*/
|
|
8
|
+
export interface XmlRecord {
|
|
9
|
+
readonly [name: string]: XmlValue | undefined;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* @description One value in an XML document: nothing at all, character data, a repeated run of children, or a record of attributes, text and child elements.
|
|
13
|
+
* `undefined` is a value of its own rather than an omission, because that is how an absent optional field survives a round trip: a field with no
|
|
14
|
+
* value stays distinguishable from a field whose value is the empty string, and the renderer writes neither of them. The shape is deliberately the
|
|
15
|
+
* same one `Schema.toCodecStringTree` derives, which is what lets every schema feature Effect supports round-trip through this package without
|
|
16
|
+
* re-implementing the derivation.
|
|
17
|
+
*/
|
|
18
|
+
export type XmlValue = string | undefined | ReadonlyArray<XmlValue> | XmlRecord;
|
|
19
|
+
/**
|
|
20
|
+
* @description Whether an arbitrary value is a well-formed {@link XmlValue}.
|
|
21
|
+
*
|
|
22
|
+
* @param input - The candidate value.
|
|
23
|
+
*
|
|
24
|
+
* @returns Whether the value is absent, character data, an array of `XmlValue`, or a plain record of them.
|
|
25
|
+
*/
|
|
26
|
+
export declare const isXmlValue: (input: unknown) => input is XmlValue;
|
|
27
|
+
/**
|
|
28
|
+
* @description A schema for {@link XmlValue}, so a value can be validated on its own — when it arrives from a store or a queue rather than from {@link parseXml},
|
|
29
|
+
* and the schema it belongs to is not in hand.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* import { Schema } from 'effect';
|
|
34
|
+
* import { XmlValue } from '@endevops/effect-xml-codec';
|
|
35
|
+
*
|
|
36
|
+
* Schema.decodeUnknownSync(XmlValue)({ book: { '@id': '1', title: 'Dune' } }); // => { book: { '@id': '1', title: 'Dune' } }
|
|
37
|
+
* Schema.decodeUnknownSync(XmlValue)({ book: { title: 42 } }); // => throws XmlValue
|
|
38
|
+
* ```;
|
|
39
|
+
*/
|
|
40
|
+
export declare const XmlValue: Schema.Codec<XmlValue>;
|
|
41
|
+
//#endregion
|
|
42
|
+
//# sourceMappingURL=xml-value.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xml-value.d.ts","names":[],"sources":["../src/xml-value.ts"],"mappings":";;;;;;;iBAOiB;YACL,eAAe;;;;;;;;;YAUf,gCAAgC,cAAc,YAAY;;;;;;;;qBAezD,aAAU,mBAAqB,SAAS;;;;;;;;;;;;;;qBAuExC,UAAU,OAAO,MAAM"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Predicate, Schema } from "effect";
|
|
2
|
+
//#region src/xml-value.ts
|
|
3
|
+
/**
|
|
4
|
+
* @description How deep {@link isXmlValue} will walk before giving up. A document nested deeper than this is treated as invalid rather than allowed to exhaust the
|
|
5
|
+
* stack.
|
|
6
|
+
*/
|
|
7
|
+
const MAX_GUARD_DEPTH = 512;
|
|
8
|
+
/**
|
|
9
|
+
* @description Whether an arbitrary value is a well-formed {@link XmlValue}.
|
|
10
|
+
*
|
|
11
|
+
* @param input - The candidate value.
|
|
12
|
+
*
|
|
13
|
+
* @returns Whether the value is absent, character data, an array of `XmlValue`, or a plain record of them.
|
|
14
|
+
*/
|
|
15
|
+
const isXmlValue = (input) => check(input, 0);
|
|
16
|
+
/**
|
|
17
|
+
* @description One level of {@link isXmlValue}, with the depth it was reached at. The depth is the whole defence against a value built to be hostile: a
|
|
18
|
+
* self-referential object would otherwise recurse until the stack gave out, and a value nested thousands deep would take it with it. Both are
|
|
19
|
+
* rejected here instead, which is why this is a real recursion with a bound rather than a loop — the model is a tree, and a tree is walked by walking
|
|
20
|
+
* it.
|
|
21
|
+
*
|
|
22
|
+
* @param input - The candidate value.
|
|
23
|
+
* @param depth - How many levels down this value sits.
|
|
24
|
+
*
|
|
25
|
+
* @returns Whether this level is legal, and the rest of the value with it.
|
|
26
|
+
*/
|
|
27
|
+
const check = (input, depth) => {
|
|
28
|
+
if (Predicate.isUndefined(input) || Predicate.isString(input)) return true;
|
|
29
|
+
if (Predicate.isNull(input) || !Predicate.isObjectOrArray(input)) return false;
|
|
30
|
+
if (depth > MAX_GUARD_DEPTH) return false;
|
|
31
|
+
return Array.isArray(input) ? everyMemberIs(input, depth) : everyFieldIs(input, depth);
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* @description Whether an array holds nothing but legal values one level down.
|
|
35
|
+
*
|
|
36
|
+
* @param members - The array's members.
|
|
37
|
+
* @param depth - The depth the array itself sits at.
|
|
38
|
+
*
|
|
39
|
+
* @returns Whether every member is a legal `XmlValue`.
|
|
40
|
+
*/
|
|
41
|
+
const everyMemberIs = (members, depth) => {
|
|
42
|
+
for (const member of members) if (!check(member, depth + 1)) return false;
|
|
43
|
+
return true;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* @description Whether an object is a plain record of legal values one level down. Plainness is checked here rather than by the caller because a `Date` or a `Map`
|
|
47
|
+
* has values a record cannot hold, and walking them would be walking something the model has no way to represent.
|
|
48
|
+
*
|
|
49
|
+
* @param input - The object to walk.
|
|
50
|
+
* @param depth - The depth the object itself sits at.
|
|
51
|
+
*
|
|
52
|
+
* @returns Whether the object is a plain record whose every field is a legal `XmlValue`.
|
|
53
|
+
*/
|
|
54
|
+
const everyFieldIs = (input, depth) => {
|
|
55
|
+
if (!Predicate.isReadonlyObject(input)) return false;
|
|
56
|
+
for (const key of Object.keys(input)) if (!check(input[key], depth + 1)) return false;
|
|
57
|
+
return true;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* @description A schema for {@link XmlValue}, so a value can be validated on its own — when it arrives from a store or a queue rather than from {@link parseXml},
|
|
61
|
+
* and the schema it belongs to is not in hand.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```typescript
|
|
65
|
+
* import { Schema } from 'effect';
|
|
66
|
+
* import { XmlValue } from '@endevops/effect-xml-codec';
|
|
67
|
+
*
|
|
68
|
+
* Schema.decodeUnknownSync(XmlValue)({ book: { '@id': '1', title: 'Dune' } }); // => { book: { '@id': '1', title: 'Dune' } }
|
|
69
|
+
* Schema.decodeUnknownSync(XmlValue)({ book: { title: 42 } }); // => throws XmlValue
|
|
70
|
+
* ```;
|
|
71
|
+
*/
|
|
72
|
+
const XmlValue = Schema.declare(isXmlValue, {
|
|
73
|
+
identifier: "XmlValue",
|
|
74
|
+
expected: "an XML value: character data, an array of them, or a record of them"
|
|
75
|
+
});
|
|
76
|
+
//#endregion
|
|
77
|
+
export { XmlValue, isXmlValue };
|
|
78
|
+
|
|
79
|
+
//# sourceMappingURL=xml-value.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xml-value.js","names":[],"sources":["../src/xml-value.ts"],"sourcesContent":["import { Predicate, Schema } from 'effect';\n\n/**\n * @description A record of child elements, attributes and character data. A key starting with `@` is an attribute, the reserved `#text` key is character data, and\n * every other key is a child element name. An `undefined` value means the field is absent, which is how an absent optional field stays\n * distinguishable from an empty one.\n */\nexport interface XmlRecord {\n readonly [name: string]: XmlValue | undefined;\n}\n\n/**\n * @description One value in an XML document: nothing at all, character data, a repeated run of children, or a record of attributes, text and child elements.\n * `undefined` is a value of its own rather than an omission, because that is how an absent optional field survives a round trip: a field with no\n * value stays distinguishable from a field whose value is the empty string, and the renderer writes neither of them. The shape is deliberately the\n * same one `Schema.toCodecStringTree` derives, which is what lets every schema feature Effect supports round-trip through this package without\n * re-implementing the derivation.\n */\nexport type XmlValue = string | undefined | ReadonlyArray<XmlValue> | XmlRecord;\n\n/**\n * @description How deep {@link isXmlValue} will walk before giving up. A document nested deeper than this is treated as invalid rather than allowed to exhaust the\n * stack.\n */\nconst MAX_GUARD_DEPTH = 512;\n\n/**\n * @description Whether an arbitrary value is a well-formed {@link XmlValue}.\n *\n * @param input - The candidate value.\n *\n * @returns Whether the value is absent, character data, an array of `XmlValue`, or a plain record of them.\n */\nexport const isXmlValue = (input: unknown): input is XmlValue => check(input, 0);\n\n/**\n * @description One level of {@link isXmlValue}, with the depth it was reached at. The depth is the whole defence against a value built to be hostile: a\n * self-referential object would otherwise recurse until the stack gave out, and a value nested thousands deep would take it with it. Both are\n * rejected here instead, which is why this is a real recursion with a bound rather than a loop — the model is a tree, and a tree is walked by walking\n * it.\n *\n * @param input - The candidate value.\n * @param depth - How many levels down this value sits.\n *\n * @returns Whether this level is legal, and the rest of the value with it.\n */\nconst check = (input: unknown, depth: number): boolean => {\n // `undefined` is a value in its own right: it is how an absent optional field survives a round trip, so a\n // record is allowed to hold one and a document is allowed to be missing a field.\n if (Predicate.isUndefined(input) || Predicate.isString(input)) return true;\n if (Predicate.isNull(input) || !Predicate.isObjectOrArray(input)) return false;\n\n if (depth > MAX_GUARD_DEPTH) return false;\n\n // Arrays and records are the only two things left, and each is a walk of its\n // own rather than another branch here: an array of them or a record of them.\n return Array.isArray(input) ? everyMemberIs(input, depth) : everyFieldIs(input, depth);\n};\n\n/**\n * @description Whether an array holds nothing but legal values one level down.\n *\n * @param members - The array's members.\n * @param depth - The depth the array itself sits at.\n *\n * @returns Whether every member is a legal `XmlValue`.\n */\nconst everyMemberIs = (members: ReadonlyArray<unknown>, depth: number): boolean => {\n for (const member of members) {\n if (!check(member, depth + 1)) return false;\n }\n return true;\n};\n\n/**\n * @description Whether an object is a plain record of legal values one level down. Plainness is checked here rather than by the caller because a `Date` or a `Map`\n * has values a record cannot hold, and walking them would be walking something the model has no way to represent.\n *\n * @param input - The object to walk.\n * @param depth - The depth the object itself sits at.\n *\n * @returns Whether the object is a plain record whose every field is a legal `XmlValue`.\n */\nconst everyFieldIs = (input: object, depth: number): boolean => {\n if (!Predicate.isReadonlyObject(input)) return false;\n for (const key of Object.keys(input)) {\n if (!check(input[key], depth + 1)) return false;\n }\n return true;\n};\n\n/**\n * @description A schema for {@link XmlValue}, so a value can be validated on its own — when it arrives from a store or a queue rather than from {@link parseXml},\n * and the schema it belongs to is not in hand.\n *\n * @example\n * ```typescript\n * import { Schema } from 'effect';\n * import { XmlValue } from '@endevops/effect-xml-codec';\n *\n * Schema.decodeUnknownSync(XmlValue)({ book: { '@id': '1', title: 'Dune' } }); // => { book: { '@id': '1', title: 'Dune' } }\n * Schema.decodeUnknownSync(XmlValue)({ book: { title: 42 } }); // => throws XmlValue\n * ```;\n */\nexport const XmlValue: Schema.Codec<XmlValue> = Schema.declare<XmlValue>(isXmlValue, {\n identifier: 'XmlValue',\n expected: 'an XML value: character data, an array of them, or a record of them',\n});\n"],"mappings":";;;;;;AAwBA,MAAM,kBAAkB;;;;;;;;AASxB,MAAa,cAAc,UAAsC,MAAM,OAAO,CAAC;;;;;;;;;;;;AAa/E,MAAM,SAAS,OAAgB,UAA2B;CAGxD,IAAI,UAAU,YAAY,KAAK,KAAK,UAAU,SAAS,KAAK,GAAG,OAAO;CACtE,IAAI,UAAU,OAAO,KAAK,KAAK,CAAC,UAAU,gBAAgB,KAAK,GAAG,OAAO;CAEzE,IAAI,QAAQ,iBAAiB,OAAO;CAIpC,OAAO,MAAM,QAAQ,KAAK,IAAI,cAAc,OAAO,KAAK,IAAI,aAAa,OAAO,KAAK;AACvF;;;;;;;;;AAUA,MAAM,iBAAiB,SAAiC,UAA2B;CACjF,KAAK,MAAM,UAAU,SACnB,IAAI,CAAC,MAAM,QAAQ,QAAQ,CAAC,GAAG,OAAO;CAExC,OAAO;AACT;;;;;;;;;;AAWA,MAAM,gBAAgB,OAAe,UAA2B;CAC9D,IAAI,CAAC,UAAU,iBAAiB,KAAK,GAAG,OAAO;CAC/C,KAAK,MAAM,OAAO,OAAO,KAAK,KAAK,GACjC,IAAI,CAAC,MAAM,MAAM,MAAM,QAAQ,CAAC,GAAG,OAAO;CAE5C,OAAO;AACT;;;;;;;;;;;;;;AAeA,MAAa,WAAmC,OAAO,QAAkB,YAAY;CACnF,YAAY;CACZ,UAAU;AACZ,CAAC"}
|