@endevops/effect-codec-xml 0.0.1 → 0.1.0-beta.2

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.
Files changed (50) hide show
  1. package/README.md +81 -62
  2. package/dist/codec.d.ts +17 -9
  3. package/dist/codec.d.ts.map +1 -1
  4. package/dist/codec.js +29 -18
  5. package/dist/codec.js.map +1 -1
  6. package/dist/conventions.d.ts +4 -4
  7. package/dist/conventions.js +7 -7
  8. package/dist/conventions.js.map +1 -1
  9. package/dist/entities/entity-decoder.d.ts +33 -33
  10. package/dist/entities/entity-decoder.d.ts.map +1 -1
  11. package/dist/entities/entity-decoder.js +63 -64
  12. package/dist/entities/entity-decoder.js.map +1 -1
  13. package/dist/errors.d.ts +2 -2
  14. package/dist/errors.js +2 -2
  15. package/dist/errors.js.map +1 -1
  16. package/dist/namespaces.js +40 -13
  17. package/dist/namespaces.js.map +1 -1
  18. package/dist/naming.d.ts +6 -6
  19. package/dist/naming.d.ts.map +1 -1
  20. package/dist/naming.js +3 -3
  21. package/dist/naming.js.map +1 -1
  22. package/dist/parse.d.ts +5 -5
  23. package/dist/parse.js +14 -14
  24. package/dist/parse.js.map +1 -1
  25. package/dist/plain-value.js +242 -0
  26. package/dist/plain-value.js.map +1 -0
  27. package/dist/render.d.ts +1 -1
  28. package/dist/render.d.ts.map +1 -1
  29. package/dist/render.js +28 -28
  30. package/dist/render.js.map +1 -1
  31. package/dist/xml-error.d.ts +9 -9
  32. package/dist/xml-error.js +18 -18
  33. package/dist/xml-error.js.map +1 -1
  34. package/dist/xml-value.d.ts +7 -7
  35. package/dist/xml-value.d.ts.map +1 -1
  36. package/dist/xml-value.js +6 -7
  37. package/dist/xml-value.js.map +1 -1
  38. package/package.json +1 -1
  39. package/src/codec.ts +98 -71
  40. package/src/conventions.ts +7 -7
  41. package/src/entities/entity-decoder.ts +98 -99
  42. package/src/errors.ts +3 -3
  43. package/src/index.ts +3 -3
  44. package/src/namespaces.ts +62 -36
  45. package/src/naming.ts +34 -35
  46. package/src/parse.ts +26 -26
  47. package/src/plain-value.ts +312 -0
  48. package/src/render.ts +44 -44
  49. package/src/xml-error.ts +18 -18
  50. package/src/xml-value.ts +10 -11
@@ -1 +1 @@
1
- {"version":3,"file":"namespaces.js","names":[],"sources":["../src/namespaces.ts"],"sourcesContent":["// Namespaces: attributing an element to a namespace URI, and resolving that at\n// the text boundary.\n//\n// A schema describes a value in local names. This module lets a schema node say\n// which namespace its element belongs to, through Effect's own annotations:\n//\n// - `xmlNamespace` is the namespace URI of the element. A field that carries\n// one has its element placed in that namespace; the field's name stays the\n// local name, so the schema does not hard-code a wire prefix.\n// - `xmlPrefix` is the wire prefix to write for that URI. When it is omitted\n// the namespace is written as the default namespace (`xmlns=\"…\"`), and an\n// unprefixed element name is used.\n// - `xmlName` is the wire local name to write when it differs from the schema\n// field's own name. It applies to an element or an attribute, and a colon\n// is not allowed because the prefix comes from `xmlPrefix`.\n// - `xmlAttribute` marks a field as an attribute without the schema key\n// carrying the `@` prefix. A namespaced attribute still needs `xmlPrefix`,\n// because a default namespace does not apply to attributes.\n// - `xmlValue` marks one field as the element's character data, the `#text`\n// value, for an element that also carries attributes or children.\n//\n// The namespace of an element is inherited by its descendants, the way an XML\n// default namespace is. An attribute never inherits: it is in a namespace only\n// when it is annotated with one explicitly, because a default namespace does\n// not apply to attributes.\n//\n// On the way out, each element writes its own declaration when the prefix or\n// default is not already in scope. On the way in, the parser's own declarations\n// are read into scope and every name is resolved to its URI, so a document that\n// binds the same URI to a different prefix still decodes to the same value. The\n// declaration attributes are dropped from the decoded value; they are the\n// codec's to manage, not the schema's.\n//\n// The plan is built per local name, which is what a schema field is. One local\n// name cannot belong to two namespaces in one codec; that is reported when the\n// codec is built rather than guessed at.\n\nimport type { Schema } from 'effect';\n\nimport { Predicate, SchemaAST } from 'effect';\n\nimport type { XmlRecord, XmlValue } from './xml-value.ts';\n\nimport { ATTRIBUTE_PREFIX, isAttributeKey, TEXT_KEY } from './conventions.ts';\n\ndeclare module 'effect/Schema' {\n namespace Annotations {\n interface XmlAnnotations {\n /**\n * @description The namespace URI this schema's element belongs to. Read as the local name on the wire, with `xmlPrefix` choosing the prefix.\n */\n readonly xmlNamespace?: string | undefined;\n\n /**\n * @description The wire prefix to write for {@link xmlNamespace}. Omit it to write the namespace as the default (`xmlns=\"…\"`) with unprefixed element names.\n */\n readonly xmlPrefix?: string | undefined;\n\n /**\n * @description The local name to write for an element or attribute, when it differs from the schema key. A colon is not allowed here; the prefix comes from\n * {@link xmlPrefix}.\n */\n readonly xmlName?: string | undefined;\n\n /**\n * @description Whether this field is an XML attribute rather than a child element. Use it to keep the schema key a plain name instead of carrying the `@`\n * prefix. A namespaced attribute still needs `xmlPrefix`, because a default namespace does not apply to attributes.\n */\n readonly xmlAttribute?: boolean | undefined;\n\n /**\n * @description Whether this field holds the element's character data, the `#text` value, rather than a child element. It has no name, so it cannot be\n * combined with `xmlAttribute`, `xmlName`, or a namespace.\n */\n readonly xmlValue?: boolean | undefined;\n }\n\n interface Annotations extends XmlAnnotations {}\n }\n}\n\n/**\n * @description The annotation key holding an element's namespace URI.\n */\nexport const NAMESPACE_KEY = 'xmlNamespace';\n\n/**\n * @description The annotation key holding the wire prefix for an element's namespace.\n */\nexport const PREFIX_KEY = 'xmlPrefix';\n\n/**\n * @description The annotation key holding the wire local name for an element or attribute.\n */\nexport const NAME_KEY = 'xmlName';\n\n/**\n * @description The annotation key marking a field as an XML attribute.\n */\nexport const ATTRIBUTE_KEY = 'xmlAttribute';\n\n/**\n * @description The annotation key marking a field as the element's character data.\n */\nexport const VALUE_KEY = 'xmlValue';\n\n/**\n * @description An element's namespace: the URI, and the prefix to write it with. An empty prefix is the default namespace.\n */\nexport interface XmlNamespace {\n readonly uri: string;\n readonly prefix: string;\n}\n\n/**\n * @description What one codec needs to place and resolve names: the namespace of every local name in the schema, the root element's namespace, and the reverse\n * lookup from a resolved `(uri, local)` back to the schema's key.\n */\nexport interface NamespacePlan {\n /**\n * @description The namespace of each field, keyed by the field's element path, so the same name nested differently stays apart.\n */\n readonly byKey: ReadonlyMap<string, XmlNamespace>;\n\n /**\n * @description The wire local name of each field that overrides the schema's own name with `xmlName`, keyed by the field's element path.\n */\n readonly nameByKey: ReadonlyMap<string, string>;\n\n /**\n * @description The paths of the fields that `xmlAttribute` marks as attributes but whose names do not carry the `@` prefix.\n */\n readonly attributeKeys: ReadonlySet<string>;\n\n /**\n * @description The value field of each element, keyed by the element's path. The root's path is {@link ROOT_ELEMENT}.\n */\n readonly valueByElement: ReadonlyMap<string, string>;\n\n /**\n * @description The paths of array fields that name their element with `xmlName`, so a single occurrence decodes as a one-member array.\n */\n readonly arrayKeys: ReadonlySet<string>;\n\n /**\n * @description The root element's namespace, or `undefined` when the root is unannotated.\n */\n readonly root: XmlNamespace | undefined;\n\n /**\n * @description The root element's local name from its `xmlName` annotation, or `undefined`.\n */\n readonly rootName: string | undefined;\n\n /**\n * @description The schema key for a resolved name, keyed by kind, `uri`, and local name. Lets a document with any prefix, and an element beside an attribute of\n * the same name, resolve back to the schema.\n */\n readonly byResolved: ReadonlyMap<string, string>;\n}\n\n/**\n * @description The namespace an AST's own annotations declare, or `undefined`. `Schema.optional` and `Schema.suspend` wrap a node without moving its annotation,\n * so both are unwrapped to find the namespace the field actually carries.\n *\n * @param ast - The AST to read.\n *\n * @returns The namespace, or `undefined`.\n */\nconst annotationAt = (ast: SchemaAST.AST, key: string): unknown => {\n const seen = new Set<SchemaAST.AST>();\n const find = (node: SchemaAST.AST): unknown => {\n if (seen.has(node)) return undefined;\n seen.add(node);\n const value = SchemaAST.resolve(node)?.[key];\n if (value !== undefined) return value;\n if (node._tag === 'Union') {\n for (const member of node.types) {\n const found = find(member);\n if (found !== undefined) return found;\n }\n return undefined;\n }\n return node._tag === 'Suspend' ? find(node.thunk()) : undefined;\n };\n return find(ast);\n};\n\n/**\n * @description The namespace an AST's own annotations declare, or `undefined`. `Schema.optional` and `Schema.suspend` wrap a node without moving its annotation,\n * so both are unwrapped to find the namespace the field carries.\n *\n * @param ast - The AST to read.\n *\n * @returns The namespace, or `undefined`.\n */\nconst namespaceOf = (ast: SchemaAST.AST): XmlNamespace | undefined => {\n const uri = annotationAt(ast, NAMESPACE_KEY);\n if (!Predicate.isString(uri)) return undefined;\n const prefix = annotationAt(ast, PREFIX_KEY);\n return { uri, prefix: Predicate.isString(prefix) ? prefix : '' };\n};\n\n/**\n * @description The wire local name an AST's own annotations declare, or `undefined`.\n *\n * @param ast - The AST to read.\n *\n * @returns The local name, or `undefined`.\n */\nconst nameOf = (ast: SchemaAST.AST): string | undefined => {\n const name = annotationAt(ast, NAME_KEY);\n return Predicate.isString(name) ? name : undefined;\n};\n\n/**\n * @description The namespace a property carries when its annotation was attached with `Schema.annotateKey` rather than to the field's schema.\n *\n * @param ast - The property's value AST, whose context holds the key annotations.\n *\n * @returns The namespace, or `undefined`.\n */\nconst keyNamespaceOf = (ast: SchemaAST.AST): XmlNamespace | undefined => {\n const annotations = ast.context?.annotations;\n const uri = annotations?.[NAMESPACE_KEY];\n if (!Predicate.isString(uri)) return undefined;\n const prefix = annotations?.[PREFIX_KEY];\n return { uri, prefix: Predicate.isString(prefix) ? prefix : '' };\n};\n\n/**\n * @description The wire local name a property's key annotations declare, or `undefined`.\n *\n * @param ast - The property's value AST, whose context holds the key annotations.\n *\n * @returns The local name, or `undefined`.\n */\nconst keyNameOf = (ast: SchemaAST.AST): string | undefined => {\n const name = ast.context?.annotations?.[NAME_KEY];\n return Predicate.isString(name) ? name : undefined;\n};\n\n/**\n * @description Whether an AST's own annotations mark the field as an XML attribute.\n *\n * @param ast - The AST to read.\n *\n * @returns Whether the annotation is set.\n */\nconst attributeOf = (ast: SchemaAST.AST): boolean => annotationAt(ast, ATTRIBUTE_KEY) === true;\n\n/**\n * @description Whether a property's key annotations mark the field as an XML attribute.\n *\n * @param ast - The property's value AST, whose context holds the key annotations.\n *\n * @returns Whether the annotation is set.\n */\nconst keyAttributeOf = (ast: SchemaAST.AST): boolean => ast.context?.annotations?.[ATTRIBUTE_KEY] === true;\n\n/**\n * @description Whether an AST's own annotations mark the field as the element's character data.\n *\n * @param ast - The AST to read.\n *\n * @returns Whether the annotation is set.\n */\nconst valueOf = (ast: SchemaAST.AST): boolean => annotationAt(ast, VALUE_KEY) === true;\n\n/**\n * @description Whether a property's key annotations mark the field as the element's character data.\n *\n * @param ast - The property's value AST, whose context holds the key annotations.\n *\n * @returns Whether the annotation is set.\n */\nconst keyValueOf = (ast: SchemaAST.AST): boolean => ast.context?.annotations?.[VALUE_KEY] === true;\n\n/**\n * @description Whether a struct property holds the element's character data, marked with `xmlValue` on its schema or its key.\n *\n * @param ast - The property's value AST.\n *\n * @returns Whether the property is the value.\n */\nconst isValueProperty = (ast: SchemaAST.AST): boolean => keyValueOf(ast) || valueOf(ast);\n\n/**\n * @description The local name a schema key names, with the attribute prefix removed. This is what a declaration resolves to.\n *\n * @param key - The schema key.\n *\n * @returns The local name.\n */\nconst localName = (key: string): string => (isAttributeKey(key) ? key.slice(ATTRIBUTE_PREFIX.length) : key);\n\n/**\n * @description The path of the root element. A plan is keyed by the path of the element a field belongs to, so the same name nested differently stays apart. The\n * path is the schema keys from the root joined by {@link PATH_SEPARATOR}.\n */\nexport const ROOT_ELEMENT = '';\n\n/**\n * @description The separator between the schema keys in an element path. A NUL is not a legal XML name character, so it cannot collide with a key.\n */\nconst PATH_SEPARATOR = '\\u0000';\n\n/**\n * @description The path of a child element, appended to its parent's path.\n *\n * @param parent - The parent element's path.\n * @param key - The child's schema key.\n *\n * @returns The child's path.\n */\nconst childPath = (parent: string, key: string): string => (parent === ROOT_ELEMENT ? key : `${parent}${PATH_SEPARATOR}${key}`);\n\n/**\n * @description The schema key at the end of an element path.\n *\n * @param path - The element path.\n *\n * @returns The schema key of the element itself.\n */\nconst pathKey = (path: string): string => {\n const at = path.lastIndexOf(PATH_SEPARATOR);\n return at === -1 ? path : path.slice(at + 1);\n};\n\n/**\n * @description Whether a field is an attribute: the key at the end of its path carries the `@` prefix, or `xmlAttribute` marks the field.\n *\n * @param plan - The namespace plan.\n * @param path - The field's element path.\n *\n * @returns Whether the field is an attribute.\n */\nconst isAttributeOf = (plan: NamespacePlan, path: string): boolean => isAttributeKey(pathKey(path)) || plan.attributeKeys.has(path);\n\n/**\n * @description The wire local name of a field: its `xmlName` override, or the key at the end of its path with the attribute prefix removed.\n *\n * @param plan - The namespace plan.\n * @param path - The field's element path.\n *\n * @returns The local name.\n */\nconst localOf = (plan: NamespacePlan, path: string): string => plan.nameByKey.get(path) ?? localName(pathKey(path));\n\n/**\n * @description The reverse-lookup key for a resolved wire name. The attribute flag is part of it, so an element and an attribute of the same local name in the\n * same namespace stay distinct.\n *\n * @param isAttribute - Whether the name is an attribute.\n * @param uri - The resolved namespace URI, or `undefined`.\n * @param local - The resolved local name.\n *\n * @returns The lookup key.\n */\nconst resolvedKey = (isAttribute: boolean, uri: string | undefined, local: string): string =>\n `${isAttribute ? ATTRIBUTE_PREFIX : ''}${uri ?? ''}|${local}`;\n\n/**\n * @description The path of the parent element, one segment shorter than the field's own path.\n *\n * @param path - The field's element path.\n *\n * @returns The parent element's path, or the root sentinel.\n */\nconst parentPath = (path: string): string => {\n const at = path.lastIndexOf(PATH_SEPARATOR);\n return at === -1 ? ROOT_ELEMENT : path.slice(0, at);\n};\n\n/**\n * @description The reverse-lookup key for a wire name under one parent element. The parent's path is part of it, so the same wire name under two parents resolves\n * to each parent's own field.\n *\n * @param parent - The parent element's path.\n * @param resolved - A name from {@link resolvedKey}.\n *\n * @returns The lookup key.\n */\nconst lookupKey = (parent: string, resolved: string): string => `${parent}\\u0001${resolved}`;\n\n/**\n * @description The mutable state one namespace scan carries: the plan under construction, the local names that resolve to more than one namespace, and the AST\n * nodes already visited so a recursive schema terminates.\n */\ninterface Scan {\n readonly byKey: Map<string, XmlNamespace>;\n readonly nameByKey: Map<string, string>;\n readonly attributeKeys: Set<string>;\n readonly valueByElement: Map<string, string>;\n readonly arrayKeys: Set<string>;\n readonly problems: Array<string>;\n /**\n * @description The AST nodes on the current scan path. A recursive schema terminates because the `Suspend` node is still on the path when its thunk is reached,\n * and a schema reused under two sibling paths is scanned once per path because each node is removed again on the way out.\n */\n readonly seen: Set<SchemaAST.AST>;\n}\n\n/**\n * @description Records a field's namespace, reporting a local name that would belong to two namespaces at once.\n *\n * @param scan - The scan state.\n * @param path - The field's element path.\n * @param namespace - The namespace, or `undefined` when the field has none.\n */\nconst record = (scan: Scan, path: string, namespace: XmlNamespace | undefined): void => {\n if (Predicate.isUndefined(namespace)) {\n return;\n }\n\n const previous = scan.byKey.get(path);\n if (Predicate.isNotUndefined(previous) && (previous.uri !== namespace.uri || previous.prefix !== namespace.prefix)) {\n scan.problems.push(\n `the local name \"${pathKey(path)}\" belongs to more than one namespace (${previous.uri}:${previous.prefix} vs ${namespace.uri}:${namespace.prefix})`\n );\n } else {\n scan.byKey.set(path, namespace);\n }\n};\n\n/**\n * @description Records a field's `xmlName` override, refusing one that carries a prefix because the prefix comes from `xmlPrefix`.\n *\n * @param scan - The scan state.\n * @param path - The field's element path.\n * @param name - The annotated local name, or `undefined`.\n */\nconst recordName = (scan: Scan, path: string, name: string | undefined): void => {\n if (Predicate.isUndefined(name)) return;\n\n if (name.includes(':')) {\n scan.problems.push(`xmlName \"${name}\" on \"${pathKey(path)}\" must be a local name; use xmlPrefix for the prefix`);\n } else {\n scan.nameByKey.set(path, name);\n }\n};\n\n/**\n * @description Records a field's namespace, refusing the two annotations that cannot be honored: a namespaced field whose name already carries a prefix, and a\n * namespaced attribute without a prefix, because a default namespace does not apply to attributes.\n *\n * @param scan - The scan state.\n * @param path - The field's element path.\n * @param isAttribute - Whether the field is an attribute.\n * @param field - The field's own namespace, or `undefined`.\n * @param inherited - The namespace the enclosing element passes down.\n */\nconst recordFieldNamespace = (\n scan: Scan,\n path: string,\n isAttribute: boolean,\n field: XmlNamespace | undefined,\n inherited: XmlNamespace | undefined\n): void => {\n const key = pathKey(path);\n if (Predicate.isNotUndefined(field) && key.includes(':')) {\n scan.problems.push(`\"${key}\" already carries a prefix, so it cannot also carry a namespace annotation`);\n } else if (isAttribute && Predicate.isNotUndefined(field) && field.prefix === '') {\n scan.problems.push(`the attribute \"${key}\" needs xmlPrefix, because a default namespace does not apply to attributes`);\n } else {\n record(scan, path, isAttribute ? field : (field ?? inherited));\n }\n};\n\n/**\n * @description Whether a struct property is an attribute: its key carries the `@` prefix, or its own or key annotation marks it.\n *\n * @param key - The schema key.\n * @param ast - The property's value AST.\n *\n * @returns Whether the property is an attribute.\n */\nconst isAttributeProperty = (key: string, ast: SchemaAST.AST): boolean => isAttributeKey(key) || keyAttributeOf(ast) || attributeOf(ast);\n\n/**\n * @description Notes a field that `xmlAttribute` marks as an attribute but whose name does not carry the `@` prefix.\n *\n * @param scan - The scan state.\n * @param path - The field's element path.\n * @param isAttribute - Whether the field is an attribute.\n */\nconst noteAttribute = (scan: Scan, path: string, isAttribute: boolean): void => {\n if (isAttribute && !isAttributeKey(pathKey(path))) {\n scan.attributeKeys.add(path);\n }\n};\n\n/**\n * @description Records a field marked `xmlValue` against the element that owns it, refusing the annotations a value cannot combine with: character data has no\n * name and no namespace, and an element has room for only one value.\n *\n * @param scan - The scan state.\n * @param elementPath - The path of the element the field belongs to, or the root sentinel.\n * @param valueKey - The schema key of the value field.\n * @param isAttribute - Whether the field is an attribute.\n * @param name - The field's `xmlName` override, or `undefined`.\n * @param field - The field's own namespace, or `undefined`.\n */\nconst recordValue = (\n scan: Scan,\n elementPath: string,\n valueKey: string,\n isAttribute: boolean,\n name: string | undefined,\n field: XmlNamespace | undefined\n): void => {\n if (isAttribute) scan.problems.push(`the field \"${valueKey}\" is marked as both an attribute and the element's value`);\n else if (Predicate.isNotUndefined(name))\n scan.problems.push(`the value field \"${valueKey}\" cannot have an xmlName, because character data has no name`);\n else if (Predicate.isNotUndefined(field))\n scan.problems.push(`the value field \"${valueKey}\" cannot have a namespace, because character data has none`);\n else {\n const previous = scan.valueByElement.get(elementPath);\n if (Predicate.isNotUndefined(previous) && previous !== valueKey) {\n scan.problems.push(\n `the element \"${elementPath === ROOT_ELEMENT ? 'root' : elementPath}\" has more than one value field, \"${previous}\" and \"${valueKey}\"`\n );\n } else {\n scan.valueByElement.set(elementPath, valueKey);\n }\n }\n};\n\n/**\n * @description Records one struct property: its `xmlName`, its namespace, and the namespace its descendants inherit. An attribute carries a namespace only when\n * annotated; an element field falls back to the namespace it inherits. A value field is character data, so it is recorded against its element\n * instead.\n *\n * @param scan - The scan state.\n * @param property - The property signature.\n * @param inherited - The namespace the enclosing element passes down.\n * @param elementPath - The path of the element the property belongs to, or the root sentinel.\n */\nconst scanProperty = (scan: Scan, property: SchemaAST.PropertySignature, inherited: XmlNamespace | undefined, elementPath: string): void => {\n const key = Predicate.isString(property.name) ? property.name : String(property.name);\n const path = childPath(elementPath, key);\n const isAttribute = isAttributeProperty(key, property.type);\n const isValue = isValueProperty(property.type);\n const name = keyNameOf(property.type) ?? nameOf(property.type);\n const field = keyNamespaceOf(property.type) ?? namespaceOf(property.type);\n if (isValue) recordValue(scan, elementPath, key, isAttribute, name, field);\n else {\n noteAttribute(scan, path, isAttribute);\n recordName(scan, path, name);\n recordFieldNamespace(scan, path, isAttribute, field, inherited);\n }\n scanNode(scan, property.type, isAttribute ? inherited : (field ?? inherited), path);\n};\n\n/**\n * @description Walks a list of child ASTs under one inherited namespace.\n *\n * @param scan - The scan state.\n * @param nodes - The child ASTs.\n * @param inherited - The namespace they inherit.\n * @param elementPath - The path of the element they belong to, or the root sentinel.\n */\nconst scanAll = (scan: Scan, nodes: ReadonlyArray<SchemaAST.AST>, inherited: XmlNamespace | undefined, elementPath: string): void => {\n for (const node of nodes) scanNode(scan, node, inherited, elementPath);\n};\n\n/**\n * @description Records the names in one object node: its properties and its index signatures, each under the namespace the object passes down.\n *\n * @param scan - The scan state.\n * @param ast - The object AST.\n * @param namespace - The namespace the object passes to its members.\n * @param elementPath - The path of the element the object describes, or the root sentinel.\n */\nconst scanObject = (scan: Scan, ast: SchemaAST.Objects, namespace: XmlNamespace | undefined, elementPath: string): void => {\n for (const property of ast.propertySignatures) {\n scanProperty(scan, property, namespace, elementPath);\n }\n for (const index of ast.indexSignatures) {\n scanNode(scan, index.type, namespace, elementPath);\n }\n};\n\n/**\n * @description Notes an array field that names its element with `xmlName`, so the decoder reads a single occurrence as a one-member array. An unnamed array keeps\n * the ambiguity and does not wrap.\n *\n * @param scan - The scan state.\n * @param ast - The AST being scanned.\n * @param elementPath - The path of the field the AST describes.\n */\nconst noteArray = (scan: Scan, ast: SchemaAST.AST, elementPath: string): void => {\n if (ast._tag === 'Arrays' && scan.nameByKey.has(elementPath)) {\n scan.arrayKeys.add(elementPath);\n }\n};\n\n/**\n * @description Records every name in one schema AST, carrying the namespace an element passes to its descendants and the element the names belong to.\n *\n * @param scan - The scan state.\n * @param ast - The AST to walk.\n * @param inherited - The namespace the enclosing element passes down.\n * @param elementPath - The path of the element this AST describes, or the root sentinel.\n */\nconst scanNode = (scan: Scan, ast: SchemaAST.AST, inherited: XmlNamespace | undefined, elementPath: string): void => {\n if (scan.seen.has(ast)) return;\n scan.seen.add(ast);\n try {\n noteArray(scan, ast, elementPath);\n const namespace = namespaceOf(ast) ?? inherited;\n switch (ast._tag) {\n case 'Objects':\n scanObject(scan, ast, namespace, elementPath);\n return;\n case 'Arrays':\n scanAll(scan, [...ast.elements, ...ast.rest], namespace, elementPath);\n return;\n case 'Union':\n scanAll(scan, ast.types, namespace, elementPath);\n return;\n case 'Suspend':\n scanNode(scan, ast.thunk(), namespace, elementPath);\n return;\n case 'Declaration':\n scanAll(scan, ast.typeParameters, namespace, elementPath);\n return;\n default:\n return;\n }\n } finally {\n scan.seen.delete(ast);\n }\n};\n\n/**\n * @description Collects the namespace and name of every field in a schema. A namespace is inherited by descendant elements, the way a default namespace is, and an\n * element field records its own namespace, so encode and decode can find it by the local name alone.\n *\n * @param schema - The schema to walk.\n *\n * @returns The plan, or the annotations that cannot be honored.\n */\nexport const namespacePlan = (schema: Schema.Constraint): { readonly plan: NamespacePlan } | { readonly error: string } => {\n const scan: Scan = {\n byKey: new Map(),\n nameByKey: new Map(),\n attributeKeys: new Set(),\n valueByElement: new Map(),\n arrayKeys: new Set(),\n problems: [],\n seen: new Set(),\n };\n scanNode(scan, schema.ast, undefined, ROOT_ELEMENT);\n\n const byResolved = new Map<string, string>();\n for (const path of new Set([...scan.byKey.keys(), ...scan.nameByKey.keys(), ...scan.attributeKeys])) {\n const key = pathKey(path);\n const namespace = scan.byKey.get(path);\n const isAttribute = isAttributeKey(key) || scan.attributeKeys.has(path);\n const local = scan.nameByKey.get(path) ?? localName(key);\n const resolved = lookupKey(parentPath(path), resolvedKey(isAttribute, namespace?.uri, local));\n const previous = byResolved.get(resolved);\n\n if (previous !== undefined && previous !== key) {\n scan.problems.push(`\"${previous}\" and \"${key}\" both resolve to \"${local}\" under the same element`);\n } else {\n byResolved.set(resolved, key);\n }\n }\n\n if (scan.problems.length > 0) {\n return { error: [...new Set(scan.problems)].join(';\\n\\t- ') };\n }\n\n return {\n plan: {\n byKey: scan.byKey,\n nameByKey: scan.nameByKey,\n attributeKeys: scan.attributeKeys,\n valueByElement: scan.valueByElement,\n arrayKeys: scan.arrayKeys,\n root: namespaceOf(schema.ast),\n rootName: nameOf(schema.ast),\n byResolved,\n },\n };\n};\n\n/**\n * @description Whether a key is a namespace declaration the codec manages: `@xmlns` or `@xmlns:prefix`.\n *\n * @param key - The record key.\n *\n * @returns Whether the key is a declaration.\n */\nconst isDeclarationKey = (key: string): boolean => key === `${ATTRIBUTE_PREFIX}xmlns` || key.startsWith(`${ATTRIBUTE_PREFIX}xmlns:`);\n\n/**\n * @description The prefix a declaration key carries, or `''` for the default namespace.\n *\n * @param key - A key {@link isDeclarationKey} accepted.\n *\n * @returns The prefix.\n */\nconst declarationPrefix = (key: string): string => (key === `${ATTRIBUTE_PREFIX}xmlns` ? '' : key.slice(`${ATTRIBUTE_PREFIX}xmlns:`.length));\n\n/**\n * @description Writes the declaration for a namespace into an element's record, when the scope does not already bind it. A prefixed namespace is declared as\n * `xmlns:prefix`; the default namespace as `xmlns`; and an element that leaves a default namespace in scope for no namespace of its own clears it\n * with `xmlns=\"\"`.\n *\n * @param out - The element's record, written in place.\n * @param scope - The in-scope prefix bindings, updated in place.\n * @param namespace - The element's namespace, or `undefined`.\n */\nconst declare = (out: Record<string, XmlValue>, scope: Record<string, string | undefined>, namespace: XmlNamespace | undefined): void => {\n if (Predicate.isNotUndefined(namespace) && namespace.prefix !== '') {\n if (scope[namespace.prefix] === namespace.uri) return;\n out[`${ATTRIBUTE_PREFIX}xmlns:${namespace.prefix}`] = namespace.uri;\n scope[namespace.prefix] = namespace.uri;\n return;\n }\n\n if (!Predicate.isUndefined(namespace)) {\n if (scope[''] === namespace.uri) return;\n out[`${ATTRIBUTE_PREFIX}xmlns`] = namespace.uri;\n scope[''] = namespace.uri;\n return;\n }\n\n if (Predicate.isUndefined(scope[''])) return;\n\n out[`${ATTRIBUTE_PREFIX}xmlns`] = '';\n scope[''] = undefined;\n};\n\n/**\n * @description The wire key for one field: the local name with the prefix its namespace declares, or the local name unchanged when there is no prefix.\n *\n * @param plan - The namespace plan.\n * @param path - The field's element path.\n * @param namespace - The field's namespace, or `undefined`.\n *\n * @returns The key to write.\n */\nconst wireKey = (plan: NamespacePlan, path: string, namespace: XmlNamespace | undefined): string => {\n const isAttribute = isAttributeOf(plan, path);\n const local = localOf(plan, path);\n\n if (Predicate.isUndefined(namespace) || namespace.prefix === '') {\n return isAttribute ? `${ATTRIBUTE_PREFIX}${local}` : local;\n }\n\n return `${isAttribute ? ATTRIBUTE_PREFIX : ''}${namespace.prefix}:${local}`;\n};\n\n/**\n * @description Writes one record's fields under the element's scope: an attribute is a leaf and declares its prefix on this element, the value field becomes the\n * element's character data, and a child element recurses with its own namespace.\n *\n * @param value - The element's record, keyed by the schema's local names.\n * @param plan - The namespace plan.\n * @param out - The wire record, written in place.\n * @param scope - The prefix bindings in scope for this element.\n * @param elementPath - The path of this element, or the root sentinel.\n */\nconst encodeFields = (\n value: XmlRecord,\n plan: NamespacePlan,\n out: Record<string, XmlValue>,\n scope: Record<string, string | undefined>,\n elementPath: string\n): void => {\n const valueKey = plan.valueByElement.get(elementPath);\n for (const [key, child] of Object.entries(value)) {\n if (isDeclarationKey(key)) {\n continue;\n }\n\n if (key === valueKey) {\n out[TEXT_KEY] = child;\n continue;\n }\n\n const path = childPath(elementPath, key);\n const childNamespace = plan.byKey.get(path);\n if (isAttributeOf(plan, path)) {\n if (Predicate.isNotUndefined(childNamespace)) {\n declare(out, scope, childNamespace);\n }\n out[wireKey(plan, path, childNamespace)] = child;\n continue;\n }\n\n out[wireKey(plan, path, childNamespace)] = encodeNames(child, plan, childNamespace, scope, path);\n }\n};\n\n/**\n * @description Rewrites a value tree into its namespaced wire form: every field with a plan entry gets its prefix, and every element declares the namespace its\n * subtree uses. A leaf that has to carry a declaration is wrapped as a `#text` record so the attribute has somewhere to live.\n *\n * @param value - The value tree, keyed by the schema's local names.\n * @param plan - The namespace plan.\n * @param namespace - This element's namespace.\n * @param scope - The prefix bindings in scope above this element.\n * @param elementPath - The path of this element, or the root sentinel.\n *\n * @returns The wire tree.\n */\nexport const encodeNames = (\n value: XmlValue,\n plan: NamespacePlan,\n namespace: XmlNamespace | undefined,\n scope: Record<string, string | undefined>,\n elementPath: string\n): XmlValue => {\n if (Array.isArray(value)) {\n return value.map(member => encodeNames(member, plan, namespace, scope, elementPath));\n }\n\n const out: Record<string, XmlValue> = {};\n const inner = { ...scope };\n declare(out, inner, namespace);\n\n if (Predicate.isUndefined(value)) {\n return undefined;\n }\n if (Predicate.isString(value)) {\n return Object.keys(out).length > 0 ? { ...out, [TEXT_KEY]: value } : value;\n }\n // `Predicate.isObject` narrows to a generic index signature, so the value\n // tree's own record type is named here.\n if (!Predicate.isObject(value)) {\n return value;\n }\n\n encodeFields(value as XmlRecord, plan, out, inner, elementPath);\n return out;\n};\n\n/**\n * @description Resolves a wire name to its URI and local name against the in-scope bindings. An unprefixed element takes the default namespace; an unprefixed\n * attribute has no namespace, because a default namespace does not apply to attributes.\n *\n * @param name - The name as written, without the attribute prefix.\n * @param isAttribute - Whether the name belongs to an attribute.\n * @param scope - The in-scope prefix bindings.\n *\n * @returns The resolved URI (possibly `undefined`) and local name.\n */\nconst resolveName = (\n name: string,\n isAttribute: boolean,\n scope: Record<string, string | undefined>\n): { readonly uri: string | undefined; readonly local: string } => {\n const colon = name.indexOf(':');\n if (colon === -1) return { uri: isAttribute ? undefined : scope[''], local: name };\n return { uri: scope[name.slice(0, colon)], local: name.slice(colon + 1) };\n};\n\n/**\n * @description Reads an element's declarations into a fresh scope that falls back to the enclosing one.\n *\n * @param value - The element's record.\n * @param scope - The bindings in scope above the element.\n *\n * @returns The element's own scope.\n */\nconst scopeOf = (value: XmlRecord, scope: Record<string, string | undefined>): Record<string, string | undefined> => {\n const inner = { ...scope };\n\n for (const [key, declaration] of Object.entries(value)) {\n if (isDeclarationKey(key) && Predicate.isString(declaration)) {\n inner[declarationPrefix(key)] = declaration;\n }\n }\n\n return inner;\n};\n\n/**\n * @description The schema key a wire key resolves to under one parent element: the plan's key for its URI and local name, or the local name when the schema left\n * it unannotated.\n *\n * @param plan - The namespace plan.\n * @param parent - The parent element's path.\n * @param key - The wire key.\n * @param scope - The in-scope prefix bindings.\n *\n * @returns The schema key.\n */\nconst schemaKey = (plan: NamespacePlan, parent: string, key: string, scope: Record<string, string | undefined>): string => {\n const isAttribute = isAttributeKey(key);\n const { uri, local } = resolveName(localName(key), isAttribute, scope);\n return plan.byResolved.get(lookupKey(parent, resolvedKey(isAttribute, uri, local))) ?? (isAttribute ? `${ATTRIBUTE_PREFIX}${local}` : local);\n};\n\n/**\n * @description The scope a child element resolves its own name against: the declarations it carries on itself, layered over the parent scope. An element may\n * declare the prefix it uses on the element itself, so its own name is read with those bindings in scope. A repeated element arrives as an array, so\n * the first member stands in for the run — every member describes the same element and carries the same declaration.\n *\n * @param child - The child value.\n * @param scope - The bindings in scope above the child.\n *\n * @returns The scope to resolve the child's own name with.\n */\nconst childScopeOf = (child: XmlValue, scope: Record<string, string | undefined>): Record<string, string | undefined> => {\n if (Array.isArray(child)) {\n return child.length > 0 ? childScopeOf(child[0], scope) : scope;\n }\n return Predicate.isObject(child) ? scopeOf(child as XmlRecord, scope) : scope;\n};\n\n/**\n * @description Rewrites a wire value tree back to the schema's local names, resolving every name against the declarations the document carries and dropping those\n * declarations. Character data maps to the value field of the element it belongs to. A record left holding only character data collapses back to that\n * string, which is how a namespaced leaf stays a `Schema.String`.\n *\n * @param value - The parsed wire tree.\n * @param plan - The namespace plan.\n * @param scope - The prefix bindings in scope above this element.\n * @param elementPath - The path of this element, or the root sentinel.\n *\n * @returns The value tree, keyed by the schema's names.\n */\nexport const decodeNames = (value: XmlValue, plan: NamespacePlan, scope: Record<string, string | undefined>, elementPath: string): XmlValue => {\n if (Array.isArray(value)) {\n return value.map(member => decodeNames(member, plan, scope, elementPath));\n }\n if (Predicate.isString(value) || Predicate.isUndefined(value)) {\n return value;\n }\n if (!Predicate.isObject(value)) {\n return value;\n }\n\n // `Predicate.isObject` narrows to a generic index signature, so the value\n // tree's own record type is named here.\n const record = value as XmlRecord;\n const inner = scopeOf(record, scope);\n const valueKey = plan.valueByElement.get(elementPath);\n const out: Record<string, XmlValue> = {};\n\n for (const [key, child] of Object.entries(record)) {\n if (isDeclarationKey(key)) {\n continue;\n }\n\n if (key === TEXT_KEY) {\n out[valueKey ?? TEXT_KEY] = decodeNames(child, plan, inner, elementPath);\n continue;\n }\n\n const childKey = schemaKey(plan, elementPath, key, childScopeOf(child, inner));\n const path = childPath(elementPath, childKey);\n const decoded = decodeNames(child, plan, inner, path);\n out[childKey] = plan.arrayKeys.has(path) && !Array.isArray(decoded) ? [decoded] : decoded;\n }\n\n const keys = Object.keys(out);\n if (keys.length === 1 && keys[0] === TEXT_KEY) {\n return out[TEXT_KEY];\n }\n return out;\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"namespaces.js","names":[],"sources":["../src/namespaces.ts"],"sourcesContent":["// Namespaces: attributing an element to a namespace URI, and resolving that at\n// the text boundary.\n//\n// A schema describes a value in local names. This module lets a schema node say\n// which namespace its element belongs to, through Effect's own annotations:\n//\n// - `xmlNamespace` is the namespace URI of the element. A field that carries\n// one has its element placed in that namespace; the field's name stays the\n// local name, so the schema does not hard-code a wire prefix.\n// - `xmlPrefix` is the wire prefix to write for that URI. When it is omitted\n// the namespace is written as the default namespace (`xmlns=\"…\"`), and an\n// unprefixed element name is used.\n// - `xmlName` is the wire local name to write when it differs from the schema\n// field's own name. It applies to an element or an attribute, and a colon\n// is not allowed because the prefix comes from `xmlPrefix`.\n// - `xmlAttribute` marks a field as an attribute without the schema key\n// carrying the `@` prefix. A namespaced attribute still needs `xmlPrefix`,\n// because a default namespace does not apply to attributes.\n// - `xmlValue` marks one field as the element's character data, the `#text`\n// value, for an element that also carries attributes or children.\n//\n// An element's namespace is inherited by its descendants, as an XML default\n// namespace is. An attribute never inherits: it is in a namespace only when it\n// is annotated with one explicitly, because a default namespace does not apply\n// to attributes.\n//\n// On encode, each element writes its own declaration when the prefix or default\n// is not already in scope. On decode, the parser's own declarations are read\n// into scope and every name is resolved to its URI, so a document that binds the\n// same URI to a different prefix still decodes to the same value. The\n// declaration attributes are dropped from the decoded value; the codec manages\n// them, not the schema.\n//\n// The plan is built per local name, and a schema field is one local name. One\n// local name cannot belong to two namespaces in one codec; the plan reports that\n// when the codec is built rather than guessing.\n\nimport type { Schema } from 'effect';\n\nimport { Predicate, SchemaAST } from 'effect';\n\nimport type { XmlRecord, XmlValue } from './xml-value.ts';\n\nimport { ATTRIBUTE_PREFIX, isAttributeKey, TEXT_KEY } from './conventions.ts';\n\ndeclare module 'effect/Schema' {\n namespace Annotations {\n interface XmlAnnotations {\n /**\n * @description The namespace URI this schema's element belongs to. Read as the local name on the wire, with `xmlPrefix` choosing the prefix.\n */\n readonly xmlNamespace?: string | undefined;\n\n /**\n * @description The wire prefix to write for {@link xmlNamespace}. Omit it to write the namespace as the default (`xmlns=\"…\"`) with unprefixed element names.\n */\n readonly xmlPrefix?: string | undefined;\n\n /**\n * @description The local name to write for an element or attribute, when it differs from the schema key. A colon is not allowed here; the prefix comes from\n * {@link xmlPrefix}.\n */\n readonly xmlName?: string | undefined;\n\n /**\n * @description Whether this field is an XML attribute rather than a child element. Use it to keep the schema key a plain name instead of carrying the `@`\n * prefix. A namespaced attribute still needs `xmlPrefix`, because a default namespace does not apply to attributes.\n */\n readonly xmlAttribute?: boolean | undefined;\n\n /**\n * @description Whether this field holds the element's character data, the `#text` value, rather than a child element. It has no name, so it cannot be\n * combined with `xmlAttribute`, `xmlName`, or a namespace.\n */\n readonly xmlValue?: boolean | undefined;\n }\n\n interface Annotations extends XmlAnnotations {}\n }\n}\n\n/**\n * @description The annotation key holding an element's namespace URI.\n */\nexport const NAMESPACE_KEY = 'xmlNamespace';\n\n/**\n * @description The annotation key holding the wire prefix for an element's namespace.\n */\nexport const PREFIX_KEY = 'xmlPrefix';\n\n/**\n * @description The annotation key holding the wire local name for an element or attribute.\n */\nexport const NAME_KEY = 'xmlName';\n\n/**\n * @description The annotation key marking a field as an XML attribute.\n */\nexport const ATTRIBUTE_KEY = 'xmlAttribute';\n\n/**\n * @description The annotation key marking a field as the element's character data.\n */\nexport const VALUE_KEY = 'xmlValue';\n\n/**\n * @description An element's namespace: the URI, and the prefix to write it with. An empty prefix is the default namespace.\n */\nexport interface XmlNamespace {\n readonly uri: string;\n readonly prefix: string;\n}\n\n/**\n * @description What one codec needs to place and resolve names: the namespace of every local name in the schema, the root element's namespace, and the reverse\n * lookup from a resolved `(uri, local)` back to the schema's key.\n */\nexport interface NamespacePlan {\n /**\n * @description The namespace of each field, keyed by the field's element path, so the same name nested differently stays apart.\n */\n readonly byKey: ReadonlyMap<string, XmlNamespace>;\n\n /**\n * @description The wire local name of each field that overrides the schema's own name with `xmlName`, keyed by the field's element path.\n */\n readonly nameByKey: ReadonlyMap<string, string>;\n\n /**\n * @description The paths of the fields that `xmlAttribute` marks as attributes but whose names do not carry the `@` prefix.\n */\n readonly attributeKeys: ReadonlySet<string>;\n\n /**\n * @description The value field of each element, keyed by the element's path. The root's path is {@link ROOT_ELEMENT}.\n */\n readonly valueByElement: ReadonlyMap<string, string>;\n\n /**\n * @description The paths of array fields that name their element with `xmlName`, so a single occurrence decodes as a one-member array.\n */\n readonly arrayKeys: ReadonlySet<string>;\n\n /**\n * @description The root element's namespace, or `undefined` when the root is unannotated.\n */\n readonly root: XmlNamespace | undefined;\n\n /**\n * @description The root element's local name from its `xmlName` annotation, or `undefined`.\n */\n readonly rootName: string | undefined;\n\n /**\n * @description The schema key for a resolved name, keyed by kind, `uri`, and local name. Lets a document with any prefix, and an element beside an attribute of\n * the same name, resolve back to the schema.\n */\n readonly byResolved: ReadonlyMap<string, string>;\n}\n\n/**\n * @description The namespace an AST's own annotations declare, or `undefined`. `Schema.optional` and `Schema.suspend` wrap a node without moving its annotation,\n * so both are unwrapped to find the namespace the field actually carries.\n *\n * @param ast - The AST to read.\n *\n * @returns The namespace, or `undefined`.\n */\nconst annotationAt = (ast: SchemaAST.AST, key: string): unknown => {\n const seen = new Set<SchemaAST.AST>();\n const find = (node: SchemaAST.AST): unknown => {\n if (seen.has(node)) return undefined;\n seen.add(node);\n const value = SchemaAST.resolve(node)?.[key];\n if (value !== undefined) return value;\n if (node._tag === 'Union') {\n for (const member of node.types) {\n const found = find(member);\n if (found !== undefined) return found;\n }\n return undefined;\n }\n return node._tag === 'Suspend' ? find(node.thunk()) : undefined;\n };\n return find(ast);\n};\n\n/**\n * @description The namespace an AST's own annotations declare, or `undefined`. `Schema.optional` and `Schema.suspend` wrap a node without moving its annotation,\n * so both are unwrapped to find the namespace the field carries.\n *\n * @param ast - The AST to read.\n *\n * @returns The namespace, or `undefined`.\n */\nconst namespaceOf = (ast: SchemaAST.AST): XmlNamespace | undefined => {\n const uri = annotationAt(ast, NAMESPACE_KEY);\n if (!Predicate.isString(uri)) return undefined;\n const prefix = annotationAt(ast, PREFIX_KEY);\n return { uri, prefix: Predicate.isString(prefix) ? prefix : '' };\n};\n\n/**\n * @description The wire local name an AST's own annotations declare, or `undefined`.\n *\n * @param ast - The AST to read.\n *\n * @returns The local name, or `undefined`.\n */\nconst nameOf = (ast: SchemaAST.AST): string | undefined => {\n const name = annotationAt(ast, NAME_KEY);\n return Predicate.isString(name) ? name : undefined;\n};\n\n/**\n * @description The namespace a property carries when its annotation was attached with `Schema.annotateKey` rather than to the field's schema.\n *\n * @param ast - The property's value AST, whose context holds the key annotations.\n *\n * @returns The namespace, or `undefined`.\n */\nconst keyNamespaceOf = (ast: SchemaAST.AST): XmlNamespace | undefined => {\n const annotations = ast.context?.annotations;\n const uri = annotations?.[NAMESPACE_KEY];\n if (!Predicate.isString(uri)) return undefined;\n const prefix = annotations?.[PREFIX_KEY];\n return { uri, prefix: Predicate.isString(prefix) ? prefix : '' };\n};\n\n/**\n * @description The wire local name a property's key annotations declare, or `undefined`.\n *\n * @param ast - The property's value AST, whose context holds the key annotations.\n *\n * @returns The local name, or `undefined`.\n */\nconst keyNameOf = (ast: SchemaAST.AST): string | undefined => {\n const name = ast.context?.annotations?.[NAME_KEY];\n return Predicate.isString(name) ? name : undefined;\n};\n\n/**\n * @description Whether an AST's own annotations mark the field as an XML attribute.\n *\n * @param ast - The AST to read.\n *\n * @returns Whether the annotation is set.\n */\nconst attributeOf = (ast: SchemaAST.AST): boolean => annotationAt(ast, ATTRIBUTE_KEY) === true;\n\n/**\n * @description Whether a property's key annotations mark the field as an XML attribute.\n *\n * @param ast - The property's value AST, whose context holds the key annotations.\n *\n * @returns Whether the annotation is set.\n */\nconst keyAttributeOf = (ast: SchemaAST.AST): boolean => ast.context?.annotations?.[ATTRIBUTE_KEY] === true;\n\n/**\n * @description Whether an AST's own annotations mark the field as the element's character data.\n *\n * @param ast - The AST to read.\n *\n * @returns Whether the annotation is set.\n */\nconst valueOf = (ast: SchemaAST.AST): boolean => annotationAt(ast, VALUE_KEY) === true;\n\n/**\n * @description Whether a property's key annotations mark the field as the element's character data.\n *\n * @param ast - The property's value AST, whose context holds the key annotations.\n *\n * @returns Whether the annotation is set.\n */\nconst keyValueOf = (ast: SchemaAST.AST): boolean => ast.context?.annotations?.[VALUE_KEY] === true;\n\n/**\n * @description Whether a struct property holds the element's character data, marked with `xmlValue` on its schema or its key.\n *\n * @param ast - The property's value AST.\n *\n * @returns Whether the property is the value.\n */\nconst isValueProperty = (ast: SchemaAST.AST): boolean => keyValueOf(ast) || valueOf(ast);\n\n/**\n * @description The local name a schema key names, with the attribute prefix removed. This is what a declaration resolves to.\n *\n * @param key - The schema key.\n *\n * @returns The local name.\n */\nconst localName = (key: string): string => (isAttributeKey(key) ? key.slice(ATTRIBUTE_PREFIX.length) : key);\n\n/**\n * @description The path of the root element. A plan is keyed by the path of the element a field belongs to, so the same name nested differently stays apart. The\n * path is the schema keys from the root joined by {@link PATH_SEPARATOR}.\n */\nexport const ROOT_ELEMENT = '';\n\n/**\n * @description The separator between the schema keys in an element path. A NUL is not a legal XML name character, so it cannot collide with a key.\n */\nconst PATH_SEPARATOR = '\\u0000';\n\n/**\n * @description The path of a child element, appended to its parent's path.\n *\n * @param parent - The parent element's path.\n * @param key - The child's schema key.\n *\n * @returns The child's path.\n */\nconst childPath = (parent: string, key: string): string => (parent === ROOT_ELEMENT ? key : `${parent}${PATH_SEPARATOR}${key}`);\n\n/**\n * @description The schema key at the end of an element path.\n *\n * @param path - The element path.\n *\n * @returns The schema key of the element itself.\n */\nconst pathKey = (path: string): string => {\n const at = path.lastIndexOf(PATH_SEPARATOR);\n return at === -1 ? path : path.slice(at + 1);\n};\n\n/**\n * @description Whether a field is an attribute: the key at the end of its path carries the `@` prefix, or `xmlAttribute` marks the field.\n *\n * @param plan - The namespace plan.\n * @param path - The field's element path.\n *\n * @returns Whether the field is an attribute.\n */\nconst isAttributeOf = (plan: NamespacePlan, path: string): boolean => isAttributeKey(pathKey(path)) || plan.attributeKeys.has(path);\n\n/**\n * @description The wire local name of a field: its `xmlName` override, or the key at the end of its path with the attribute prefix removed.\n *\n * @param plan - The namespace plan.\n * @param path - The field's element path.\n *\n * @returns The local name.\n */\nconst localOf = (plan: NamespacePlan, path: string): string => plan.nameByKey.get(path) ?? localName(pathKey(path));\n\n/**\n * @description The reverse-lookup key for a resolved wire name. The attribute flag is part of it, so an element and an attribute of the same local name in the\n * same namespace stay distinct.\n *\n * @param isAttribute - Whether the name is an attribute.\n * @param uri - The resolved namespace URI, or `undefined`.\n * @param local - The resolved local name.\n *\n * @returns The lookup key.\n */\nconst resolvedKey = (isAttribute: boolean, uri: string | undefined, local: string): string =>\n `${isAttribute ? ATTRIBUTE_PREFIX : ''}${uri ?? ''}|${local}`;\n\n/**\n * @description The path of the parent element, one segment shorter than the field's own path.\n *\n * @param path - The field's element path.\n *\n * @returns The parent element's path, or the root sentinel.\n */\nconst parentPath = (path: string): string => {\n const at = path.lastIndexOf(PATH_SEPARATOR);\n return at === -1 ? ROOT_ELEMENT : path.slice(0, at);\n};\n\n/**\n * @description The reverse-lookup key for a wire name under one parent element. The parent's path is part of it, so the same wire name under two parents resolves\n * to each parent's own field.\n *\n * @param parent - The parent element's path.\n * @param resolved - A name from {@link resolvedKey}.\n *\n * @returns The lookup key.\n */\nconst lookupKey = (parent: string, resolved: string): string => `${parent}\\u0001${resolved}`;\n\n/**\n * @description The mutable state one namespace scan carries: the plan under construction, the local names that resolve to more than one namespace, and the AST\n * nodes already visited so a recursive schema terminates.\n */\ninterface Scan {\n readonly byKey: Map<string, XmlNamespace>;\n readonly nameByKey: Map<string, string>;\n readonly attributeKeys: Set<string>;\n readonly valueByElement: Map<string, string>;\n readonly arrayKeys: Set<string>;\n readonly problems: Array<string>;\n /**\n * @description The AST nodes on the current scan path. A recursive schema terminates because the `Suspend` node is still on the path when its thunk is reached,\n * and a schema reused under two sibling paths is scanned once per path because each node is removed again on the way back out.\n */\n readonly seen: Set<SchemaAST.AST>;\n}\n\n/**\n * @description Records a field's namespace, reporting a local name that would belong to two namespaces at once.\n *\n * @param scan - The scan state.\n * @param path - The field's element path.\n * @param namespace - The namespace, or `undefined` when the field has none.\n */\nconst record = (scan: Scan, path: string, namespace: XmlNamespace | undefined): void => {\n if (Predicate.isUndefined(namespace)) {\n return;\n }\n\n const previous = scan.byKey.get(path);\n if (Predicate.isNotUndefined(previous) && (previous.uri !== namespace.uri || previous.prefix !== namespace.prefix)) {\n scan.problems.push(\n `the local name \"${pathKey(path)}\" belongs to more than one namespace (${previous.uri}:${previous.prefix} vs ${namespace.uri}:${namespace.prefix})`\n );\n } else {\n scan.byKey.set(path, namespace);\n }\n};\n\n/**\n * @description Records a field's `xmlName` override, refusing one that carries a prefix because the prefix comes from `xmlPrefix`.\n *\n * @param scan - The scan state.\n * @param path - The field's element path.\n * @param name - The annotated local name, or `undefined`.\n */\nconst recordName = (scan: Scan, path: string, name: string | undefined): void => {\n if (Predicate.isUndefined(name)) return;\n\n if (name.includes(':')) {\n scan.problems.push(`xmlName \"${name}\" on \"${pathKey(path)}\" must be a local name; use xmlPrefix for the prefix`);\n } else {\n scan.nameByKey.set(path, name);\n }\n};\n\n/**\n * @description Records a field's namespace, refusing the two annotations that cannot be honored: a namespaced field whose name already carries a prefix, and a\n * namespaced attribute without a prefix, because a default namespace does not apply to attributes.\n *\n * @param scan - The scan state.\n * @param path - The field's element path.\n * @param isAttribute - Whether the field is an attribute.\n * @param field - The field's own namespace, or `undefined`.\n * @param inherited - The namespace the enclosing element passes down.\n */\nconst recordFieldNamespace = (\n scan: Scan,\n path: string,\n isAttribute: boolean,\n field: XmlNamespace | undefined,\n inherited: XmlNamespace | undefined\n): void => {\n const key = pathKey(path);\n if (Predicate.isNotUndefined(field) && key.includes(':')) {\n scan.problems.push(`\"${key}\" already carries a prefix, so it cannot also carry a namespace annotation`);\n } else if (isAttribute && Predicate.isNotUndefined(field) && field.prefix === '') {\n scan.problems.push(`the attribute \"${key}\" needs xmlPrefix, because a default namespace does not apply to attributes`);\n } else {\n record(scan, path, isAttribute ? field : (field ?? inherited));\n }\n};\n\n/**\n * @description Whether a struct property is an attribute: its key carries the `@` prefix, or its own or key annotation marks it.\n *\n * @param key - The schema key.\n * @param ast - The property's value AST.\n *\n * @returns Whether the property is an attribute.\n */\nconst isAttributeProperty = (key: string, ast: SchemaAST.AST): boolean => isAttributeKey(key) || keyAttributeOf(ast) || attributeOf(ast);\n\n/**\n * @description Notes a field that `xmlAttribute` marks as an attribute but whose name does not carry the `@` prefix.\n *\n * @param scan - The scan state.\n * @param path - The field's element path.\n * @param isAttribute - Whether the field is an attribute.\n */\nconst noteAttribute = (scan: Scan, path: string, isAttribute: boolean): void => {\n if (isAttribute && !isAttributeKey(pathKey(path))) {\n scan.attributeKeys.add(path);\n }\n};\n\n/**\n * @description Records a field marked `xmlValue` against the element that owns it, refusing the annotations a value cannot combine with: character data has no\n * name and no namespace, and an element has room for only one value.\n *\n * @param scan - The scan state.\n * @param elementPath - The path of the element the field belongs to, or the root sentinel.\n * @param valueKey - The schema key of the value field.\n * @param isAttribute - Whether the field is an attribute.\n * @param name - The field's `xmlName` override, or `undefined`.\n * @param field - The field's own namespace, or `undefined`.\n */\nconst recordValue = (\n scan: Scan,\n elementPath: string,\n valueKey: string,\n isAttribute: boolean,\n name: string | undefined,\n field: XmlNamespace | undefined\n): void => {\n if (isAttribute) scan.problems.push(`the field \"${valueKey}\" is marked as both an attribute and the element's value`);\n else if (Predicate.isNotUndefined(name))\n scan.problems.push(`the value field \"${valueKey}\" cannot have an xmlName, because character data has no name`);\n else if (Predicate.isNotUndefined(field))\n scan.problems.push(`the value field \"${valueKey}\" cannot have a namespace, because character data has none`);\n else {\n const previous = scan.valueByElement.get(elementPath);\n if (Predicate.isNotUndefined(previous) && previous !== valueKey) {\n scan.problems.push(\n `the element \"${elementPath === ROOT_ELEMENT ? 'root' : elementPath}\" has more than one value field, \"${previous}\" and \"${valueKey}\"`\n );\n } else {\n scan.valueByElement.set(elementPath, valueKey);\n }\n }\n};\n\n/**\n * @description Records one struct property: its `xmlName`, its namespace, and the namespace its descendants inherit. An attribute carries a namespace only when\n * annotated; an element field falls back to the namespace it inherits. A value field is character data, so it is recorded against its element\n * instead.\n *\n * @param scan - The scan state.\n * @param property - The property signature.\n * @param inherited - The namespace the enclosing element passes down.\n * @param elementPath - The path of the element the property belongs to, or the root sentinel.\n */\nconst scanProperty = (scan: Scan, property: SchemaAST.PropertySignature, inherited: XmlNamespace | undefined, elementPath: string): void => {\n const key = Predicate.isString(property.name) ? property.name : String(property.name);\n const path = childPath(elementPath, key);\n const isAttribute = isAttributeProperty(key, property.type);\n const isValue = isValueProperty(property.type);\n const name = keyNameOf(property.type) ?? nameOf(property.type);\n const field = keyNamespaceOf(property.type) ?? namespaceOf(property.type);\n if (isValue) recordValue(scan, elementPath, key, isAttribute, name, field);\n else {\n noteAttribute(scan, path, isAttribute);\n recordName(scan, path, name);\n recordFieldNamespace(scan, path, isAttribute, field, inherited);\n }\n scanNode(scan, property.type, isAttribute ? inherited : (field ?? inherited), path);\n};\n\n/**\n * @description Walks a list of child ASTs under one inherited namespace.\n *\n * @param scan - The scan state.\n * @param nodes - The child ASTs.\n * @param inherited - The namespace they inherit.\n * @param elementPath - The path of the element they belong to, or the root sentinel.\n */\nconst scanAll = (scan: Scan, nodes: ReadonlyArray<SchemaAST.AST>, inherited: XmlNamespace | undefined, elementPath: string): void => {\n for (const node of nodes) scanNode(scan, node, inherited, elementPath);\n};\n\n/**\n * @description Records the names in one object node: its properties and its index signatures, each under the namespace the object passes down.\n *\n * @param scan - The scan state.\n * @param ast - The object AST.\n * @param namespace - The namespace the object passes to its members.\n * @param elementPath - The path of the element the object describes, or the root sentinel.\n */\nconst scanObject = (scan: Scan, ast: SchemaAST.Objects, namespace: XmlNamespace | undefined, elementPath: string): void => {\n for (const property of ast.propertySignatures) {\n scanProperty(scan, property, namespace, elementPath);\n }\n for (const index of ast.indexSignatures) {\n scanNode(scan, index.type, namespace, elementPath);\n }\n};\n\n/**\n * @description Notes an array field that names its element with `xmlName`, so the decoder reads a single occurrence as a one-member array. An unnamed array keeps\n * the ambiguity and does not wrap.\n *\n * @param scan - The scan state.\n * @param ast - The AST being scanned.\n * @param elementPath - The path of the field the AST describes.\n */\nconst noteArray = (scan: Scan, ast: SchemaAST.AST, elementPath: string): void => {\n if (ast._tag === 'Arrays' && scan.nameByKey.has(elementPath)) {\n scan.arrayKeys.add(elementPath);\n }\n};\n\n/**\n * @description Records every name in one schema AST, carrying the namespace an element passes to its descendants and the element the names belong to.\n *\n * @param scan - The scan state.\n * @param ast - The AST to walk.\n * @param inherited - The namespace the enclosing element passes down.\n * @param elementPath - The path of the element this AST describes, or the root sentinel.\n */\nconst scanNode = (scan: Scan, ast: SchemaAST.AST, inherited: XmlNamespace | undefined, elementPath: string): void => {\n if (scan.seen.has(ast)) return;\n scan.seen.add(ast);\n try {\n noteArray(scan, ast, elementPath);\n const namespace = namespaceOf(ast) ?? inherited;\n switch (ast._tag) {\n case 'Objects':\n scanObject(scan, ast, namespace, elementPath);\n return;\n case 'Arrays':\n scanAll(scan, [...ast.elements, ...ast.rest], namespace, elementPath);\n return;\n case 'Union':\n scanAll(scan, ast.types, namespace, elementPath);\n return;\n case 'Suspend':\n scanNode(scan, ast.thunk(), namespace, elementPath);\n return;\n case 'Declaration':\n scanAll(scan, ast.typeParameters, namespace, elementPath);\n return;\n default:\n return;\n }\n } finally {\n scan.seen.delete(ast);\n }\n};\n\n/**\n * @description Collects the namespace and name of every field in a schema. Descendant elements inherit a namespace, as they inherit a default namespace, and an\n * element field records its own namespace, so encode and decode can find it by the local name alone.\n *\n * @param schema - The schema to walk.\n *\n * @returns The plan, or the annotations that cannot be honored.\n */\nexport const namespacePlan = (schema: Schema.Constraint): { readonly plan: NamespacePlan } | { readonly error: string } => {\n const scan: Scan = {\n byKey: new Map(),\n nameByKey: new Map(),\n attributeKeys: new Set(),\n valueByElement: new Map(),\n arrayKeys: new Set(),\n problems: [],\n seen: new Set(),\n };\n scanNode(scan, schema.ast, undefined, ROOT_ELEMENT);\n\n const byResolved = new Map<string, string>();\n for (const path of new Set([...scan.byKey.keys(), ...scan.nameByKey.keys(), ...scan.attributeKeys])) {\n const key = pathKey(path);\n const namespace = scan.byKey.get(path);\n const isAttribute = isAttributeKey(key) || scan.attributeKeys.has(path);\n const local = scan.nameByKey.get(path) ?? localName(key);\n const resolved = lookupKey(parentPath(path), resolvedKey(isAttribute, namespace?.uri, local));\n const previous = byResolved.get(resolved);\n\n if (previous !== undefined && previous !== key) {\n scan.problems.push(`\"${previous}\" and \"${key}\" both resolve to \"${local}\" under the same element`);\n } else {\n byResolved.set(resolved, key);\n }\n }\n\n if (scan.problems.length > 0) {\n return { error: [...new Set(scan.problems)].join(';\\n\\t- ') };\n }\n\n return {\n plan: {\n byKey: scan.byKey,\n nameByKey: scan.nameByKey,\n attributeKeys: scan.attributeKeys,\n valueByElement: scan.valueByElement,\n arrayKeys: scan.arrayKeys,\n root: namespaceOf(schema.ast),\n rootName: nameOf(schema.ast),\n byResolved,\n },\n };\n};\n\n/**\n * @description Whether a key is a namespace declaration the codec manages: `@xmlns` or `@xmlns:prefix`.\n *\n * @param key - The record key.\n *\n * @returns Whether the key is a declaration.\n */\nconst isDeclarationKey = (key: string): boolean => key === `${ATTRIBUTE_PREFIX}xmlns` || key.startsWith(`${ATTRIBUTE_PREFIX}xmlns:`);\n\n/**\n * @description The prefix a declaration key carries, or `''` for the default namespace.\n *\n * @param key - A key {@link isDeclarationKey} accepted.\n *\n * @returns The prefix.\n */\nconst declarationPrefix = (key: string): string => (key === `${ATTRIBUTE_PREFIX}xmlns` ? '' : key.slice(`${ATTRIBUTE_PREFIX}xmlns:`.length));\n\n/**\n * @description Writes the declaration for a namespace into an element's record, when the scope does not already bind it. A prefixed namespace is declared as\n * `xmlns:prefix`; the default namespace as `xmlns`; and an element that leaves a default namespace in scope for no namespace of its own clears it\n * with `xmlns=\"\"`.\n *\n * @param out - The element's record, written in place.\n * @param scope - The in-scope prefix bindings, updated in place.\n * @param namespace - The element's namespace, or `undefined`.\n */\nconst declare = (out: Record<string, XmlValue>, scope: Record<string, string | undefined>, namespace: XmlNamespace | undefined): void => {\n if (Predicate.isNotUndefined(namespace) && namespace.prefix !== '') {\n if (scope[namespace.prefix] === namespace.uri) return;\n out[`${ATTRIBUTE_PREFIX}xmlns:${namespace.prefix}`] = namespace.uri;\n scope[namespace.prefix] = namespace.uri;\n return;\n }\n\n if (!Predicate.isUndefined(namespace)) {\n if (scope[''] === namespace.uri) return;\n out[`${ATTRIBUTE_PREFIX}xmlns`] = namespace.uri;\n scope[''] = namespace.uri;\n return;\n }\n\n if (Predicate.isUndefined(scope[''])) return;\n\n out[`${ATTRIBUTE_PREFIX}xmlns`] = '';\n scope[''] = undefined;\n};\n\n/**\n * @description The wire key for one field: the local name with the prefix its namespace declares, or the local name unchanged when there is no prefix.\n *\n * @param plan - The namespace plan.\n * @param path - The field's element path.\n * @param namespace - The field's namespace, or `undefined`.\n *\n * @returns The key to write.\n */\nconst wireKey = (plan: NamespacePlan, path: string, namespace: XmlNamespace | undefined): string => {\n const isAttribute = isAttributeOf(plan, path);\n const local = localOf(plan, path);\n\n if (Predicate.isUndefined(namespace) || namespace.prefix === '') {\n return isAttribute ? `${ATTRIBUTE_PREFIX}${local}` : local;\n }\n\n return `${isAttribute ? ATTRIBUTE_PREFIX : ''}${namespace.prefix}:${local}`;\n};\n\n/**\n * @description Writes one record's fields under the element's scope: an attribute is a leaf and declares its prefix on this element, the value field becomes the\n * element's character data, and a child element recurses with its own namespace.\n *\n * @param value - The element's record, keyed by the schema's local names.\n * @param plan - The namespace plan.\n * @param out - The wire record, written in place.\n * @param scope - The prefix bindings in scope for this element.\n * @param elementPath - The path of this element, or the root sentinel.\n */\nconst encodeFields = (\n value: XmlRecord,\n plan: NamespacePlan,\n out: Record<string, XmlValue>,\n scope: Record<string, string | undefined>,\n elementPath: string\n): void => {\n const valueKey = plan.valueByElement.get(elementPath);\n for (const [key, child] of Object.entries(value)) {\n if (isDeclarationKey(key)) {\n continue;\n }\n\n if (key === valueKey) {\n out[TEXT_KEY] = child;\n continue;\n }\n\n const path = childPath(elementPath, key);\n const childNamespace = plan.byKey.get(path);\n if (isAttributeOf(plan, path)) {\n if (Predicate.isNotUndefined(childNamespace)) {\n declare(out, scope, childNamespace);\n }\n out[wireKey(plan, path, childNamespace)] = child;\n continue;\n }\n\n out[wireKey(plan, path, childNamespace)] = encodeNames(child, plan, childNamespace, scope, path);\n }\n};\n\n/**\n * @description Rewrites a value tree into its namespaced wire form: every field with a plan entry gets its prefix, and every element declares the namespace its\n * subtree uses. A leaf that has to carry a declaration is wrapped as a `#text` record so the attribute has somewhere to live.\n *\n * @param value - The value tree, keyed by the schema's local names.\n * @param plan - The namespace plan.\n * @param namespace - This element's namespace.\n * @param scope - The prefix bindings in scope above this element.\n * @param elementPath - The path of this element, or the root sentinel.\n *\n * @returns The wire tree.\n */\nexport const encodeNames = (\n value: XmlValue,\n plan: NamespacePlan,\n namespace: XmlNamespace | undefined,\n scope: Record<string, string | undefined>,\n elementPath: string\n): XmlValue => {\n if (Array.isArray(value)) {\n return value.map(member => encodeNames(member, plan, namespace, scope, elementPath));\n }\n\n const out: Record<string, XmlValue> = {};\n const inner = { ...scope };\n declare(out, inner, namespace);\n\n if (Predicate.isUndefined(value)) {\n return undefined;\n }\n if (Predicate.isString(value)) {\n return Object.keys(out).length > 0 ? { ...out, [TEXT_KEY]: value } : value;\n }\n // `Predicate.isObject` narrows to a generic index signature, so the value\n // tree's own record type is named here.\n if (!Predicate.isObject(value)) {\n return value;\n }\n\n encodeFields(value as XmlRecord, plan, out, inner, elementPath);\n return out;\n};\n\n/**\n * @description Resolves a wire name to its URI and local name against the in-scope bindings. An unprefixed element takes the default namespace; an unprefixed\n * attribute has no namespace, because a default namespace does not apply to attributes.\n *\n * @param name - The name as written, without the attribute prefix.\n * @param isAttribute - Whether the name belongs to an attribute.\n * @param scope - The in-scope prefix bindings.\n *\n * @returns The resolved URI (possibly `undefined`) and local name.\n */\nconst resolveName = (\n name: string,\n isAttribute: boolean,\n scope: Record<string, string | undefined>\n): { readonly uri: string | undefined; readonly local: string } => {\n const colon = name.indexOf(':');\n if (colon === -1) return { uri: isAttribute ? undefined : scope[''], local: name };\n return { uri: scope[name.slice(0, colon)], local: name.slice(colon + 1) };\n};\n\n/**\n * @description Reads an element's declarations into a fresh scope that falls back to the enclosing one.\n *\n * @param value - The element's record.\n * @param scope - The bindings in scope above the element.\n *\n * @returns The element's own scope.\n */\nconst scopeOf = (value: XmlRecord, scope: Record<string, string | undefined>): Record<string, string | undefined> => {\n const inner = { ...scope };\n\n for (const [key, declaration] of Object.entries(value)) {\n if (isDeclarationKey(key) && Predicate.isString(declaration)) {\n inner[declarationPrefix(key)] = declaration;\n }\n }\n\n return inner;\n};\n\n/**\n * @description The schema key a wire key resolves to under one parent element: the plan's key for its URI and local name, or the local name when the schema left\n * it unannotated.\n *\n * @param plan - The namespace plan.\n * @param parent - The parent element's path.\n * @param key - The wire key.\n * @param scope - The in-scope prefix bindings.\n *\n * @returns The schema key.\n */\nconst schemaKey = (plan: NamespacePlan, parent: string, key: string, scope: Record<string, string | undefined>): string => {\n const isAttribute = isAttributeKey(key);\n const { uri, local } = resolveName(localName(key), isAttribute, scope);\n return plan.byResolved.get(lookupKey(parent, resolvedKey(isAttribute, uri, local))) ?? (isAttribute ? `${ATTRIBUTE_PREFIX}${local}` : local);\n};\n\n/**\n * @description The scope a child element resolves its own name against: the declarations it carries on itself, layered over the parent scope. An element may\n * declare the prefix it uses on the element itself, so its own name is read with those bindings in scope. A repeated element arrives as an array, so\n * the first member stands in for the run; every member describes the same element and carries the same declaration.\n *\n * @param child - The child value.\n * @param scope - The bindings in scope above the child.\n *\n * @returns The scope to resolve the child's own name with.\n */\nconst childScopeOf = (child: XmlValue, scope: Record<string, string | undefined>): Record<string, string | undefined> => {\n if (Array.isArray(child)) {\n return child.length > 0 ? childScopeOf(child[0], scope) : scope;\n }\n return Predicate.isObject(child) ? scopeOf(child as XmlRecord, scope) : scope;\n};\n\n/**\n * @description Character data read back as a value tree. An element the schema reads as a struct with a value field derives that field from the element's\n * character data, and the parser reduces an element with no attributes and no children to a bare string, so the string is put back under the value's\n * key for that struct to read.\n *\n * @param value - The character data the parser produced.\n * @param plan - The namespace plan.\n * @param elementPath - The path of the element the character data belongs to.\n *\n * @returns The character data, under the element's value key when it has one.\n */\nconst decodeText = (value: string, plan: NamespacePlan, elementPath: string): XmlValue => {\n const valueKey = plan.valueByElement.get(elementPath);\n return valueKey !== undefined ? { [valueKey]: value } : value;\n};\n\n/**\n * @description One record read back: its declarations dropped, its keys resolved to the schema's names, and its character data placed under the value field.\n *\n * @param record - The record to read.\n * @param plan - The namespace plan.\n * @param scope - The prefix bindings in scope above this element.\n * @param elementPath - The path of this element, or the root sentinel.\n *\n * @returns The record, keyed by the schema's names, or the string it collapses to.\n */\nconst decodeRecord = (record: XmlRecord, plan: NamespacePlan, scope: Record<string, string | undefined>, elementPath: string): XmlValue => {\n const inner = scopeOf(record, scope);\n const valueKey = plan.valueByElement.get(elementPath);\n const out: Record<string, XmlValue> = {};\n\n for (const [key, child] of Object.entries(record)) {\n if (isDeclarationKey(key)) {\n continue;\n }\n\n if (key === TEXT_KEY) {\n out[valueKey ?? TEXT_KEY] = child;\n continue;\n }\n\n const childKey = schemaKey(plan, elementPath, key, childScopeOf(child, inner));\n const path = childPath(elementPath, childKey);\n const decoded = decodeNames(child, plan, inner, path);\n out[childKey] = plan.arrayKeys.has(path) && !Array.isArray(decoded) ? [decoded] : decoded;\n }\n\n const keys = Object.keys(out);\n if (keys.length === 1 && keys[0] === TEXT_KEY) {\n return out[TEXT_KEY];\n }\n return out;\n};\n\n/**\n * @description Rewrites a wire value tree back to the schema's local names, resolving every name against the declarations the document carries and dropping those\n * declarations. Character data maps to the value field of the element it belongs to. A record left holding only character data collapses back to that\n * string, which is how a namespaced leaf stays a `Schema.String`.\n *\n * @param value - The parsed wire tree.\n * @param plan - The namespace plan.\n * @param scope - The prefix bindings in scope above this element.\n * @param elementPath - The path of this element, or the root sentinel.\n *\n * @returns The value tree, keyed by the schema's names.\n */\nexport const decodeNames = (value: XmlValue, plan: NamespacePlan, scope: Record<string, string | undefined>, elementPath: string): XmlValue => {\n if (Array.isArray(value)) {\n return value.map(member => decodeNames(member, plan, scope, elementPath));\n }\n if (Predicate.isString(value)) {\n return decodeText(value, plan, elementPath);\n }\n if (!Predicate.isObject(value)) {\n return value;\n }\n return decodeRecord(value as XmlRecord, plan, scope, elementPath);\n};\n"],"mappings":"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"}
package/dist/naming.d.ts CHANGED
@@ -2,7 +2,7 @@ import { XmlError } from "./xml-error.js";
2
2
  import { Effect } from "effect";
3
3
  //#region src/naming.d.ts
4
4
  /**
5
- * @description The XML specification version a production is validated against. The two differ only in their non-ASCII character ranges — see {@link getRegexes}.
5
+ * @description The XML specification version a production is validated against. The two differ only in their non-ASCII character ranges. See {@link getRegexes}.
6
6
  */
7
7
  export type XmlVersion = '1.0' | '1.1';
8
8
  /**
@@ -22,7 +22,7 @@ export interface ValidationOptions {
22
22
  /**
23
23
  * @description Restrict matching to the ASCII subset of the NameStartChar/NameChar productions and skip unicode-aware regex matching entirely. Faster,
24
24
  * especially for XML 1.1 (which otherwise requires the `/u` regex flag), but rejects legitimate non-ASCII XML names. Off by default for backward
25
- * compatibility — opt in only when inputs are known to be ASCII. Defaults to false.
25
+ * compatibility. Opt in only when inputs are known to be ASCII. Defaults to false.
26
26
  */
27
27
  asciiOnly?: boolean;
28
28
  }
@@ -39,7 +39,7 @@ export interface SanitizeOptions {
39
39
  */
40
40
  asciiOnly?: boolean;
41
41
  /**
42
- * @description Accepted and ignored. Sanitizing is not version-dependent — the character set it considers illegal is the union of both versions — but the option
42
+ * @description Accepted and ignored. Sanitizing is not version-dependent: the character set it considers illegal is the union of both versions. But the option
43
43
  * is part of the published signature, so dropping it would break callers that pass it through a shared options object.
44
44
  */
45
45
  xmlVersion?: XmlVersion;
@@ -104,7 +104,7 @@ export declare const isQName: (str: string, { xmlVersion, asciiOnly }?: Validati
104
104
  */
105
105
  export declare const isNmToken: (str: string, { xmlVersion, asciiOnly }?: ValidationOptions) => boolean;
106
106
  /**
107
- * @description Whether the string is a valid NMTokens value — a whitespace-separated list of NMToken values. Used for: DTD NMTOKENS attribute values.
107
+ * @description Whether the string is a valid NMTokens value: a whitespace-separated list of NMToken values. Used for: DTD NMTOKENS attribute values.
108
108
  *
109
109
  * @param str - The candidate list.
110
110
  * @param opts - `asciiOnly` skips unicode-aware matching, ASCII names only (default false).
@@ -118,7 +118,7 @@ export declare const isNmTokens: (str: string, { xmlVersion, asciiOnly }?: Valid
118
118
  * @example
119
119
  * ```typescript
120
120
  * import { Effect } from 'effect';
121
- * import { validate } from '@endevops/effect-xml-codec';
121
+ * import { validate } from '@endevops/effect-codec-xml';
122
122
  *
123
123
  * Effect.runSync(validate('not a name', 'ncName'));
124
124
  * // { valid: false, production: 'ncName', input: 'not a name', reason: 'First character " " is not a valid NameStartChar', position: 0 }
@@ -129,7 +129,7 @@ export declare const isNmTokens: (str: string, { xmlVersion, asciiOnly }?: Valid
129
129
  * @param opts - Version and ASCII-only selection, as for the boolean validators.
130
130
  *
131
131
  * @returns An effect producing a discriminated result: the plain triple when valid, or the offending `reason` and `position` when not. A name that
132
- * fails to validate is a `valid: false` result, not a failure — an invalid name is the question being answered. The effect fails only with an
132
+ * fails to validate is a `valid: false` result, not a failure: an invalid name is the question being answered. The effect fails only with an
133
133
  * {@link XmlError} and the `InvalidProduction` reason for an unknown production, which is unreachable from TypeScript and is the guard for untyped
134
134
  * JavaScript callers.
135
135
  */
@@ -1 +1 @@
1
- {"version":3,"file":"naming.d.ts","names":[],"sources":["../src/naming.ts"],"mappings":";;;;;;YAwBY;;;;;;YAOA;;;;iBAKK;;;;EAIf,aAAa;;;;;;EAMb;;;;;iBAMe;;;;EAIf;;;;EAIA;;;;;EAKA,aAAa;;;;;YAMH;EACN;EAAa,YAAY;EAAY;;EAErC;EACA,YAAY;EACZ;EACA;;;;;EAKA;;;;;;;;;;;qBA+KO,SAAM,eAAe,YAAA,cAA6C;;;;;;;;;;;qBAalE,WAAQ,eAAe,YAAA,cAA6C;;;;;;;;;;;qBAapE,UAAO,eAAe,YAAA,cAA6C;;;;;;;;;qBAWnE,YAAS,eAAe,YAAA,cAA6C;;;;;;;;;qBAWrE,aAAU,eAAe,YAAA,cAA6C;;;;;;;;;;;;;;;;;;;;;;qBAuKtE,WAAQ,aAAA,YAAA,YAAA,SAAA,kCAAA,OAAA,OAAA,kBAAA;;;;;;;;;;;qBAwBR,WAAQ,aAAe,aAAc,cAAU,aAAA,cAAqD"}
1
+ {"version":3,"file":"naming.d.ts","names":[],"sources":["../src/naming.ts"],"mappings":";;;;;;YAwBY;;;;;;YAOA;;;;iBAKK;;;;EAIf,aAAa;;;;;;EAMb;;;;;iBAMe;;;;EAIf;;;;EAIA;;;;;EAKA,aAAa;;;;;YAMH;EACN;EAAa,YAAY;EAAY;;EAErC;EACA,YAAY;EACZ;EACA;;;;;EAKA;;;;;;;;;;;qBA8KO,SAAM,eAAe,YAAA,cAA6C;;;;;;;;;;;qBAalE,WAAQ,eAAe,YAAA,cAA6C;;;;;;;;;;;qBAapE,UAAO,eAAe,YAAA,cAA6C;;;;;;;;;qBAWnE,YAAS,eAAe,YAAA,cAA6C;;;;;;;;;qBAWrE,aAAU,eAAe,YAAA,cAA6C;;;;;;;;;;;;;;;;;;;;;;qBAuKtE,WAAQ,aAAA,YAAA,YAAA,SAAA,kCAAA,OAAA,OAAA,kBAAA;;;;;;;;;;;qBAwBR,WAAQ,aAAe,aAAc,cAAU,aAAA,cAAqD"}
package/dist/naming.js CHANGED
@@ -91,7 +91,7 @@ const isQName = (str, { xmlVersion = "1.0", asciiOnly = false } = {}) => getRege
91
91
  */
92
92
  const isNmToken = (str, { xmlVersion = "1.0", asciiOnly = false } = {}) => getRegexes(xmlVersion, asciiOnly).nmToken.test(str);
93
93
  /**
94
- * @description Whether the string is a valid NMTokens value — a whitespace-separated list of NMToken values. Used for: DTD NMTOKENS attribute values.
94
+ * @description Whether the string is a valid NMTokens value: a whitespace-separated list of NMToken values. Used for: DTD NMTOKENS attribute values.
95
95
  *
96
96
  * @param str - The candidate list.
97
97
  * @param opts - `asciiOnly` skips unicode-aware matching, ASCII names only (default false).
@@ -249,7 +249,7 @@ const diagnoseWith = (str, production, isValid, asciiOnly) => {
249
249
  * @example
250
250
  * ```typescript
251
251
  * import { Effect } from 'effect';
252
- * import { validate } from '@endevops/effect-xml-codec';
252
+ * import { validate } from '@endevops/effect-codec-xml';
253
253
  *
254
254
  * Effect.runSync(validate('not a name', 'ncName'));
255
255
  * // { valid: false, production: 'ncName', input: 'not a name', reason: 'First character " " is not a valid NameStartChar', position: 0 }
@@ -260,7 +260,7 @@ const diagnoseWith = (str, production, isValid, asciiOnly) => {
260
260
  * @param opts - Version and ASCII-only selection, as for the boolean validators.
261
261
  *
262
262
  * @returns An effect producing a discriminated result: the plain triple when valid, or the offending `reason` and `position` when not. A name that
263
- * fails to validate is a `valid: false` result, not a failure — an invalid name is the question being answered. The effect fails only with an
263
+ * fails to validate is a `valid: false` result, not a failure: an invalid name is the question being answered. The effect fails only with an
264
264
  * {@link XmlError} and the `InvalidProduction` reason for an unknown production, which is unreachable from TypeScript and is the guard for untyped
265
265
  * JavaScript callers.
266
266
  */
@@ -1 +1 @@
1
- {"version":3,"file":"naming.js","names":[],"sources":["../src/naming.ts"],"sourcesContent":["// xml-naming\n// Validates XML Name productions as defined in the XML 1.0 and 1.1 specifications.\n// Covers: Name, NCName, QName, NMToken, NMTokens\n//\n// XML 1.0 spec: https://www.w3.org/TR/xml/#NT-Name\n// XML 1.1 spec: https://www.w3.org/TR/xml11/#NT-NameStartChar\n// XML NS spec: https://www.w3.org/TR/xml-names/#NT-NCName\n//\n// The five predicates and `sanitize` are plain synchronous functions: a regex\n// test cannot fail and a character substitution has nothing to fail about, so\n// there is no effect to model. `validate` does have one failure to report — an\n// unknown production, unreachable from TypeScript where `Production` is a\n// closed union but reachable for an untyped JavaScript caller, or a value that\n// crossed a boundary as `unknown` — so it answers with an `Effect` whose error\n// channel is that {@link XmlError}. The value it produces is still a plain\n// result.\n\nimport { Effect } from 'effect';\n\nimport { XmlError } from '#/xml-error.ts';\n\n/**\n * @description The XML specification version a production is validated against. The two differ only in their non-ASCII character ranges — see {@link getRegexes}.\n */\nexport type XmlVersion = '1.0' | '1.1';\n\n/**\n * @description One of the five XML name productions this package validates. The name is the production's grammar rule: `name` is the full Name production,\n * `ncName` its non-colonized form, `qName` the prefixed form, and `nmToken`/`nmTokens` the attribute-value productions that drop the first-character\n * restriction.\n */\nexport type Production = 'name' | 'ncName' | 'qName' | 'nmToken' | 'nmTokens';\n\n/**\n * @description Options shared by every validator.\n */\nexport interface ValidationOptions {\n /**\n * @description XML specification version to validate against. Defaults to '1.0'.\n */\n xmlVersion?: XmlVersion;\n /**\n * @description Restrict matching to the ASCII subset of the NameStartChar/NameChar productions and skip unicode-aware regex matching entirely. Faster,\n * especially for XML 1.1 (which otherwise requires the `/u` regex flag), but rejects legitimate non-ASCII XML names. Off by default for backward\n * compatibility — opt in only when inputs are known to be ASCII. Defaults to false.\n */\n asciiOnly?: boolean;\n}\n\n/**\n * @description Options for {@link sanitize}.\n */\nexport interface SanitizeOptions {\n /**\n * @description Character used to replace invalid characters. Defaults to '_'.\n */\n replacement?: string;\n /**\n * @description Also replace any non-ASCII character, not just XML-illegal ones. Defaults to false.\n */\n asciiOnly?: boolean;\n /**\n * @description Accepted and ignored. Sanitizing is not version-dependent — the character set it considers illegal is the union of both versions — but the option\n * is part of the published signature, so dropping it would break callers that pass it through a shared options object.\n */\n xmlVersion?: XmlVersion;\n}\n\n/**\n * @description The outcome of {@link validate}, discriminated on `valid` so a caller can narrow to the reason and position without a cast.\n */\nexport type ValidationResult =\n | { valid: true; production: Production; input: string }\n | {\n valid: false;\n production: Production;\n input: string;\n reason: string;\n /**\n * @description Index of the first offending character, or `undefined` when the failure is structural (an empty input, or a colon count) rather than a\n * specific character.\n */\n position: number | undefined;\n };\n\n/**\n * @description The five productions, in the order the runtime error message lists them.\n */\nconst PRODUCTIONS = ['name', 'ncName', 'qName', 'nmToken', 'nmTokens'] as const satisfies ReadonlyArray<Production>;\n\n/**\n * @description One compiled regex per production.\n */\ntype ProductionRegexes = Record<Production, RegExp>;\n\n// ---------------------------------------------------------------------------\n// Character class strings — XML 1.0\n//\n// NameStartChar ::= \":\" | [A-Z] | \"_\" | [a-z]\n// | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF]\n// | [#x370-#x37D] | [#x37F-#x1FFF] <- split to exclude #x0487\n// | [#x200C-#x200D]\n// | [#x2070-#x218F] | [#x2C00-#x2FEF]\n// | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD]\n//\n// NameChar ::= NameStartChar | \"-\" | \".\" | [0-9]\n// | #xB7 | [#x0300-#x036F] | [#x203F-#x2040]\n//\n// Note: \\u0487 (Combining Cyrillic Millions Sign) was added in Unicode 4.0,\n// after XML 1.0 was defined against Unicode 2.0. It falls inside the range\n// \\u037F-\\u1FFF but must be excluded. We split that range into\n// \\u037F-\\u0486 and \\u0488-\\u1FFF to exclude it explicitly.\n// ---------------------------------------------------------------------------\n\nconst nameStartChar10 =\n ':A-Za-z_' +\n '\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF' +\n '\\u0370-\\u037D' +\n '\\u037F-\\u0486\\u0488-\\u1FFF' + // split to exclude \\u0487\n '\\u200C-\\u200D' +\n '\\u2070-\\u218F' +\n '\\u2C00-\\u2FEF' +\n '\\u3001-\\uD7FF' +\n '\\uF900-\\uFDCF' +\n '\\uFDF0-\\uFFFD';\n\nconst nameChar10 = nameStartChar10 + '\\\\-\\\\.\\\\d' + '\\u00B7' + '\\u0300-\\u036F' + '\\u203F-\\u2040';\n\n// ---------------------------------------------------------------------------\n// Character class strings — XML 1.1\n//\n// Differences from XML 1.0:\n//\n// NameStartChar:\n// 1.0 has split ranges: \\u00C0-\\u00D6, \\u00D8-\\u00F6, \\u00F8-\\u02FF\n// 1.1 merges them into: \\u00C0-\\u02FF\n// (\\u00D7 x and \\u00F7 / are division symbols, excluded in both versions)\n//\n// 1.0 tops out at \\uFFFD (BMP only)\n// 1.1 adds \\u{10000}-\\u{EFFFF} (supplementary planes)\n// These require the /u flag on the RegExp — see buildRegexes below.\n//\n// NameChar:\n// 1.1 adds \\u0487 (Combining Cyrillic Millions Sign, added in Unicode 4.0)\n// ---------------------------------------------------------------------------\n\nconst nameStartChar11 =\n ':A-Za-z_' +\n '\\u00C0-\\u02FF' + // merged — 1.0 had three split ranges here\n '\\u0370-\\u037D' +\n '\\u037F-\\u0486\\u0488-\\u1FFF' + // split to exclude \\u0487 (combining mark, never a NameStartChar)\n '\\u200C-\\u200D' +\n '\\u2070-\\u218F' +\n '\\u2C00-\\u2FEF' +\n '\\u3001-\\uD7FF' +\n '\\uF900-\\uFDCF' +\n '\\uFDF0-\\uFFFD' +\n '\\u{10000}-\\u{EFFFF}'; // supplementary planes — REQUIRES /u flag on RegExp\n\nconst nameChar11 =\n nameStartChar11 +\n '\\\\-\\\\.\\\\d' +\n '\\u00B7' +\n '\\u0300-\\u036F' +\n '\\u0487' + // Combining Cyrillic Millions Sign — valid in 1.1, not 1.0\n '\\u203F-\\u2040';\n\n// ---------------------------------------------------------------------------\n// Regex builders\n//\n// XML 1.0 regexes: no flags — BMP only, standard JS regex behaviour.\n// XML 1.1 regexes: /u flag — required for \\u{10000}-\\u{EFFFF} to match actual\n// supplementary code points rather than lone surrogates (which are illegal XML).\n// ---------------------------------------------------------------------------\n\n/**\n * @description Compiles the five production regexes from a NameStartChar/NameChar pair.\n *\n * @param startChar - Character class body for the first character.\n * @param char - Character class body for every subsequent character.\n * @param flags - RegExp flags. `'u'` for the XML 1.1 set, so its supplementary-plane range matches code points rather than lone surrogates.\n *\n * @returns One regex per {@link Production}.\n */\nconst buildRegexes = (startChar: string, char: string, flags = ''): ProductionRegexes => {\n const ncStart = startChar.replace(':', '');\n const ncChar = char.replace(':', '');\n const ncNamePat = `[${ncStart}][${ncChar}]*`;\n\n return {\n name: new RegExp(`^[${startChar}][${char}]*$`, flags),\n ncName: new RegExp(`^${ncNamePat}$`, flags),\n qName: new RegExp(`^${ncNamePat}(?::${ncNamePat})?$`, flags),\n nmToken: new RegExp(`^[${char}]+$`, flags),\n nmTokens: new RegExp(`^[${char}]+(?:\\\\s+[${char}]+)*$`, flags),\n };\n};\n\nconst regexes10 = buildRegexes(nameStartChar10, nameChar10); // no /u — BMP only\nconst regexes11 = buildRegexes(nameStartChar11, nameChar11, 'u'); // /u — enables \\u{10000}-\\u{EFFFF}\n\n// ---------------------------------------------------------------------------\n// ASCII-only fast path (opt-in, off by default)\n//\n// The XML 1.0 vs 1.1 NameStartChar/NameChar productions differ *only* in\n// their non-ASCII ranges (merged vs split Latin-1 ranges, \\u0487, and\n// supplementary planes). Restricted to ASCII, both versions collapse to the\n// same character classes, so a single regex pair covers both xmlVersion\n// values — no /u flag needed.\n//\n// Rationale: unicode-aware regexes (the /u flag, required for XML 1.1's\n// supplementary-plane range) are measurably slower in V8 than plain\n// non-unicode regexes on the same input, even when the input is pure ASCII.\n// For the common case — HTML/SVG ids, XML tags — names are ASCII, so callers\n// who know this can opt in to skip the unicode-aware matching path entirely.\n// This is a real but *conditional* win: mainly for XML 1.1 input (avoids /u),\n// or at scale where the larger unicode character classes add engine\n// overhead. It also changes behaviour (rejects legitimate non-ASCII XML\n// 1.0/1.1 names), so it must never be silently enabled — hence off by\n// default.\n// ---------------------------------------------------------------------------\n\nconst nameStartCharAscii = ':A-Za-z_';\nconst nameCharAscii = nameStartCharAscii + '\\\\-\\\\.\\\\d';\n\nconst regexesAscii = buildRegexes(nameStartCharAscii, nameCharAscii); // no /u — ASCII only\n\n/**\n * @description The compiled regex set for a version/ASCII combination. Only three sets are ever built, at module load; this is a lookup, not a compile.\n *\n * @param xmlVersion - Which XML version's character classes to use.\n * @param asciiOnly - Return the ASCII-only set regardless of `xmlVersion`.\n *\n * @returns The regex set to validate against.\n */\nconst getRegexes = (xmlVersion: XmlVersion = '1.0', asciiOnly = false): ProductionRegexes => {\n if (asciiOnly) return regexesAscii;\n return xmlVersion === '1.1' ? regexes11 : regexes10;\n};\n\n// ---------------------------------------------------------------------------\n// Boolean validators\n//\n// One plain predicate per production. A regex test cannot fail, and every one of these is called per name\n// inside a parser's or codec's hot loop, so a boolean is the honest answer and there is no effect to\n// allocate or run.\n// ---------------------------------------------------------------------------\n\n/**\n * @description Whether the string is a valid XML Name. Colons are allowed anywhere (Name production). Used for: DOCTYPE entity names, notation names, DTD element\n * declarations.\n *\n * @param str - The candidate name.\n * @param opts - `asciiOnly` skips unicode-aware matching, ASCII names only (default false).\n *\n * @returns Whether `str` satisfies the production.\n */\nexport const isName = (str: string, { xmlVersion = '1.0', asciiOnly = false }: ValidationOptions = {}): boolean =>\n getRegexes(xmlVersion, asciiOnly).name.test(str);\n\n/**\n * @description Whether the string is a valid NCName (Non-Colonized Name).\\\n * Colons are not permitted.\\\n * Used for: namespace prefixes, local names, SVG id attributes.\n *\n * @param str - The candidate name.\n * @param opts - `asciiOnly` skips unicode-aware matching, ASCII names only (default false).\n *\n * @returns Whether `str` satisfies the production.\n */\nexport const isNcName = (str: string, { xmlVersion = '1.0', asciiOnly = false }: ValidationOptions = {}): boolean =>\n getRegexes(xmlVersion, asciiOnly).ncName.test(str);\n\n/**\n * @description Whether the string is a valid QName (Qualified Name).\\\n * Allows exactly one colon as a prefix separator: `prefix:localName`.\\\n * Used for: element and attribute names in namespace-aware XML/SVG.\n *\n * @param str - The candidate name.\n * @param opts - `asciiOnly` skips unicode-aware matching, ASCII names only (default false).\n *\n * @returns Whether `str` satisfies the production.\n */\nexport const isQName = (str: string, { xmlVersion = '1.0', asciiOnly = false }: ValidationOptions = {}): boolean =>\n getRegexes(xmlVersion, asciiOnly).qName.test(str);\n\n/**\n * @description Whether the string is a valid NMToken. Like Name but no restriction on the first character. Used for: DTD NMTOKEN attribute values.\n *\n * @param str - The candidate token.\n * @param opts - `asciiOnly` skips unicode-aware matching, ASCII names only (default false).\n *\n * @returns Whether `str` satisfies the production.\n */\nexport const isNmToken = (str: string, { xmlVersion = '1.0', asciiOnly = false }: ValidationOptions = {}): boolean =>\n getRegexes(xmlVersion, asciiOnly).nmToken.test(str);\n\n/**\n * @description Whether the string is a valid NMTokens value — a whitespace-separated list of NMToken values. Used for: DTD NMTOKENS attribute values.\n *\n * @param str - The candidate list.\n * @param opts - `asciiOnly` skips unicode-aware matching, ASCII names only (default false).\n *\n * @returns Whether `str` satisfies the production.\n */\nexport const isNmTokens = (str: string, { xmlVersion = '1.0', asciiOnly = false }: ValidationOptions = {}): boolean =>\n getRegexes(xmlVersion, asciiOnly).nmTokens.test(str);\n\n/**\n * @description The failure to report when a production is not one of the five this module knows, so `validate` reports it the same way. The single place the\n * unknown-production guard lives. It is unreachable from TypeScript, where `Production` is a closed union; this is the guard for untyped JavaScript\n * callers.\n *\n * @param production - The production to check.\n *\n * @returns The `InvalidProduction` failure, or `null` when the production is known.\n */\nconst productionError = (production: Production): XmlError | null => {\n if (PRODUCTIONS.includes(production)) return null;\n return new XmlError({\n reason: { _tag: 'InvalidProduction', production, expected: PRODUCTIONS.join(', ') },\n message: `Unknown production \"${production}\". Must be one of: ${PRODUCTIONS.join(', ')}`,\n });\n};\n\nconst validators: Record<Production, (str: string, opts?: ValidationOptions) => boolean> = {\n name: isName,\n ncName: isNcName,\n qName: isQName,\n nmToken: isNmToken,\n nmTokens: isNmTokens,\n};\n\n/**\n * @description The diagnostic body {@link validate} reports, with the production already known to be valid. Kept separate so the reason-finding logic carries no\n * unknown-production check and the batch path can map over it without re-entering the guard per element.\n *\n * @param str - The candidate name.\n * @param production - The production to validate against, already checked.\n * @param xmlVersion - Which version's character classes to use.\n * @param asciiOnly - Whether the ASCII-only fast path applied.\n *\n * @returns The discriminated result.\n */\nconst diagnose = (str: string, production: Production, xmlVersion: XmlVersion, asciiOnly: boolean): ValidationResult =>\n diagnoseWith(str, production, validators[production](str, { xmlVersion, asciiOnly }), asciiOnly);\n\n/**\n * @description The colon-specific reason a `qName` or `ncName` failed, or `undefined` when the failure is not about a colon. The three QName forms are checked in\n * the order they can co-occur: a string that both starts and ends with a colon is reported as the leading one, and a two-colon string is reported as\n * the count.\n *\n * @param str - The candidate name.\n * @param production - The production checked.\n *\n * @returns The reason and position, or `undefined`.\n */\nconst diagnoseColon = (str: string, production: Production): { reason: string; position: number } | undefined => {\n if (production === 'ncName' && str.includes(':')) {\n return { reason: 'Colon is not allowed in NCName', position: str.indexOf(':') };\n }\n\n if (production !== 'qName') {\n return undefined;\n }\n if (str.startsWith(':')) {\n return { reason: 'QName cannot start with a colon', position: 0 };\n }\n if (str.endsWith(':')) {\n return { reason: 'QName cannot end with a colon', position: str.length - 1 };\n }\n if ((str.match(/:/g) ?? []).length > 1) {\n return { reason: 'QName can have at most one colon', position: str.lastIndexOf(':') };\n }\n return undefined;\n};\n\n/**\n * @description The first character that is not a legal `NameChar`, and where it sits.\n *\n * @param str - The candidate name.\n * @param namePattern - The `NameChar` test for the character set the validator used.\n *\n * @returns The reason and position, or `undefined` when every character is legal.\n */\nconst diagnoseNameChar = (str: string, namePattern: RegExp): { reason: string; position: number } | undefined => {\n for (let i = 0; i < str.length; i++) {\n const char = str[i];\n if (char !== undefined && !namePattern.test(char)) {\n return { reason: `Character \"${char}\" at position ${i} is not a valid NameChar`, position: i };\n }\n }\n return undefined;\n};\n\n/**\n * @description Why a name failed, once whether it failed is already known. The first character outranks the rest: a name that cannot start is reported as a bad\n * `NameStartChar` even when a later character is also illegal.\n *\n * @param str - The candidate name.\n * @param production - The production checked.\n * @param startCharPattern - The `NameStartChar` test for the character set the validator used.\n * @param namePattern - The `NameChar` test for the same character set.\n *\n * @returns The reason, and the offending position, which is `undefined` for a structural failure.\n */\nconst findFailure = (\n str: string,\n production: Production,\n startCharPattern: RegExp,\n namePattern: RegExp\n): { reason: string; position: number | undefined } => {\n if (str.length === 0) {\n return { reason: 'Input is empty', position: undefined };\n }\n\n const colon = diagnoseColon(str, production);\n if (colon !== undefined) return colon;\n\n const firstChar = str[0];\n if (['name', 'ncName', 'qName'].includes(production) && !startCharPattern.test(firstChar ?? '')) {\n return { reason: `First character \"${firstChar}\" is not a valid NameStartChar`, position: 0 };\n }\n\n return diagnoseNameChar(str, namePattern) ?? { reason: 'Does not match the production rules', position: undefined };\n};\n\n/**\n * @description Why a name failed, once whether it failed is already known. Split from {@link diagnose} so the effect that asks the question stays a one-liner and\n * the reason-finding is plain.\n *\n * @param str - The candidate name.\n * @param production - The production checked.\n * @param isValid - Whether it passed.\n * @param asciiOnly - Whether the ASCII-only fast path applied.\n *\n * @returns The discriminated result.\n */\nconst diagnoseWith = (str: string, production: Production, isValid: boolean, asciiOnly: boolean): ValidationResult => {\n if (isValid) return { valid: true, production, input: str };\n\n // Diagnostic fallback char checks must mirror the same character set the\n // boolean validator above used, or the reported reason/position could\n // contradict the `valid: false` result (e.g. flagging a char as illegal\n // that the unicode-aware check would have accepted).\n const startCharPattern = asciiOnly ? /^[:A-Za-z_]/ : /^[:A-Za-z_\\u00C0-\\uFFFD]/;\n const namePattern = asciiOnly ? /[\\w\\-\\\\.:]/ : /[\\w\\-\\\\.:\\u00B7\\u00C0-\\uFFFD]/;\n\n return { valid: false, production, input: str, ...findFailure(str, production, startCharPattern, namePattern) };\n};\n\n/**\n * @description Validates a string against a named production and, on failure, reports why and where.\n *\n * @example\n * ```typescript\n * import { Effect } from 'effect';\n * import { validate } from '@endevops/effect-xml-codec';\n *\n * Effect.runSync(validate('not a name', 'ncName'));\n * // { valid: false, production: 'ncName', input: 'not a name', reason: 'First character \" \" is not a valid NameStartChar', position: 0 }\n * ```;\n *\n * @param str - The candidate name.\n * @param production - The production to validate against.\n * @param opts - Version and ASCII-only selection, as for the boolean validators.\n *\n * @returns An effect producing a discriminated result: the plain triple when valid, or the offending `reason` and `position` when not. A name that\n * fails to validate is a `valid: false` result, not a failure — an invalid name is the question being answered. The effect fails only with an\n * {@link XmlError} and the `InvalidProduction` reason for an unknown production, which is unreachable from TypeScript and is the guard for untyped\n * JavaScript callers.\n */\nexport const validate = Effect.fnUntraced(function* (\n str: string,\n production: Production,\n { xmlVersion = '1.0', asciiOnly = false }: ValidationOptions = {}\n): Effect.fn.Return<ValidationResult, XmlError> {\n const invalid = productionError(production);\n if (invalid) return yield* invalid;\n return diagnose(str, production, xmlVersion, asciiOnly);\n});\n\n// ---------------------------------------------------------------------------\n// Sanitizer\n// ---------------------------------------------------------------------------\n\n/**\n * @description Transforms an invalid string into the nearest valid XML name for the given production: strips or replaces illegal characters, fixes an invalid\n * start character by prepending the replacement, and removes colons for NCName.\n *\n * @param str - The candidate name.\n * @param production - The production to sanitize for. Defaults to `'name'`.\n * @param opts - `replacement` is the substitute character (default `'_'`); `asciiOnly` also replaces non-ASCII characters.\n *\n * @returns A string that satisfies `production` for the ASCII range, or the nearest approximation of it.\n */\nexport const sanitize = (str: string, production: Production = 'name', { replacement = '_', asciiOnly = false }: SanitizeOptions = {}): string => {\n if (!str) return replacement;\n\n let result = str;\n\n // Strip colons for NCName\n if (production === 'ncName') {\n result = result.replace(/:/g, '');\n }\n\n // Replace illegal characters\n const allowedCharPattern = asciiOnly ? /[^\\w\\-.:]/g : /[^\\w\\-.:\\u00B7\\u00C0-\\uFFFD]/g;\n result = result.replace(allowedCharPattern, replacement);\n\n // Fix invalid start character for Name / NCName / QName\n if (production !== 'nmToken' && production !== 'nmTokens') {\n if (/^[-.\\d]/.test(result)) {\n result = replacement + result;\n }\n }\n\n return result || replacement;\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"naming.js","names":[],"sources":["../src/naming.ts"],"sourcesContent":["// xml-naming\n// Validates XML Name productions as defined in the XML 1.0 and 1.1 specifications.\n// Covers: Name, NCName, QName, NMToken, NMTokens\n//\n// XML 1.0 spec: https://www.w3.org/TR/xml/#NT-Name\n// XML 1.1 spec: https://www.w3.org/TR/xml11/#NT-NameStartChar\n// XML NS spec: https://www.w3.org/TR/xml-names/#NT-NCName\n//\n// The five predicates and `sanitize` are plain synchronous functions: a regex\n// test cannot fail and a character substitution has nothing to fail about, so\n// there is no effect to model. `validate` does have one failure to report: an\n// unknown production, unreachable from TypeScript where `Production` is a\n// closed union but reachable for an untyped JavaScript caller, or a value that\n// crossed a boundary as `unknown`. It answers with an `Effect` whose error\n// channel is that {@link XmlError}. The value it produces is still a plain\n// result.\n\nimport { Effect } from 'effect';\n\nimport { XmlError } from '#/xml-error.ts';\n\n/**\n * @description The XML specification version a production is validated against. The two differ only in their non-ASCII character ranges. See {@link getRegexes}.\n */\nexport type XmlVersion = '1.0' | '1.1';\n\n/**\n * @description One of the five XML name productions this package validates. The name is the production's grammar rule: `name` is the full Name production,\n * `ncName` its non-colonized form, `qName` the prefixed form, and `nmToken`/`nmTokens` the attribute-value productions that drop the first-character\n * restriction.\n */\nexport type Production = 'name' | 'ncName' | 'qName' | 'nmToken' | 'nmTokens';\n\n/**\n * @description Options shared by every validator.\n */\nexport interface ValidationOptions {\n /**\n * @description XML specification version to validate against. Defaults to '1.0'.\n */\n xmlVersion?: XmlVersion;\n /**\n * @description Restrict matching to the ASCII subset of the NameStartChar/NameChar productions and skip unicode-aware regex matching entirely. Faster,\n * especially for XML 1.1 (which otherwise requires the `/u` regex flag), but rejects legitimate non-ASCII XML names. Off by default for backward\n * compatibility. Opt in only when inputs are known to be ASCII. Defaults to false.\n */\n asciiOnly?: boolean;\n}\n\n/**\n * @description Options for {@link sanitize}.\n */\nexport interface SanitizeOptions {\n /**\n * @description Character used to replace invalid characters. Defaults to '_'.\n */\n replacement?: string;\n /**\n * @description Also replace any non-ASCII character, not just XML-illegal ones. Defaults to false.\n */\n asciiOnly?: boolean;\n /**\n * @description Accepted and ignored. Sanitizing is not version-dependent: the character set it considers illegal is the union of both versions. But the option\n * is part of the published signature, so dropping it would break callers that pass it through a shared options object.\n */\n xmlVersion?: XmlVersion;\n}\n\n/**\n * @description The outcome of {@link validate}, discriminated on `valid` so a caller can narrow to the reason and position without a cast.\n */\nexport type ValidationResult =\n | { valid: true; production: Production; input: string }\n | {\n valid: false;\n production: Production;\n input: string;\n reason: string;\n /**\n * @description Index of the first offending character, or `undefined` when the failure is structural (an empty input, or a colon count) rather than a\n * specific character.\n */\n position: number | undefined;\n };\n\n/**\n * @description The five productions, in the order the runtime error message lists them.\n */\nconst PRODUCTIONS = ['name', 'ncName', 'qName', 'nmToken', 'nmTokens'] as const satisfies ReadonlyArray<Production>;\n\n/**\n * @description One compiled regex per production.\n */\ntype ProductionRegexes = Record<Production, RegExp>;\n\n// ---------------------------------------------------------------------------\n// Character class strings: XML 1.0\n//\n// NameStartChar ::= \":\" | [A-Z] | \"_\" | [a-z]\n// | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF]\n// | [#x370-#x37D] | [#x37F-#x1FFF] <- split to exclude #x0487\n// | [#x200C-#x200D]\n// | [#x2070-#x218F] | [#x2C00-#x2FEF]\n// | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD]\n//\n// NameChar ::= NameStartChar | \"-\" | \".\" | [0-9]\n// | #xB7 | [#x0300-#x036F] | [#x203F-#x2040]\n//\n// Note: \\u0487 (Combining Cyrillic Millions Sign) was added in Unicode 4.0,\n// after XML 1.0 was defined against Unicode 2.0. It falls inside the range\n// \\u037F-\\u1FFF but must be excluded. We split that range into\n// \\u037F-\\u0486 and \\u0488-\\u1FFF to exclude it explicitly.\n// ---------------------------------------------------------------------------\n\nconst nameStartChar10 =\n ':A-Za-z_' +\n '\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF' +\n '\\u0370-\\u037D' +\n '\\u037F-\\u0486\\u0488-\\u1FFF' + // split to exclude \\u0487\n '\\u200C-\\u200D' +\n '\\u2070-\\u218F' +\n '\\u2C00-\\u2FEF' +\n '\\u3001-\\uD7FF' +\n '\\uF900-\\uFDCF' +\n '\\uFDF0-\\uFFFD';\n\nconst nameChar10 = nameStartChar10 + '\\\\-\\\\.\\\\d' + '\\u00B7' + '\\u0300-\\u036F' + '\\u203F-\\u2040';\n\n// ---------------------------------------------------------------------------\n// Character class strings: XML 1.1\n//\n// Differences from XML 1.0:\n//\n// NameStartChar:\n// 1.0 has split ranges: \\u00C0-\\u00D6, \\u00D8-\\u00F6, \\u00F8-\\u02FF\n// 1.1 merges them into: \\u00C0-\\u02FF\n// (\\u00D7 x and \\u00F7 / are division symbols, excluded in both versions)\n//\n// 1.0 tops out at \\uFFFD (BMP only)\n// 1.1 adds \\u{10000}-\\u{EFFFF} (supplementary planes)\n// These require the /u flag on the RegExp. See buildRegexes below.\n//\n// NameChar:\n// 1.1 adds \\u0487 (Combining Cyrillic Millions Sign, added in Unicode 4.0)\n// ---------------------------------------------------------------------------\n\nconst nameStartChar11 =\n ':A-Za-z_' +\n '\\u00C0-\\u02FF' + // merged: 1.0 had three split ranges here\n '\\u0370-\\u037D' +\n '\\u037F-\\u0486\\u0488-\\u1FFF' + // split to exclude \\u0487 (combining mark, never a NameStartChar)\n '\\u200C-\\u200D' +\n '\\u2070-\\u218F' +\n '\\u2C00-\\u2FEF' +\n '\\u3001-\\uD7FF' +\n '\\uF900-\\uFDCF' +\n '\\uFDF0-\\uFFFD' +\n '\\u{10000}-\\u{EFFFF}'; // supplementary planes: REQUIRES /u flag on RegExp\n\nconst nameChar11 =\n nameStartChar11 +\n '\\\\-\\\\.\\\\d' +\n '\\u00B7' +\n '\\u0300-\\u036F' +\n '\\u0487' + // Combining Cyrillic Millions Sign: valid in 1.1, not 1.0\n '\\u203F-\\u2040';\n\n// ---------------------------------------------------------------------------\n// Regex builders\n//\n// XML 1.0 regexes: no flags, BMP only, standard JS regex behaviour.\n// XML 1.1 regexes: /u flag, required for \\u{10000}-\\u{EFFFF} to match actual\n// supplementary code points rather than lone surrogates (which are illegal XML).\n// ---------------------------------------------------------------------------\n\n/**\n * @description Compiles the five production regexes from a NameStartChar/NameChar pair.\n *\n * @param startChar - Character class body for the first character.\n * @param char - Character class body for every subsequent character.\n * @param flags - RegExp flags. `'u'` for the XML 1.1 set, so its supplementary-plane range matches code points rather than lone surrogates.\n *\n * @returns One regex per {@link Production}.\n */\nconst buildRegexes = (startChar: string, char: string, flags = ''): ProductionRegexes => {\n const ncStart = startChar.replace(':', '');\n const ncChar = char.replace(':', '');\n const ncNamePat = `[${ncStart}][${ncChar}]*`;\n\n return {\n name: new RegExp(`^[${startChar}][${char}]*$`, flags),\n ncName: new RegExp(`^${ncNamePat}$`, flags),\n qName: new RegExp(`^${ncNamePat}(?::${ncNamePat})?$`, flags),\n nmToken: new RegExp(`^[${char}]+$`, flags),\n nmTokens: new RegExp(`^[${char}]+(?:\\\\s+[${char}]+)*$`, flags),\n };\n};\n\nconst regexes10 = buildRegexes(nameStartChar10, nameChar10); // no /u, BMP only\nconst regexes11 = buildRegexes(nameStartChar11, nameChar11, 'u'); // /u enables \\u{10000}-\\u{EFFFF}\n\n// ---------------------------------------------------------------------------\n// ASCII-only fast path (opt-in, off by default)\n//\n// The XML 1.0 and 1.1 NameStartChar/NameChar productions differ only in their\n// non-ASCII ranges: merged vs split Latin-1 ranges, \\u0487, and supplementary\n// planes. Restricted to ASCII, both versions collapse to the same character\n// classes, so one regex pair covers both xmlVersion values and no /u flag is\n// needed.\n//\n// Unicode-aware regexes (the /u flag, required for XML 1.1's supplementary-\n// plane range) are measurably slower in V8 than plain non-unicode regexes on\n// the same input, even when the input is pure ASCII. For the common case\n// (HTML/SVG ids, XML tags) names are ASCII, so callers who know this can opt\n// in to skip the unicode-aware matching path entirely. The win is conditional:\n// it applies mainly to XML 1.1 input (avoids /u), or at scale where the larger\n// unicode character classes add engine overhead. The option also changes\n// behaviour (it rejects legitimate non-ASCII XML 1.0/1.1 names), so it must\n// never be enabled silently. That is why the default is off.\n// ---------------------------------------------------------------------------\n\nconst nameStartCharAscii = ':A-Za-z_';\nconst nameCharAscii = nameStartCharAscii + '\\\\-\\\\.\\\\d';\n\nconst regexesAscii = buildRegexes(nameStartCharAscii, nameCharAscii); // no /u, ASCII only\n\n/**\n * @description The compiled regex set for a version/ASCII combination. Only three sets are ever built, at module load; this is a lookup, not a compile.\n *\n * @param xmlVersion - Which XML version's character classes to use.\n * @param asciiOnly - Return the ASCII-only set regardless of `xmlVersion`.\n *\n * @returns The regex set to validate against.\n */\nconst getRegexes = (xmlVersion: XmlVersion = '1.0', asciiOnly = false): ProductionRegexes => {\n if (asciiOnly) return regexesAscii;\n return xmlVersion === '1.1' ? regexes11 : regexes10;\n};\n\n// ---------------------------------------------------------------------------\n// Boolean validators\n//\n// One plain predicate per production. A regex test cannot fail, and every one of these is called per name\n// inside a parser's or codec's hot loop, so a boolean is the direct answer and there is no effect to\n// allocate or run.\n// ---------------------------------------------------------------------------\n\n/**\n * @description Whether the string is a valid XML Name. Colons are allowed anywhere (Name production). Used for: DOCTYPE entity names, notation names, DTD element\n * declarations.\n *\n * @param str - The candidate name.\n * @param opts - `asciiOnly` skips unicode-aware matching, ASCII names only (default false).\n *\n * @returns Whether `str` satisfies the production.\n */\nexport const isName = (str: string, { xmlVersion = '1.0', asciiOnly = false }: ValidationOptions = {}): boolean =>\n getRegexes(xmlVersion, asciiOnly).name.test(str);\n\n/**\n * @description Whether the string is a valid NCName (Non-Colonized Name).\\\n * Colons are not permitted.\\\n * Used for: namespace prefixes, local names, SVG id attributes.\n *\n * @param str - The candidate name.\n * @param opts - `asciiOnly` skips unicode-aware matching, ASCII names only (default false).\n *\n * @returns Whether `str` satisfies the production.\n */\nexport const isNcName = (str: string, { xmlVersion = '1.0', asciiOnly = false }: ValidationOptions = {}): boolean =>\n getRegexes(xmlVersion, asciiOnly).ncName.test(str);\n\n/**\n * @description Whether the string is a valid QName (Qualified Name).\\\n * Allows exactly one colon as a prefix separator: `prefix:localName`.\\\n * Used for: element and attribute names in namespace-aware XML/SVG.\n *\n * @param str - The candidate name.\n * @param opts - `asciiOnly` skips unicode-aware matching, ASCII names only (default false).\n *\n * @returns Whether `str` satisfies the production.\n */\nexport const isQName = (str: string, { xmlVersion = '1.0', asciiOnly = false }: ValidationOptions = {}): boolean =>\n getRegexes(xmlVersion, asciiOnly).qName.test(str);\n\n/**\n * @description Whether the string is a valid NMToken. Like Name but no restriction on the first character. Used for: DTD NMTOKEN attribute values.\n *\n * @param str - The candidate token.\n * @param opts - `asciiOnly` skips unicode-aware matching, ASCII names only (default false).\n *\n * @returns Whether `str` satisfies the production.\n */\nexport const isNmToken = (str: string, { xmlVersion = '1.0', asciiOnly = false }: ValidationOptions = {}): boolean =>\n getRegexes(xmlVersion, asciiOnly).nmToken.test(str);\n\n/**\n * @description Whether the string is a valid NMTokens value: a whitespace-separated list of NMToken values. Used for: DTD NMTOKENS attribute values.\n *\n * @param str - The candidate list.\n * @param opts - `asciiOnly` skips unicode-aware matching, ASCII names only (default false).\n *\n * @returns Whether `str` satisfies the production.\n */\nexport const isNmTokens = (str: string, { xmlVersion = '1.0', asciiOnly = false }: ValidationOptions = {}): boolean =>\n getRegexes(xmlVersion, asciiOnly).nmTokens.test(str);\n\n/**\n * @description The failure to report when a production is not one of the five this module knows, so `validate` reports it the same way. The single place the\n * unknown-production guard lives. It is unreachable from TypeScript, where `Production` is a closed union; this is the guard for untyped JavaScript\n * callers.\n *\n * @param production - The production to check.\n *\n * @returns The `InvalidProduction` failure, or `null` when the production is known.\n */\nconst productionError = (production: Production): XmlError | null => {\n if (PRODUCTIONS.includes(production)) return null;\n return new XmlError({\n reason: { _tag: 'InvalidProduction', production, expected: PRODUCTIONS.join(', ') },\n message: `Unknown production \"${production}\". Must be one of: ${PRODUCTIONS.join(', ')}`,\n });\n};\n\nconst validators: Record<Production, (str: string, opts?: ValidationOptions) => boolean> = {\n name: isName,\n ncName: isNcName,\n qName: isQName,\n nmToken: isNmToken,\n nmTokens: isNmTokens,\n};\n\n/**\n * @description The diagnostic body {@link validate} reports, with the production already known to be valid. Kept separate so the reason-finding logic carries no\n * unknown-production check and the batch path can map over it without re-entering the guard per element.\n *\n * @param str - The candidate name.\n * @param production - The production to validate against, already checked.\n * @param xmlVersion - Which version's character classes to use.\n * @param asciiOnly - Whether the ASCII-only fast path applied.\n *\n * @returns The discriminated result.\n */\nconst diagnose = (str: string, production: Production, xmlVersion: XmlVersion, asciiOnly: boolean): ValidationResult =>\n diagnoseWith(str, production, validators[production](str, { xmlVersion, asciiOnly }), asciiOnly);\n\n/**\n * @description The colon-specific reason a `qName` or `ncName` failed, or `undefined` when the failure is not about a colon. The three QName forms are checked in\n * the order they can co-occur: a string that both starts and ends with a colon is reported as the leading one, and a two-colon string is reported as\n * the count.\n *\n * @param str - The candidate name.\n * @param production - The production checked.\n *\n * @returns The reason and position, or `undefined`.\n */\nconst diagnoseColon = (str: string, production: Production): { reason: string; position: number } | undefined => {\n if (production === 'ncName' && str.includes(':')) {\n return { reason: 'Colon is not allowed in NCName', position: str.indexOf(':') };\n }\n\n if (production !== 'qName') {\n return undefined;\n }\n if (str.startsWith(':')) {\n return { reason: 'QName cannot start with a colon', position: 0 };\n }\n if (str.endsWith(':')) {\n return { reason: 'QName cannot end with a colon', position: str.length - 1 };\n }\n if ((str.match(/:/g) ?? []).length > 1) {\n return { reason: 'QName can have at most one colon', position: str.lastIndexOf(':') };\n }\n return undefined;\n};\n\n/**\n * @description The first character that is not a legal `NameChar`, and where it sits.\n *\n * @param str - The candidate name.\n * @param namePattern - The `NameChar` test for the character set the validator used.\n *\n * @returns The reason and position, or `undefined` when every character is legal.\n */\nconst diagnoseNameChar = (str: string, namePattern: RegExp): { reason: string; position: number } | undefined => {\n for (let i = 0; i < str.length; i++) {\n const char = str[i];\n if (char !== undefined && !namePattern.test(char)) {\n return { reason: `Character \"${char}\" at position ${i} is not a valid NameChar`, position: i };\n }\n }\n return undefined;\n};\n\n/**\n * @description Why a name failed, once whether it failed is already known. The first character outranks the rest: a name that cannot start is reported as a bad\n * `NameStartChar` even when a later character is also illegal.\n *\n * @param str - The candidate name.\n * @param production - The production checked.\n * @param startCharPattern - The `NameStartChar` test for the character set the validator used.\n * @param namePattern - The `NameChar` test for the same character set.\n *\n * @returns The reason, and the offending position, which is `undefined` for a structural failure.\n */\nconst findFailure = (\n str: string,\n production: Production,\n startCharPattern: RegExp,\n namePattern: RegExp\n): { reason: string; position: number | undefined } => {\n if (str.length === 0) {\n return { reason: 'Input is empty', position: undefined };\n }\n\n const colon = diagnoseColon(str, production);\n if (colon !== undefined) return colon;\n\n const firstChar = str[0];\n if (['name', 'ncName', 'qName'].includes(production) && !startCharPattern.test(firstChar ?? '')) {\n return { reason: `First character \"${firstChar}\" is not a valid NameStartChar`, position: 0 };\n }\n\n return diagnoseNameChar(str, namePattern) ?? { reason: 'Does not match the production rules', position: undefined };\n};\n\n/**\n * @description Why a name failed, once whether it failed is already known. Split from {@link diagnose} so the effect that asks the question stays a one-liner and\n * the reason-finding is plain.\n *\n * @param str - The candidate name.\n * @param production - The production checked.\n * @param isValid - Whether it passed.\n * @param asciiOnly - Whether the ASCII-only fast path applied.\n *\n * @returns The discriminated result.\n */\nconst diagnoseWith = (str: string, production: Production, isValid: boolean, asciiOnly: boolean): ValidationResult => {\n if (isValid) return { valid: true, production, input: str };\n\n // Diagnostic fallback char checks must mirror the same character set the\n // boolean validator above used, or the reported reason/position could\n // contradict the `valid: false` result (e.g. flagging a char as illegal\n // that the unicode-aware check would have accepted).\n const startCharPattern = asciiOnly ? /^[:A-Za-z_]/ : /^[:A-Za-z_\\u00C0-\\uFFFD]/;\n const namePattern = asciiOnly ? /[\\w\\-\\\\.:]/ : /[\\w\\-\\\\.:\\u00B7\\u00C0-\\uFFFD]/;\n\n return { valid: false, production, input: str, ...findFailure(str, production, startCharPattern, namePattern) };\n};\n\n/**\n * @description Validates a string against a named production and, on failure, reports why and where.\n *\n * @example\n * ```typescript\n * import { Effect } from 'effect';\n * import { validate } from '@endevops/effect-codec-xml';\n *\n * Effect.runSync(validate('not a name', 'ncName'));\n * // { valid: false, production: 'ncName', input: 'not a name', reason: 'First character \" \" is not a valid NameStartChar', position: 0 }\n * ```;\n *\n * @param str - The candidate name.\n * @param production - The production to validate against.\n * @param opts - Version and ASCII-only selection, as for the boolean validators.\n *\n * @returns An effect producing a discriminated result: the plain triple when valid, or the offending `reason` and `position` when not. A name that\n * fails to validate is a `valid: false` result, not a failure: an invalid name is the question being answered. The effect fails only with an\n * {@link XmlError} and the `InvalidProduction` reason for an unknown production, which is unreachable from TypeScript and is the guard for untyped\n * JavaScript callers.\n */\nexport const validate = Effect.fnUntraced(function* (\n str: string,\n production: Production,\n { xmlVersion = '1.0', asciiOnly = false }: ValidationOptions = {}\n): Effect.fn.Return<ValidationResult, XmlError> {\n const invalid = productionError(production);\n if (invalid) return yield* invalid;\n return diagnose(str, production, xmlVersion, asciiOnly);\n});\n\n// ---------------------------------------------------------------------------\n// Sanitizer\n// ---------------------------------------------------------------------------\n\n/**\n * @description Transforms an invalid string into the nearest valid XML name for the given production: strips or replaces illegal characters, fixes an invalid\n * start character by prepending the replacement, and removes colons for NCName.\n *\n * @param str - The candidate name.\n * @param production - The production to sanitize for. Defaults to `'name'`.\n * @param opts - `replacement` is the substitute character (default `'_'`); `asciiOnly` also replaces non-ASCII characters.\n *\n * @returns A string that satisfies `production` for the ASCII range, or the nearest approximation of it.\n */\nexport const sanitize = (str: string, production: Production = 'name', { replacement = '_', asciiOnly = false }: SanitizeOptions = {}): string => {\n if (!str) return replacement;\n\n let result = str;\n\n // Strip colons for NCName\n if (production === 'ncName') {\n result = result.replace(/:/g, '');\n }\n\n // Replace illegal characters\n const allowedCharPattern = asciiOnly ? /[^\\w\\-.:]/g : /[^\\w\\-.:\\u00B7\\u00C0-\\uFFFD]/g;\n result = result.replace(allowedCharPattern, replacement);\n\n // Fix invalid start character for Name / NCName / QName\n if (production !== 'nmToken' && production !== 'nmTokens') {\n if (/^[-.\\d]/.test(result)) {\n result = replacement + result;\n }\n }\n\n return result || replacement;\n};\n"],"mappings":"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"}
package/dist/parse.d.ts CHANGED
@@ -26,7 +26,7 @@ export interface XmlParseOptions {
26
26
  * @description Keep the whitespace at the edges of every text run.
27
27
  *
28
28
  * @default false\
29
- * which trims it — and trimming is what makes a pretty-printed document
29
+ * which trims it. Trimming makes a pretty-printed document
30
30
  * read as the same value as an unindented one, because the indentation around a child element and around a closing tag lands at the edges of its
31
31
  * parent's text. Whitespace _inside_ a run is content and is never touched either way, so `'one two'` and a paragraph with a newline in the middle
32
32
  * of it survive. Set it to `true` to keep leading and trailing spaces in text exactly as written, at the cost of a document that was laid out on
@@ -56,13 +56,13 @@ export interface XmlParseOptions {
56
56
  /**
57
57
  * @description Parses an XML document into its root element's content.\
58
58
  * The walk itself is synchronous, but it reports a malformed document by failing with an {@link XmlParseError} rather than by throwing, so the failure lands in the effect's error channel where `catchTag`, `retry` and a fallback can all
59
- * see it. A failed parse is an expected outcome of reading untrusted text — it is what those combinators key off — and only a defect would hide it.
59
+ * see it. A failed parse is an expected outcome of reading untrusted text, and those combinators key off it, so only a defect would hide it.
60
60
  * The span is the boundary a performance trace hangs off: it carries the document's length, which is the size that drives the parser's cost, so a
61
61
  * slow parse in a profile can be attributed to the input that produced it. A caller that wants the value outside an `Effect` uses
62
62
  * {@link parseXmlDocument}, which runs the same walk synchronously and throws instead. The walk is plain recursive descent rather than a chain of
63
- * `yield*`es. Publicly `parseXml` is still an `Effect` — it suspends the walk so it runs lazily under the span, and folds the failure the walk throws
64
- * into the typed error channel — but inside a document there is no effect boundary per tag, attribute or text run. A 500-row report is thousands of
65
- * those, and a fiber step for each of them was most of what the `parse 500 rows` row measured. The typed failure survives: the walk throws an
63
+ * `yield*`es. Publicly `parseXml` is still an `Effect`: it suspends the walk so it runs lazily under the span, and folds the failure the walk throws
64
+ * into the typed error channel, but inside a document there is no effect boundary per tag, attribute or text run. A 500-row report is thousands of
65
+ * those, and one fiber step per construct dominated the `parse 500 rows` benchmark. The typed failure survives: the walk throws an
66
66
  * {@link XmlParseError} and `parseXml` catches it into `Effect.fail`.
67
67
  *
68
68
  * @param text - The document to read.
package/dist/parse.js CHANGED
@@ -7,13 +7,13 @@ const decoder = EntityDecoder.make().pipe(Effect.runSync);
7
7
  /**
8
8
  * @description Parses an XML document into its root element's content.\
9
9
  * The walk itself is synchronous, but it reports a malformed document by failing with an {@link XmlParseError} rather than by throwing, so the failure lands in the effect's error channel where `catchTag`, `retry` and a fallback can all
10
- * see it. A failed parse is an expected outcome of reading untrusted text — it is what those combinators key off — and only a defect would hide it.
10
+ * see it. A failed parse is an expected outcome of reading untrusted text, and those combinators key off it, so only a defect would hide it.
11
11
  * The span is the boundary a performance trace hangs off: it carries the document's length, which is the size that drives the parser's cost, so a
12
12
  * slow parse in a profile can be attributed to the input that produced it. A caller that wants the value outside an `Effect` uses
13
13
  * {@link parseXmlDocument}, which runs the same walk synchronously and throws instead. The walk is plain recursive descent rather than a chain of
14
- * `yield*`es. Publicly `parseXml` is still an `Effect` — it suspends the walk so it runs lazily under the span, and folds the failure the walk throws
15
- * into the typed error channel — but inside a document there is no effect boundary per tag, attribute or text run. A 500-row report is thousands of
16
- * those, and a fiber step for each of them was most of what the `parse 500 rows` row measured. The typed failure survives: the walk throws an
14
+ * `yield*`es. Publicly `parseXml` is still an `Effect`: it suspends the walk so it runs lazily under the span, and folds the failure the walk throws
15
+ * into the typed error channel, but inside a document there is no effect boundary per tag, attribute or text run. A 500-row report is thousands of
16
+ * those, and one fiber step per construct dominated the `parse 500 rows` benchmark. The typed failure survives: the walk throws an
17
17
  * {@link XmlParseError} and `parseXml` catches it into `Effect.fail`.
18
18
  *
19
19
  * @param text - The document to read.
@@ -74,7 +74,7 @@ const parseDocument = (text, options) => {
74
74
  xmlVersion: resolved.xmlVersion
75
75
  };
76
76
  /**
77
- * @description Names already resolved by this parse. A document repeats names — every one of five hundred rows has a `sku` — and a validator that ran per
77
+ * @description Names already resolved by this parse. A document repeats names (every one of five hundred rows has a `sku`), and a validator that ran per
78
78
  * occurrence would pay for the same answer five hundred times.
79
79
  */
80
80
  const nameCache = /* @__PURE__ */ new Map();
@@ -122,7 +122,7 @@ const parseDocument = (text, options) => {
122
122
  });
123
123
  };
124
124
  /**
125
- * @description Consumes whitespace, comments, processing instructions and a DOCTYPE, leaving the cursor on the first character that is none of them — or at the
125
+ * @description Consumes whitespace, comments, processing instructions and a DOCTYPE, leaving the cursor on the first character that is none of them, or at the
126
126
  * end of the document.
127
127
  */
128
128
  const skipMisc = () => {
@@ -279,8 +279,8 @@ const parseDocument = (text, options) => {
279
279
  /**
280
280
  * @description What the cursor is sitting on inside an element's body. The two things the loop cannot read are refused here rather than in it: running out of
281
281
  * document and a declaration, which is markup the parser does not accept inside an element. Recognising the constructs that _are_ read is the rest,
282
- * and the order is the one that rules out the shorter prefixes first — `</` before `<?` before any other `<!`, and `<![CDATA[` before the `<!` that
283
- * would otherwise match it.
282
+ * and the order rules out the shorter prefixes first: `</` before `<?` before any other `<!`, and `<![CDATA[` before the `<!` that would otherwise
283
+ * match it.
284
284
  *
285
285
  * @param name - The name the enclosing element's start tag gave it, for the unterminated-body message.
286
286
  *
@@ -305,7 +305,7 @@ const parseDocument = (text, options) => {
305
305
  return "child";
306
306
  };
307
307
  /**
308
- * @description Consumes a `</name>`, checking on the way that it is the tag that closes this element and that it is well-formed.
308
+ * @description Consumes a `</name>`, checking as it goes that it is the tag that closes this element and that it is well-formed.
309
309
  *
310
310
  * @param name - The name the start tag gave the element, which the closing tag has to match.
311
311
  */
@@ -339,7 +339,7 @@ const parseDocument = (text, options) => {
339
339
  return run;
340
340
  };
341
341
  /**
342
- * @description Reads a `<![CDATA[…]]>` section. CDATA is character data, and character data is what it holds, so it joins the element's text as it stands — the
342
+ * @description Reads a `<![CDATA[…]]>` section. CDATA is character data, and character data is what it holds, so it joins the element's text as it stands. The
343
343
  * entities in it are literal text and must not be expanded.
344
344
  *
345
345
  * @returns The section's contents.
@@ -417,10 +417,10 @@ const parseDocumentResult = (text, options) => {
417
417
  }
418
418
  };
419
419
  /**
420
- * @description Decodes character references, falling back to the raw text when the reference is not one the decoder recognises. The fallback is what makes a bare
421
- * `&` survivable: the decoder treats it as a malformed reference and fails, and a document containing one is far more likely to be worth reading than
422
- * to be rejected. The `&` is escaped on the way out, so the value still round-trips. The decoder answers with an `Effect`, and this is the one place
423
- * a parse still runs one. It is only reached when the raw text holds an `&` — the common case returns before it — and the effect is synchronous, so
420
+ * @description Decodes character references, falling back to the raw text when the reference is not one the decoder recognises. The fallback keeps a bare `&`
421
+ * survivable: the decoder treats it as a malformed reference and fails, and a document containing one is far more likely to be worth reading than to
422
+ * be rejected. The `&` is escaped on the way out, so the value still round-trips. The decoder answers with an `Effect`, and this is the one place a
423
+ * parse still runs one. It is only reached when the raw text holds an `&`, since the common case returns before it, and the effect is synchronous, so
424
424
  * the run is cheap next to the decoder's own work.
425
425
  *
426
426
  * @param raw - Text read straight from the source, with references unexpanded.