diff --git a/lib/fxp.d.cts b/lib/fxp.d.cts index 1c349d60..99413f2f 100644 --- a/lib/fxp.d.cts +++ b/lib/fxp.d.cts @@ -1,314 +1,451 @@ -type ProcessEntitiesOptions = { - /** - * Whether to enable entity processing - * - * Defaults to `true` - */ - enabled?: boolean; - - /** - * Maximum size in characters for a single entity definition - * - * Defaults to `10000` - */ - maxEntitySize?: number; - - /** - * Maximum depth for nested entity references (reserved for future use) - * - * Defaults to `10` - */ - maxExpansionDepth?: number; - - /** - * Maximum total number of entity expansions allowed - * - * Defaults to `1000` - */ - maxTotalExpansions?: number; - - /** - * Maximum total expanded content length in characters - * - * Defaults to `100000` - */ - maxExpandedLength?: number; - - /** - * Maximum number of entities allowed in the XML - * - * Defaults to `100` - */ - maxEntityCount?: number; - - /** - * Array of tag names where entity replacement is allowed. - * If null, entities are replaced in all tags. - * - * Defaults to `null` - */ - allowedTags?: string[] | null; +//import type { Matcher, Expression } from 'path-expression-matcher'; - /** - * Custom filter function to determine if entities should be replaced in a tag - * - * @param tagName - The name of the current tag - * @param jPath - The jPath of the current tag - * @returns `true` to allow entity replacement, `false` to skip - * - * Defaults to `null` - */ - tagFilter?: ((tagName: string, jPath: string) => boolean) | null; -}; +type Matcher = unknown; +type Expression = unknown; -type X2jOptions = { +/** + * Base options shared by both jPath modes + */ +export type X2jOptionsBase = { /** * Preserve the order of tags in resulting JS object - * + * * Defaults to `false` */ preserveOrder?: boolean; /** * Give a prefix to the attribute name in the resulting JS object - * + * * Defaults to '@_' */ attributeNamePrefix?: string; /** * A name to group all attributes of a tag under, or `false` to disable - * + * * Defaults to `false` */ attributesGroupName?: false | string; /** * The name of the next node in the resulting JS - * + * * Defaults to `#text` */ textNodeName?: string; - /** - * Whether to ignore attributes when parsing - * - * When `true` - ignores all the attributes - * - * When `false` - parses all the attributes - * - * When `Array` - filters out attributes that match provided patterns - * - * When `Function` - calls the function for each attribute and filters out those for which the function returned `true` - * - * Defaults to `true` - */ - ignoreAttributes?: boolean | (string | RegExp)[] | ((attrName: string, jPath: string) => boolean); - /** * Whether to remove namespace string from tag and attribute names - * + * * Defaults to `false` */ removeNSPrefix?: boolean; /** * Whether to allow attributes without value - * + * * Defaults to `false` */ allowBooleanAttributes?: boolean; /** * Whether to parse tag value with `strnum` package - * + * * Defaults to `true` */ parseTagValue?: boolean; /** * Whether to parse attribute value with `strnum` package - * + * * Defaults to `false` */ parseAttributeValue?: boolean; /** * Whether to remove surrounding whitespace from tag or attribute value - * + * * Defaults to `true` */ trimValues?: boolean; /** * Give a property name to set CDATA values to instead of merging to tag's text value - * + * * Defaults to `false` */ cdataPropName?: false | string; /** * If set, parse comments and set as this property - * + * * Defaults to `false` */ commentPropName?: false | string; - /** - * Control how tag value should be parsed. Called only if tag value is not empty - * - * @returns {undefined|null} `undefined` or `null` to set original value. - * @returns {unknown} - * - * 1. Different value or value with different data type to set new value. - * 2. Same value to set parsed value if `parseTagValue: true`. - * - * Defaults to `(tagName, val, jPath, hasAttributes, isLeafNode) => val` - */ - tagValueProcessor?: (tagName: string, tagValue: string, jPath: string, hasAttributes: boolean, isLeafNode: boolean) => unknown; - - /** - * Control how attribute value should be parsed - * - * @param attrName - * @param attrValue - * @param jPath - * @returns {undefined|null} `undefined` or `null` to set original value - * @returns {unknown} - * - * Defaults to `(attrName, val, jPath) => val` - */ - attributeValueProcessor?: (attrName: string, attrValue: string, jPath: string) => unknown; - /** * Options to pass to `strnum` for parsing numbers - * + * * Defaults to `{ hex: true, leadingZeros: true, eNotation: true }` */ numberParseOptions?: strnumOptions; /** * Nodes to stop parsing at - * + * + * Accepts string patterns or Expression objects from path-expression-matcher + * + * String patterns starting with "*." are automatically converted to ".." for backward compatibility + * * Defaults to `[]` */ - stopNodes?: string[]; + stopNodes?: (string | Expression)[]; /** * List of tags without closing tags - * + * * Defaults to `[]` */ unpairedTags?: string[]; /** * Whether to always create a text node - * + * * Defaults to `false` */ alwaysCreateTextNode?: boolean; - /** - * Determine whether a tag should be parsed as an array - * - * @param tagName - * @param jPath - * @param isLeafNode - * @param isAttribute - * @returns {boolean} - * - * Defaults to `() => false` - */ - isArray?: (tagName: string, jPath: string, isLeafNode: boolean, isAttribute: boolean) => boolean; - /** * Whether to process default and DOCTYPE entities - * + * * When `true` - enables entity processing with default limits - * + * * When `false` - disables all entity processing - * + * * When `ProcessEntitiesOptions` - enables entity processing with custom configuration - * + * * Defaults to `true` */ processEntities?: boolean | ProcessEntitiesOptions; /** * Whether to process HTML entities - * + * * Defaults to `false` */ htmlEntities?: boolean; /** * Whether to ignore the declaration tag from output - * + * * Defaults to `false` */ ignoreDeclaration?: boolean; /** * Whether to ignore Pi tags - * + * * Defaults to `false` */ ignorePiTags?: boolean; /** * Transform tag names - * + * * Defaults to `false` */ transformTagName?: ((tagName: string) => string) | false; /** * Transform attribute names - * + * * Defaults to `false` */ transformAttributeName?: ((attributeName: string) => string) | false; + /** + * If true, adds a Symbol to all object nodes, accessible by {@link XMLParser.getMetaDataSymbol} with + * metadata about each the node in the XML file. + */ + captureMetaData?: boolean; + + /** + * Maximum number of nested tags + * + * Defaults to `100` + */ + maxNestedTags?: number; + + /** + * Whether to strictly validate tag names + * + * Defaults to `true` + */ + strictReservedNames?: boolean; + + /** + * Function to sanitize dangerous property names + * + * @param name - The name of the property + * @returns {string} The sanitized name + * + * Defaults to `(name) => __name` + */ + onDangerousProperty?: (name: string) => string; +}; + +/** + * Options when jPath is true (default) - callbacks receive jPath as string + */ +export type X2jOptionsWithJPathString = X2jOptionsBase & { + /** + * Controls whether callbacks receive jPath as string or Matcher instance + * + * When `true` - callbacks receive jPath as string (backward compatible) + * + * Defaults to `true` + */ + jPath?: true; + + /** + * Whether to ignore attributes when parsing + * + * When `true` - ignores all the attributes + * + * When `false` - parses all the attributes + * + * When `Array` - filters out attributes that match provided patterns + * + * When `Function` - calls the function for each attribute and filters out those for which the function returned `true` + * + * Defaults to `true` + */ + ignoreAttributes?: boolean | (string | RegExp)[] | ((attrName: string, jPath: string) => boolean); + + /** + * Control how tag value should be parsed. Called only if tag value is not empty + * + * @param tagName - The name of the tag + * @param tagValue - The value of the tag + * @param jPath - The jPath string representing the tag's location + * @param hasAttributes - Whether the tag has attributes + * @param isLeafNode - Whether the tag is a leaf node + * @returns {undefined|null} `undefined` or `null` to set original value. + * @returns {unknown} + * + * 1. Different value or value with different data type to set new value. + * 2. Same value to set parsed value if `parseTagValue: true`. + * + * Defaults to `(tagName, val, jPath, hasAttributes, isLeafNode) => val` + */ + tagValueProcessor?: (tagName: string, tagValue: string, jPath: string, hasAttributes: boolean, isLeafNode: boolean) => unknown; + + /** + * Control how attribute value should be parsed + * + * @param attrName - The name of the attribute + * @param attrValue - The value of the attribute + * @param jPath - The jPath string representing the attribute's location + * @returns {undefined|null} `undefined` or `null` to set original value + * @returns {unknown} + * + * Defaults to `(attrName, val, jPath) => val` + */ + attributeValueProcessor?: (attrName: string, attrValue: string, jPath: string) => unknown; + + /** + * Determine whether a tag should be parsed as an array + * + * @param tagName - The name of the tag + * @param jPath - The jPath string representing the tag's location + * @param isLeafNode - Whether the tag is a leaf node + * @param isAttribute - Whether this is an attribute + * @returns {boolean} + * + * Defaults to `() => false` + */ + isArray?: (tagName: string, jPath: string, isLeafNode: boolean, isAttribute: boolean) => boolean; + /** * Change the tag name when a different name is returned. Skip the tag from parsed result when false is returned. * Modify `attrs` object to control attributes for the given tag. - * + * + * @param tagName - The name of the tag + * @param jPath - The jPath string representing the tag's location + * @param attrs - The attributes object * @returns {string} new tag name. * @returns false to skip the tag - * + * * Defaults to `(tagName, jPath, attrs) => tagName` */ updateTag?: (tagName: string, jPath: string, attrs: { [k: string]: string }) => string | boolean; +}; +/** + * Options when jPath is false - callbacks receive Matcher instance for advanced pattern matching + */ +export type X2jOptionsWithMatcher = X2jOptionsBase & { /** - * If true, adds a Symbol to all object nodes, accessible by {@link XMLParser.getMetaDataSymbol} with - * metadata about each the node in the XML file. + * Controls whether callbacks receive jPath as string or Matcher instance + * + * When `false` - callbacks receive Matcher instance for advanced pattern matching */ - captureMetaData?: boolean; + jPath: false; /** - * Maximum number of nested tags + * Whether to ignore attributes when parsing + * + * When `true` - ignores all the attributes + * + * When `false` - parses all the attributes + * + * When `Array` - filters out attributes that match provided patterns + * + * When `Function` - calls the function for each attribute and filters out those for which the function returned `true` + * + * Defaults to `true` + */ + ignoreAttributes?: boolean | (string | RegExp)[] | ((attrName: string, matcher: Matcher) => boolean); + + /** + * Control how tag value should be parsed. Called only if tag value is not empty + * + * @param tagName - The name of the tag + * @param tagValue - The value of the tag + * @param matcher - The Matcher instance for advanced pattern matching + * @param hasAttributes - Whether the tag has attributes + * @param isLeafNode - Whether the tag is a leaf node + * @returns {undefined|null} `undefined` or `null` to set original value. + * @returns {unknown} + * + * 1. Different value or value with different data type to set new value. + * 2. Same value to set parsed value if `parseTagValue: true`. + * + * Defaults to `(tagName, val, matcher, hasAttributes, isLeafNode) => val` + */ + tagValueProcessor?: (tagName: string, tagValue: string, matcher: Matcher, hasAttributes: boolean, isLeafNode: boolean) => unknown; + + /** + * Control how attribute value should be parsed + * + * @param attrName - The name of the attribute + * @param attrValue - The value of the attribute + * @param matcher - The Matcher instance for advanced pattern matching + * @returns {undefined|null} `undefined` or `null` to set original value + * @returns {unknown} + * + * Defaults to `(attrName, val, matcher) => val` + */ + attributeValueProcessor?: (attrName: string, attrValue: string, matcher: Matcher) => unknown; + + /** + * Determine whether a tag should be parsed as an array + * + * @param tagName - The name of the tag + * @param matcher - The Matcher instance for advanced pattern matching + * @param isLeafNode - Whether the tag is a leaf node + * @param isAttribute - Whether this is an attribute + * @returns {boolean} + * + * Defaults to `() => false` + */ + isArray?: (tagName: string, matcher: Matcher, isLeafNode: boolean, isAttribute: boolean) => boolean; + + /** + * Change the tag name when a different name is returned. Skip the tag from parsed result when false is returned. + * Modify `attrs` object to control attributes for the given tag. + * + * @param tagName - The name of the tag + * @param matcher - The Matcher instance for advanced pattern matching + * @param attrs - The attributes object + * @returns {string} new tag name. + * @returns false to skip the tag + * + * Defaults to `(tagName, matcher, attrs) => tagName` + */ + updateTag?: (tagName: string, matcher: Matcher, attrs: { [k: string]: string }) => string | boolean; +}; + +/** + * XMLParser options - a discriminated union based on the jPath option + * + * When jPath is true (default) or undefined, callbacks receive jPath as a string. + * When jPath is false, callbacks receive a Matcher instance for advanced pattern matching. + */ +export type X2jOptions = X2jOptionsWithJPathString | X2jOptionsWithMatcher; + +export type ProcessEntitiesOptions = { + /** + * Whether to enable entity processing + * + * Defaults to `true` + */ + enabled?: boolean; + + /** + * Maximum size in characters for a single entity definition + * + * Defaults to `10000` + */ + maxEntitySize?: number; + + /** + * Maximum depth for nested entity references (reserved for future use) + * + * Defaults to `10` + */ + maxExpansionDepth?: number; + + /** + * Maximum total number of entity expansions allowed + * + * Defaults to `1000` + */ + maxTotalExpansions?: number; + + /** + * Maximum total expanded content length in characters + * + * Defaults to `100000` + */ + maxExpandedLength?: number; + + /** + * Maximum number of entities allowed in the XML * * Defaults to `100` */ - maxNestedTags?: number; + maxEntityCount?: number; /** - * Whether to strictly validate tag names + * Array of tag names where entity replacement is allowed. + * If null, entities are replaced in all tags. * - * Defaults to `true` + * Defaults to `null` */ - strictReservedNames?: boolean; + allowedTags?: string[] | null; + + /** + * Custom filter function to determine if entities should be replaced in a tag + * + * @param tagName - The name of the current tag + * @param jPathOrMatcher - The jPath string (if jPath: true) or Matcher instance (if jPath: false) + * @returns `true` to allow entity replacement, `false` to skip + * + * Defaults to `null` + */ + tagFilter?: ((tagName: string, jPathOrMatcher: string | Matcher) => boolean) | null; }; -type strnumOptions = { +export type strnumOptions = { hex: boolean; leadingZeros: boolean, skipLike?: RegExp, eNotation?: boolean } -type validationOptions = { +export type validationOptions = { /** * Whether to allow attributes without value * @@ -324,7 +461,7 @@ type validationOptions = { unpairedTags?: string[]; }; -type XmlBuilderOptions = { +export type XmlBuilderOptions = { /** * Give a prefix to the attribute name in the resulting JS object * @@ -435,9 +572,11 @@ type XmlBuilderOptions = { /** * Nodes to stop parsing at * + * Accepts string patterns or Expression objects from path-expression-matcher + * * Defaults to `[]` */ - stopNodes?: string[]; + stopNodes?: (string | Expression)[]; /** * Control how tag value should be parsed. Called only if tag value is not empty @@ -474,11 +613,18 @@ type XmlBuilderOptions = { oneListGroup?: boolean; + + /** + * Maximum number of nested tags + * + * Defaults to `100` + */ + maxNestedTags?: number; }; type ESchema = string | object | Array; -type ValidationError = { +export type ValidationError = { err: { code: string; msg: string, @@ -487,7 +633,7 @@ type ValidationError = { }; }; -declare class XMLParser { +export class XMLParser { constructor(options?: X2jOptions); parse(xmlData: string | Uint8Array, validationOptions?: validationOptions | boolean): any; /** @@ -510,39 +656,19 @@ declare class XMLParser { static getMetaDataSymbol(): Symbol; } -declare class XMLValidator { +export class XMLValidator { static validate(xmlData: string, options?: validationOptions): true | ValidationError; } - -declare class XMLBuilder { +export class XMLBuilder { constructor(options?: XmlBuilderOptions); build(jObj: any): string; } - /** * This object is available on nodes via the symbol {@link XMLParser.getMetaDataSymbol} * when {@link X2jOptions.captureMetaData} is true. */ -declare interface XMLMetaData { +export interface XMLMetaData { /** The index, if available, of the character where the XML node began in the input stream. */ startIndex?: number; -} - -declare namespace fxp { - export { - XMLParser, - XMLValidator, - XMLBuilder, - XMLMetaData, - XmlBuilderOptions, - X2jOptions, - ESchema, - ValidationError, - strnumOptions, - validationOptions, - ProcessEntitiesOptions, - } -} - -export = fxp; +} \ No newline at end of file diff --git a/src/fxp.d.ts b/src/fxp.d.ts index f0048cb8..99413f2f 100644 --- a/src/fxp.d.ts +++ b/src/fxp.d.ts @@ -1,307 +1,442 @@ -export type ProcessEntitiesOptions = { - /** - * Whether to enable entity processing - * - * Defaults to `true` - */ - enabled?: boolean; - - /** - * Maximum size in characters for a single entity definition - * - * Defaults to `10000` - */ - maxEntitySize?: number; - - /** - * Maximum depth for nested entity references (reserved for future use) - * - * Defaults to `10` - */ - maxExpansionDepth?: number; - - /** - * Maximum total number of entity expansions allowed - * - * Defaults to `1000` - */ - maxTotalExpansions?: number; - - /** - * Maximum total expanded content length in characters - * - * Defaults to `100000` - */ - maxExpandedLength?: number; - - /** - * Maximum number of entities allowed in the XML - * - * Defaults to `100` - */ - maxEntityCount?: number; - - /** - * Array of tag names where entity replacement is allowed. - * If null, entities are replaced in all tags. - * - * Defaults to `null` - */ - allowedTags?: string[] | null; +//import type { Matcher, Expression } from 'path-expression-matcher'; - /** - * Custom filter function to determine if entities should be replaced in a tag - * - * @param tagName - The name of the current tag - * @param jPath - The jPath of the current tag - * @returns `true` to allow entity replacement, `false` to skip - * - * Defaults to `null` - */ - tagFilter?: ((tagName: string, jPath: string) => boolean) | null; -}; +type Matcher = unknown; +type Expression = unknown; -export type X2jOptions = { +/** + * Base options shared by both jPath modes + */ +export type X2jOptionsBase = { /** * Preserve the order of tags in resulting JS object - * + * * Defaults to `false` */ preserveOrder?: boolean; /** * Give a prefix to the attribute name in the resulting JS object - * + * * Defaults to '@_' */ attributeNamePrefix?: string; /** * A name to group all attributes of a tag under, or `false` to disable - * + * * Defaults to `false` */ attributesGroupName?: false | string; /** * The name of the next node in the resulting JS - * + * * Defaults to `#text` */ textNodeName?: string; - /** - * Whether to ignore attributes when parsing - * - * When `true` - ignores all the attributes - * - * When `false` - parses all the attributes - * - * When `Array` - filters out attributes that match provided patterns - * - * When `Function` - calls the function for each attribute and filters out those for which the function returned `true` - * - * Defaults to `true` - */ - ignoreAttributes?: boolean | (string | RegExp)[] | ((attrName: string, jPath: string) => boolean); - /** * Whether to remove namespace string from tag and attribute names - * + * * Defaults to `false` */ removeNSPrefix?: boolean; /** * Whether to allow attributes without value - * + * * Defaults to `false` */ allowBooleanAttributes?: boolean; /** * Whether to parse tag value with `strnum` package - * + * * Defaults to `true` */ parseTagValue?: boolean; /** * Whether to parse attribute value with `strnum` package - * + * * Defaults to `false` */ parseAttributeValue?: boolean; /** * Whether to remove surrounding whitespace from tag or attribute value - * + * * Defaults to `true` */ trimValues?: boolean; /** * Give a property name to set CDATA values to instead of merging to tag's text value - * + * * Defaults to `false` */ cdataPropName?: false | string; /** * If set, parse comments and set as this property - * + * * Defaults to `false` */ commentPropName?: false | string; - /** - * Control how tag value should be parsed. Called only if tag value is not empty - * - * @returns {undefined|null} `undefined` or `null` to set original value. - * @returns {unknown} - * - * 1. Different value or value with different data type to set new value. - * 2. Same value to set parsed value if `parseTagValue: true`. - * - * Defaults to `(tagName, val, jPath, hasAttributes, isLeafNode) => val` - */ - tagValueProcessor?: (tagName: string, tagValue: string, jPath: string, hasAttributes: boolean, isLeafNode: boolean) => unknown; - - /** - * Control how attribute value should be parsed - * - * @param attrName - * @param attrValue - * @param jPath - * @returns {undefined|null} `undefined` or `null` to set original value - * @returns {unknown} - * - * Defaults to `(attrName, val, jPath) => val` - */ - attributeValueProcessor?: (attrName: string, attrValue: string, jPath: string) => unknown; - /** * Options to pass to `strnum` for parsing numbers - * + * * Defaults to `{ hex: true, leadingZeros: true, eNotation: true }` */ numberParseOptions?: strnumOptions; /** * Nodes to stop parsing at - * + * + * Accepts string patterns or Expression objects from path-expression-matcher + * + * String patterns starting with "*." are automatically converted to ".." for backward compatibility + * * Defaults to `[]` */ - stopNodes?: string[]; + stopNodes?: (string | Expression)[]; /** * List of tags without closing tags - * + * * Defaults to `[]` */ unpairedTags?: string[]; /** * Whether to always create a text node - * + * * Defaults to `false` */ alwaysCreateTextNode?: boolean; - /** - * Determine whether a tag should be parsed as an array - * - * @param tagName - * @param jPath - * @param isLeafNode - * @param isAttribute - * @returns {boolean} - * - * Defaults to `() => false` - */ - isArray?: (tagName: string, jPath: string, isLeafNode: boolean, isAttribute: boolean) => boolean; - /** * Whether to process default and DOCTYPE entities - * + * * When `true` - enables entity processing with default limits - * + * * When `false` - disables all entity processing - * + * * When `ProcessEntitiesOptions` - enables entity processing with custom configuration - * + * * Defaults to `true` */ processEntities?: boolean | ProcessEntitiesOptions; /** * Whether to process HTML entities - * + * * Defaults to `false` */ htmlEntities?: boolean; /** * Whether to ignore the declaration tag from output - * + * * Defaults to `false` */ ignoreDeclaration?: boolean; /** * Whether to ignore Pi tags - * + * * Defaults to `false` */ ignorePiTags?: boolean; /** * Transform tag names - * + * * Defaults to `false` */ transformTagName?: ((tagName: string) => string) | false; /** * Transform attribute names - * + * * Defaults to `false` */ transformAttributeName?: ((attributeName: string) => string) | false; + /** + * If true, adds a Symbol to all object nodes, accessible by {@link XMLParser.getMetaDataSymbol} with + * metadata about each the node in the XML file. + */ + captureMetaData?: boolean; + + /** + * Maximum number of nested tags + * + * Defaults to `100` + */ + maxNestedTags?: number; + + /** + * Whether to strictly validate tag names + * + * Defaults to `true` + */ + strictReservedNames?: boolean; + + /** + * Function to sanitize dangerous property names + * + * @param name - The name of the property + * @returns {string} The sanitized name + * + * Defaults to `(name) => __name` + */ + onDangerousProperty?: (name: string) => string; +}; + +/** + * Options when jPath is true (default) - callbacks receive jPath as string + */ +export type X2jOptionsWithJPathString = X2jOptionsBase & { + /** + * Controls whether callbacks receive jPath as string or Matcher instance + * + * When `true` - callbacks receive jPath as string (backward compatible) + * + * Defaults to `true` + */ + jPath?: true; + + /** + * Whether to ignore attributes when parsing + * + * When `true` - ignores all the attributes + * + * When `false` - parses all the attributes + * + * When `Array` - filters out attributes that match provided patterns + * + * When `Function` - calls the function for each attribute and filters out those for which the function returned `true` + * + * Defaults to `true` + */ + ignoreAttributes?: boolean | (string | RegExp)[] | ((attrName: string, jPath: string) => boolean); + + /** + * Control how tag value should be parsed. Called only if tag value is not empty + * + * @param tagName - The name of the tag + * @param tagValue - The value of the tag + * @param jPath - The jPath string representing the tag's location + * @param hasAttributes - Whether the tag has attributes + * @param isLeafNode - Whether the tag is a leaf node + * @returns {undefined|null} `undefined` or `null` to set original value. + * @returns {unknown} + * + * 1. Different value or value with different data type to set new value. + * 2. Same value to set parsed value if `parseTagValue: true`. + * + * Defaults to `(tagName, val, jPath, hasAttributes, isLeafNode) => val` + */ + tagValueProcessor?: (tagName: string, tagValue: string, jPath: string, hasAttributes: boolean, isLeafNode: boolean) => unknown; + + /** + * Control how attribute value should be parsed + * + * @param attrName - The name of the attribute + * @param attrValue - The value of the attribute + * @param jPath - The jPath string representing the attribute's location + * @returns {undefined|null} `undefined` or `null` to set original value + * @returns {unknown} + * + * Defaults to `(attrName, val, jPath) => val` + */ + attributeValueProcessor?: (attrName: string, attrValue: string, jPath: string) => unknown; + + /** + * Determine whether a tag should be parsed as an array + * + * @param tagName - The name of the tag + * @param jPath - The jPath string representing the tag's location + * @param isLeafNode - Whether the tag is a leaf node + * @param isAttribute - Whether this is an attribute + * @returns {boolean} + * + * Defaults to `() => false` + */ + isArray?: (tagName: string, jPath: string, isLeafNode: boolean, isAttribute: boolean) => boolean; + /** * Change the tag name when a different name is returned. Skip the tag from parsed result when false is returned. * Modify `attrs` object to control attributes for the given tag. - * + * + * @param tagName - The name of the tag + * @param jPath - The jPath string representing the tag's location + * @param attrs - The attributes object * @returns {string} new tag name. * @returns false to skip the tag - * + * * Defaults to `(tagName, jPath, attrs) => tagName` */ updateTag?: (tagName: string, jPath: string, attrs: { [k: string]: string }) => string | boolean; +}; +/** + * Options when jPath is false - callbacks receive Matcher instance for advanced pattern matching + */ +export type X2jOptionsWithMatcher = X2jOptionsBase & { /** - * If true, adds a Symbol to all object nodes, accessible by {@link XMLParser.getMetaDataSymbol} with - * metadata about each the node in the XML file. + * Controls whether callbacks receive jPath as string or Matcher instance + * + * When `false` - callbacks receive Matcher instance for advanced pattern matching */ - captureMetaData?: boolean; + jPath: false; /** - * Maximum number of nested tags - * - * Defaults to `100` + * Whether to ignore attributes when parsing + * + * When `true` - ignores all the attributes + * + * When `false` - parses all the attributes + * + * When `Array` - filters out attributes that match provided patterns + * + * When `Function` - calls the function for each attribute and filters out those for which the function returned `true` + * + * Defaults to `true` */ - maxNestedTags?: number; + ignoreAttributes?: boolean | (string | RegExp)[] | ((attrName: string, matcher: Matcher) => boolean); /** - * Whether to strictly validate tag names + * Control how tag value should be parsed. Called only if tag value is not empty + * + * @param tagName - The name of the tag + * @param tagValue - The value of the tag + * @param matcher - The Matcher instance for advanced pattern matching + * @param hasAttributes - Whether the tag has attributes + * @param isLeafNode - Whether the tag is a leaf node + * @returns {undefined|null} `undefined` or `null` to set original value. + * @returns {unknown} + * + * 1. Different value or value with different data type to set new value. + * 2. Same value to set parsed value if `parseTagValue: true`. + * + * Defaults to `(tagName, val, matcher, hasAttributes, isLeafNode) => val` + */ + tagValueProcessor?: (tagName: string, tagValue: string, matcher: Matcher, hasAttributes: boolean, isLeafNode: boolean) => unknown; + + /** + * Control how attribute value should be parsed + * + * @param attrName - The name of the attribute + * @param attrValue - The value of the attribute + * @param matcher - The Matcher instance for advanced pattern matching + * @returns {undefined|null} `undefined` or `null` to set original value + * @returns {unknown} + * + * Defaults to `(attrName, val, matcher) => val` + */ + attributeValueProcessor?: (attrName: string, attrValue: string, matcher: Matcher) => unknown; + + /** + * Determine whether a tag should be parsed as an array + * + * @param tagName - The name of the tag + * @param matcher - The Matcher instance for advanced pattern matching + * @param isLeafNode - Whether the tag is a leaf node + * @param isAttribute - Whether this is an attribute + * @returns {boolean} + * + * Defaults to `() => false` + */ + isArray?: (tagName: string, matcher: Matcher, isLeafNode: boolean, isAttribute: boolean) => boolean; + + /** + * Change the tag name when a different name is returned. Skip the tag from parsed result when false is returned. + * Modify `attrs` object to control attributes for the given tag. + * + * @param tagName - The name of the tag + * @param matcher - The Matcher instance for advanced pattern matching + * @param attrs - The attributes object + * @returns {string} new tag name. + * @returns false to skip the tag + * + * Defaults to `(tagName, matcher, attrs) => tagName` + */ + updateTag?: (tagName: string, matcher: Matcher, attrs: { [k: string]: string }) => string | boolean; +}; + +/** + * XMLParser options - a discriminated union based on the jPath option + * + * When jPath is true (default) or undefined, callbacks receive jPath as a string. + * When jPath is false, callbacks receive a Matcher instance for advanced pattern matching. + */ +export type X2jOptions = X2jOptionsWithJPathString | X2jOptionsWithMatcher; + +export type ProcessEntitiesOptions = { + /** + * Whether to enable entity processing * * Defaults to `true` */ - strictReservedNames?: boolean; -}; + enabled?: boolean; + /** + * Maximum size in characters for a single entity definition + * + * Defaults to `10000` + */ + maxEntitySize?: number; + /** + * Maximum depth for nested entity references (reserved for future use) + * + * Defaults to `10` + */ + maxExpansionDepth?: number; + + /** + * Maximum total number of entity expansions allowed + * + * Defaults to `1000` + */ + maxTotalExpansions?: number; + + /** + * Maximum total expanded content length in characters + * + * Defaults to `100000` + */ + maxExpandedLength?: number; + + /** + * Maximum number of entities allowed in the XML + * + * Defaults to `100` + */ + maxEntityCount?: number; + + /** + * Array of tag names where entity replacement is allowed. + * If null, entities are replaced in all tags. + * + * Defaults to `null` + */ + allowedTags?: string[] | null; + + /** + * Custom filter function to determine if entities should be replaced in a tag + * + * @param tagName - The name of the current tag + * @param jPathOrMatcher - The jPath string (if jPath: true) or Matcher instance (if jPath: false) + * @returns `true` to allow entity replacement, `false` to skip + * + * Defaults to `null` + */ + tagFilter?: ((tagName: string, jPathOrMatcher: string | Matcher) => boolean) | null; +}; export type strnumOptions = { hex: boolean; @@ -437,9 +572,11 @@ export type XmlBuilderOptions = { /** * Nodes to stop parsing at * + * Accepts string patterns or Expression objects from path-expression-matcher + * * Defaults to `[]` */ - stopNodes?: string[]; + stopNodes?: (string | Expression)[]; /** * Control how tag value should be parsed. Called only if tag value is not empty @@ -476,6 +613,13 @@ export type XmlBuilderOptions = { oneListGroup?: boolean; + + /** + * Maximum number of nested tags + * + * Defaults to `100` + */ + maxNestedTags?: number; }; type ESchema = string | object | Array; diff --git a/test/types/isArray-type-errors.test.ts b/test/types/isArray-type-errors.test.ts new file mode 100644 index 00000000..c3253fef --- /dev/null +++ b/test/types/isArray-type-errors.test.ts @@ -0,0 +1,47 @@ +/** + * Type error tests for isArray option type inference (Issue #803) + * + * These tests verify that TypeScript correctly catches type errors + * when the jPath option doesn't match the callback parameter usage. + */ + +import { XMLParser, X2jOptions, X2jOptionsWithJPathString, X2jOptionsWithMatcher } from '../../src/fxp'; + +// ERROR TEST 1: Using string methods when jPath: false +// This should cause a TypeScript error because matcher is unknown, not string +const errorOptions1: X2jOptions = { + jPath: false, + isArray: (tagName, matcher, isLeafNode, isAttribute) => { + // @ts-expect-error - matcher is unknown, not string - includes() not available + return ['root.items'].includes(matcher); + }, +}; + +// ERROR TEST 2: Explicitly typed as X2jOptionsWithMatcher but using string methods +const errorOptions2: X2jOptionsWithMatcher = { + jPath: false, + isArray: (tagName, matcher, isLeafNode, isAttribute) => { + // @ts-expect-error - matcher is unknown, not string + return matcher.startsWith('root.'); + }, +}; + +// ERROR TEST 3: Trying to set jPath: true on X2jOptionsWithMatcher type +const errorOptions3: X2jOptionsWithMatcher = { + // @ts-expect-error - jPath must be false for X2jOptionsWithMatcher + jPath: true, + isArray: (tagName, jPath, isLeafNode, isAttribute) => { + return false; + }, +}; + +// ERROR TEST 4: Trying to set jPath: false on X2jOptionsWithJPathString type +const errorOptions4: X2jOptionsWithJPathString = { + // @ts-expect-error - jPath must be true or undefined for X2jOptionsWithJPathString + jPath: false, + isArray: (tagName, jPath, isLeafNode, isAttribute) => { + return false; + }, +}; + +console.log('All error type tests defined correctly!'); diff --git a/test/types/isArray-type-inference.test.ts b/test/types/isArray-type-inference.test.ts new file mode 100644 index 00000000..c97defdf --- /dev/null +++ b/test/types/isArray-type-inference.test.ts @@ -0,0 +1,135 @@ +/** + * Type tests for isArray option type inference (Issue #803) + * + * These tests verify that TypeScript correctly narrows the type of the + * jPath/matcher parameter in callbacks based on the jPath option value. + */ + +import { XMLParser, X2jOptions, X2jOptionsBase, X2jOptionsWithJPathString, X2jOptionsWithMatcher } from '../../src/fxp'; + +// Helper type to verify type narrowing at compile time +type AssertType = T extends Expected ? (Expected extends T ? true : false) : false; + +// Test 1: Default options (jPath: true or undefined) - callbacks receive string +const defaultOptions: X2jOptions = { + // jPath defaults to true, so callbacks should receive string + isArray: (tagName, jPath, isLeafNode, isAttribute) => { + // TypeScript should infer jPath as string + const path: string = jPath; + return ['root.items', 'root.users'].includes(jPath); + }, + tagValueProcessor: (tagName, tagValue, jPath, hasAttributes, isLeafNode) => { + // jPath should be string + const path: string = jPath; + return tagValue.toUpperCase(); + }, + attributeValueProcessor: (attrName, attrValue, jPath) => { + // jPath should be string + const path: string = jPath; + return attrValue; + }, + updateTag: (tagName, jPath, attrs) => { + // jPath should be string + const path: string = jPath; + return tagName; + }, +}; + +// Test 2: Explicit jPath: true - callbacks receive string +const optionsWithJPathTrue: X2jOptions = { + jPath: true, + isArray: (tagName, jPath, isLeafNode, isAttribute) => { + // TypeScript should infer jPath as string + const path: string = jPath; + return jPath.includes('items'); + }, + ignoreAttributes: (attrName, jPath) => { + // jPath should be string + const path: string = jPath; + return attrName.startsWith('_'); + }, +}; + +// Test 3: jPath: false - callbacks receive Matcher (unknown) +const optionsWithJPathFalse: X2jOptions = { + jPath: false, + isArray: (tagName, matcher, isLeafNode, isAttribute) => { + // matcher should be Matcher (unknown) + // We can't use string methods directly - this is the expected behavior + // Users must type-cast or use Matcher methods + return false; + }, + tagValueProcessor: (tagName, tagValue, matcher, hasAttributes, isLeafNode) => { + // matcher should be Matcher (unknown) + return tagValue; + }, +}; + +// Test 4: Explicit type annotation with X2jOptionsWithJPathString +const stringPathOptions: X2jOptionsWithJPathString = { + jPath: true, // or omit for default + isArray: (tagName, jPath, isLeafNode, isAttribute) => { + // jPath is definitely string here + return ['root.a', 'root.b'].includes(jPath); + }, +}; + +// Test 5: Explicit type annotation with X2jOptionsWithMatcher +const matcherOptions: X2jOptionsWithMatcher = { + jPath: false, + isArray: (tagName, matcher, isLeafNode, isAttribute) => { + // matcher is Matcher (unknown) here + return false; + }, +}; + +// Test 6: Real-world use case from issue #803 +const alwaysArray = ['root.items.item', 'root.users.user', 'root.config.settings']; + +const realWorldOptions: X2jOptions = { + ignoreAttributes: false, + isArray: (tagName, jPath, isLeafNode, isAttribute) => { + // This should work without type assertion now! + return alwaysArray.includes(jPath); + }, +}; + +// Test 7: Using the parser with these options +const parser1 = new XMLParser(defaultOptions); +const parser2 = new XMLParser(optionsWithJPathTrue); +const parser3 = new XMLParser(optionsWithJPathFalse); +const parser4 = new XMLParser(stringPathOptions); +const parser5 = new XMLParser(matcherOptions); +const parser6 = new XMLParser(realWorldOptions); + +// Test 8: Inline options should also work +const parser7 = new XMLParser({ + isArray: (tagName, jPath, isLeafNode, isAttribute) => { + return ['root.items'].includes(jPath); + }, +}); + +const parser8 = new XMLParser({ + jPath: true, + isArray: (tagName, jPath, isLeafNode, isAttribute) => { + return jPath.startsWith('root.'); + }, +}); + +// Test 9: Combined with other options +const combinedOptions: X2jOptions = { + preserveOrder: false, + ignoreAttributes: false, + attributeNamePrefix: '@_', + isArray: (tagName, jPath, isLeafNode, isAttribute) => { + return jPath.endsWith('.item'); + }, + tagValueProcessor: (tagName, tagValue, jPath) => { + if (jPath.includes('numbers')) { + return parseInt(tagValue, 10); + } + return tagValue; + }, +}; + +console.log('All type tests passed!');