@nodable/sequential-builder 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,214 @@
1
+ # Sequential Output Builder
2
+
3
+ Produces a sequential array where every element is represented as an object with the **tag name directly as a key** pointing to its children array. There is no separate `elementname` property — the structure is array-first throughout.
4
+
5
+ ## Output structure
6
+
7
+ ```
8
+ [ ← getOutput() always returns an array
9
+ {
10
+ [tagName]: Array, ← tag name → children array (always present, empty for leaf/empty nodes)
11
+ [groupBy]?: object, ← attributes as a sibling property (only when non-empty)
12
+ text?: any ← only present on leaf nodes (no child element entries)
13
+ }
14
+ ]
15
+ ```
16
+
17
+ ### Leaf node (text only, no child elements)
18
+
19
+ ```js
20
+ { span: [], text: "Hello" }
21
+ ```
22
+
23
+ ### Empty tag (no text, no children)
24
+
25
+ ```js
26
+ { br: [] }
27
+ ```
28
+
29
+ ### Tag with child elements
30
+
31
+ ```js
32
+ { div: [ /* child entries */ ] }
33
+ ```
34
+
35
+ ### Tag with attributes and text
36
+
37
+ ```js
38
+ { item: [], attributes: { "@_id": 1 }, text: "hello" }
39
+ ```
40
+
41
+ Attributes are a **sibling property** alongside the tag key — they are not nested inside the children array.
42
+
43
+ ### Mixed content (text interleaved with child elements)
44
+
45
+ Inline text runs appear as `{ "#text": value }` entries inside the children array. The entry itself has no `text` property in this case.
46
+
47
+ Input:
48
+ ```xml
49
+ <p>Hello <b>world</b>!</p>
50
+ ```
51
+
52
+ Output:
53
+ ```js
54
+ [
55
+ {
56
+ p: [
57
+ { "#text": "Hello " },
58
+ { b: [], text: "world" },
59
+ { "#text": "!" }
60
+ ]
61
+ }
62
+ ]
63
+ ```
64
+
65
+ ## Basic example
66
+
67
+ Input:
68
+ ```xml
69
+ <root>
70
+ <child>hello</child>
71
+ <child>world</child>
72
+ </root>
73
+ ```
74
+
75
+ Output:
76
+ ```js
77
+ [
78
+ {
79
+ root: [
80
+ { child: [], text: "hello" },
81
+ { child: [], text: "world" }
82
+ ]
83
+ }
84
+ ]
85
+ ```
86
+
87
+ ## Install
88
+
89
+ ```bash
90
+ npm install @nodable/sequential-builder
91
+ ```
92
+
93
+ ## Usage
94
+
95
+ ```js
96
+ import XMLParser from "@nodable/flexible-xml-parser";
97
+ import {SequentialBuilderFactory} from "@nodable/sequential-builder";
98
+
99
+ const parser = new XMLParser({
100
+ OutputBuilder: new SequentialBuilderFactory(builderOptions),
101
+ ...parserOptions,
102
+ });
103
+
104
+ const result = parser.parse(xmlString);
105
+ // result is always an array
106
+ ```
107
+
108
+ ## Options
109
+
110
+ ### `attributes.groupBy` (default: `"attributes"`)
111
+
112
+ The property name under which all attributes are collected as a sibling alongside the tag key. The property is **only present** when attributes exist and `skip.attributes` is false.
113
+
114
+ ```js
115
+ new SequentialBuilderFactory({
116
+ attributes: { groupBy: "attributes" } // default
117
+ })
118
+ ```
119
+
120
+ To use a custom key:
121
+
122
+ ```js
123
+ new SequentialBuilderFactory({
124
+ attributes: { groupBy: ":@" }
125
+ })
126
+ ```
127
+
128
+ ### `nameFor.text` (default: `"#text"`)
129
+
130
+ The key used for inline text entries inside the children array when a node has mixed content (text interleaved with child elements).
131
+
132
+ ```js
133
+ new SequentialBuilderFactory({
134
+ nameFor: { text: ":text" }
135
+ })
136
+ ```
137
+
138
+ ### `nameFor.comment`
139
+
140
+ When `skip.comment` is false, this property name is used for comment entries in the children array.
141
+
142
+ ```js
143
+ new SequentialBuilderFactory({
144
+ nameFor: { comment: "#comment" }
145
+ })
146
+ ```
147
+
148
+ ### `nameFor.cdata`
149
+
150
+ When set, CDATA sections appear as `{ [cdata]: value }` entries in the children array. When unset (default), CDATA content is merged into the node's `text` value (same as regular text).
151
+
152
+ ```js
153
+ // builder config
154
+ const builderConfig = { nameFor: { cdata: "##cdata" } };
155
+ // parser config
156
+ const parserConfig = { skip: { cdata: false } };
157
+
158
+ const parser = new XMLParser({
159
+ OutputBuilder: new SequentialBuilderFactory(builderConfig),
160
+ ...parserConfig,
161
+ });
162
+ ```
163
+
164
+ Output for `<root><code><![CDATA[data]]></code></root>`:
165
+ ```js
166
+ [
167
+ {
168
+ root: [
169
+ {
170
+ code: [
171
+ { "##cdata": "data" }
172
+ ]
173
+ }
174
+ ]
175
+ }
176
+ ]
177
+ ```
178
+
179
+ ### `textInChild` (default: `false`)
180
+
181
+ When `true`, text is always stored as a `{ [nameFor.text]: value }` entry in the children array — even on pure leaf nodes that have no element children. The `text` sibling property is never set in this mode.
182
+
183
+ ```js
184
+ new SequentialBuilderFactory({ textInChild: true })
185
+ ```
186
+
187
+ Input:
188
+ ```xml
189
+ <root><a>hello</a></root>
190
+ ```
191
+
192
+ Default output (`textInChild: false`):
193
+ ```js
194
+ [ { root: [ { a: [], text: "hello" } ] } ]
195
+ ```
196
+
197
+ Output with `textInChild: true`:
198
+ ```js
199
+ [ { root: [ { a: [ { "#text": "hello" } ] } ] } ]
200
+ ```
201
+
202
+ ### `skip.attributes` (default: `true`)
203
+
204
+ When `true` (default), all attributes are ignored and no `attributes` property appears on entries. Set to `false` to populate attributes.
205
+
206
+ ### Value parsers
207
+
208
+ By default the parser chain `["entity", "boolean", "number"]` is applied to text content, converting `"42"` → `42` and `"true"` → `true`. Override with `tags.valueParsers`.
209
+
210
+ ```js
211
+ new SequentialBuilderFactory({
212
+ tags: { valueParsers: [] } // keep all values as raw strings
213
+ })
214
+ ```
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@nodable/sequential-builder",
3
+ "version": "1.0.0",
4
+ "description": "Sequential JS Array builder for flexible-xml-parser",
5
+ "main": "src/index.js",
6
+ "types": "./src/index.d.ts",
7
+ "type": "module",
8
+ "scripts": {
9
+ "test": "echo \"Error: no test specified\" && exit 1"
10
+ },
11
+ "keywords": [
12
+ "flexible-xml-parser",
13
+ "nodable"
14
+ ],
15
+ "author": "Amit Gupta (https://solothought.com)",
16
+ "license": "MIT",
17
+ "files": [
18
+ "src"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "dependencies": {
24
+ "@nodable/base-output-builder": "^1.0.2",
25
+ "path-expression-matcher": "^1.4.0"
26
+ },
27
+ "funding": [
28
+ {
29
+ "type": "github",
30
+ "url": "https://github.com/sponsors/nodable"
31
+ }
32
+ ],
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/nodable/flexible-output-builder.git"
36
+ }
37
+ }
@@ -0,0 +1,78 @@
1
+ const defaultOptions = {
2
+ nameFor: {
3
+ text: "#text",
4
+ // comment: "",
5
+ // cdata: "",
6
+ },
7
+ skip: {
8
+ // declaration: false,
9
+ // pi: false,
10
+ // attributes: true,
11
+ // cdata: false,
12
+ // comment: false,
13
+ // nsPrefix: false,
14
+ // tags: false,
15
+ },
16
+ tags: {
17
+ valueParsers: [],
18
+ // stopNodes: [],
19
+ },
20
+ attributes: {
21
+ prefix: "@_",
22
+ suffix: "",
23
+ groupBy: "attributes",
24
+ valueParsers: [],
25
+ },
26
+ textInChild: false,
27
+ };
28
+
29
+ // Default chains: replaceEntities first (expand references), then type coercion.
30
+ const defaultTagParsers = ["entity", "boolean", "number"];
31
+ const defaultAttrParsers = ["entity", "number", "boolean"];
32
+
33
+ export function buildOptions(options) {
34
+ const finalOptions = deepClone(defaultOptions);
35
+
36
+ if (!options || options.tags?.valueParsers === undefined) {
37
+ finalOptions.tags.valueParsers = [...defaultTagParsers];
38
+ }
39
+ if (!options || options.attributes?.valueParsers === undefined) {
40
+ finalOptions.attributes.valueParsers = [...defaultAttrParsers];
41
+ }
42
+
43
+ if (options) {
44
+ copyProperties(finalOptions, options);
45
+ }
46
+
47
+ return finalOptions;
48
+ }
49
+
50
+ function deepClone(obj) {
51
+ if (obj === null || typeof obj !== 'object') return obj;
52
+ if (Array.isArray(obj)) return obj.map(deepClone);
53
+ const clone = {};
54
+ for (const key of Object.keys(obj)) {
55
+ clone[key] = deepClone(obj[key]);
56
+ }
57
+ return clone;
58
+ }
59
+
60
+ function copyProperties(target, source) {
61
+ for (const key of Object.keys(source)) {
62
+ // Guard against prototype pollution via option keys
63
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;
64
+
65
+ if (typeof source[key] === 'function') {
66
+ target[key] = source[key];
67
+ } else if (Array.isArray(source[key])) {
68
+ target[key] = source[key];
69
+ } else if (typeof source[key] === 'object' && source[key] !== null) {
70
+ if (typeof target[key] !== 'object' || target[key] === null) {
71
+ target[key] = {};
72
+ }
73
+ copyProperties(target[key], source[key]);
74
+ } else {
75
+ target[key] = source[key];
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,219 @@
1
+ import { buildOptions } from './ParserOptionsBuilder.js';
2
+ import { BaseOutputBuilder, BaseOutputBuilderFactory, ElementType } from '@nodable/base-output-builder';
3
+
4
+ const rootName = '!sequential_root';
5
+
6
+ export default class SequentialBuilderFactory extends BaseOutputBuilderFactory {
7
+ constructor(options) {
8
+ super();
9
+ this.options = buildOptions(options);
10
+ }
11
+
12
+ getInstance(parserOptions, readonlyMatcher) {
13
+ const valParsers = { ...this.commonValParsers };
14
+ return new SequentialBuilder(parserOptions, this.options, valParsers, readonlyMatcher);
15
+ }
16
+ }
17
+
18
+ export class SequentialBuilder extends BaseOutputBuilder {
19
+
20
+ constructor(parserOptions, builderOptions, registeredValParsers, readonlyMatcher) {
21
+ super(readonlyMatcher);
22
+ this.tagsStack = [];
23
+ this.parserOptions = parserOptions;
24
+
25
+ this.options = {
26
+ ...parserOptions,
27
+ ...builderOptions,
28
+ skip: { ...parserOptions.skip, ...builderOptions.skip },
29
+ nameFor: { ...parserOptions.nameFor, ...builderOptions.nameFor },
30
+ tags: { ...parserOptions.tags, ...builderOptions.tags },
31
+ attributes: { ...parserOptions.attributes, ...builderOptions.attributes },
32
+ };
33
+
34
+ this.registeredValParsers = registeredValParsers;
35
+
36
+ this.root = new Node(rootName, this.options);
37
+ this.currentNode = this.root;
38
+ this.attributes = {};
39
+ this._pendingStopNode = false;
40
+ }
41
+
42
+ addElement(tag) {
43
+ // If the current node has text set (text arrived before any child element),
44
+ // retroactively migrate it into the children array as an inline text entry
45
+ // now that we know this is mixed content.
46
+ if (this.currentNode.text !== undefined) {
47
+ this.currentNode.children.unshift({
48
+ [this.options.nameFor.text]: this.currentNode.text
49
+ });
50
+ delete this.currentNode.text;
51
+ }
52
+
53
+ this.tagsStack.push(this.currentNode);
54
+ const node = new Node(tag.name, this.options);
55
+ // Attach any pending attributes onto the new node
56
+ if (this.attributes && Object.keys(this.attributes).length > 0) {
57
+ node[this.options.attributes.groupBy] = { ...this.attributes };
58
+ }
59
+ this.attributes = {};
60
+ this.currentNode = node;
61
+ }
62
+
63
+ /**
64
+ * Called when a stop node is fully collected, before `addValue`.
65
+ *
66
+ * @param {TagDetail} tagDetail - name, line, col, index of the stop node
67
+ * @param {string} rawContent - raw unparsed content between the tags
68
+ */
69
+ onStopNode(tagDetail, rawContent) {
70
+ this._pendingStopNode = true;
71
+ if (typeof this.options.onStopNode === 'function') {
72
+ this.options.onStopNode(tagDetail, rawContent, this.matcher);
73
+ }
74
+ }
75
+
76
+ closeElement() {
77
+ const node = this.currentNode;
78
+ this.currentNode = this.tagsStack.pop();
79
+
80
+ this._pendingStopNode = false;
81
+
82
+ if (this.options.onClose !== undefined) {
83
+ const resultTag = this.options.onClose(node, this.matcher);
84
+ if (resultTag) return;
85
+ }
86
+
87
+ // Build the sequential representation:
88
+ // { [tagName]: children, [groupBy]: attributes, text? }
89
+ // Tag name directly points to the children array.
90
+ // Attributes (when present) are a sibling property alongside the tag key.
91
+ const entry = { [node.tagname]: node.children };
92
+
93
+ const groupBy = this.options.attributes.groupBy;
94
+ if (node[groupBy] && Object.keys(node[groupBy]).length > 0) {
95
+ entry[groupBy] = node[groupBy];
96
+ }
97
+
98
+ // text is a sibling property (leaf-node case — no element children)
99
+ if (node.text !== undefined) {
100
+ entry.text = node.text;
101
+ }
102
+
103
+ this.currentNode.children.push(entry);
104
+ }
105
+
106
+ addValue(text) {
107
+ const tagName = this.currentNode?.tagname;
108
+ // Check whether there are already element children (mixed content scenario).
109
+ // Mixed content = children that are NOT bare text entries.
110
+ const hasElementChildren = this.currentNode?.children?.some(
111
+ c => !Object.prototype.hasOwnProperty.call(c, this.options.nameFor.text)
112
+ );
113
+
114
+ const context = {
115
+ elementName: tagName,
116
+ elementValue: text,
117
+ elementType: ElementType.ELEMENT,
118
+ matcher: this.matcher,
119
+ isLeafNode: !hasElementChildren,
120
+ };
121
+
122
+ const parsedValue = this.parseValue(text, this.options.tags.valueParsers, context);
123
+
124
+ if (hasElementChildren || this.options.textInChild) {
125
+ // Mixed content: text alongside child elements — store as inline text child
126
+ this.currentNode.children.push({
127
+ [this.options.nameFor.text]: parsedValue
128
+ });
129
+ } else {
130
+ // Pure text (leaf node or text before any child elements):
131
+ // set directly on the node; promoted to sibling property in closeElement.
132
+ this.currentNode.text = parsedValue;
133
+ }
134
+ }
135
+
136
+ addInstruction(name) {
137
+ const node = new Node(name, this.options);
138
+ const groupBy = this.options.attributes.groupBy;
139
+ if (this.attributes && Object.keys(this.attributes).length > 0) {
140
+ node[groupBy] = { ...this.attributes };
141
+ }
142
+ const entry = { [node.tagname]: node.children };
143
+ if (node[groupBy] && Object.keys(node[groupBy]).length > 0) {
144
+ entry[groupBy] = node[groupBy];
145
+ }
146
+ this.currentNode.children.push(entry);
147
+ this.attributes = {};
148
+ }
149
+
150
+ addComment(text) {
151
+ if (this.options.skip.comment) return;
152
+ if (this.options.nameFor.comment) {
153
+ this.currentNode.children.push({
154
+ [this.options.nameFor.comment]: text
155
+ });
156
+ }
157
+ }
158
+
159
+ addLiteral(text) {
160
+ if (this.options.skip.cdata) return;
161
+ if (this.options.nameFor.cdata) {
162
+ this.currentNode.children.push({
163
+ [this.options.nameFor.cdata]: text
164
+ });
165
+ } else {
166
+ this.addValue(text || '');
167
+ }
168
+ }
169
+
170
+ getOutput() {
171
+ return this.root.children;
172
+ }
173
+
174
+ /**
175
+ * Called by the parser when `exitIf` returns true for the current tag.
176
+ * Receives a snapshot of the parser state at the moment of exit, after
177
+ * all open tags have been cleanly closed by the parser.
178
+ *
179
+ * Override in subclasses to record the exit position or annotate output.
180
+ *
181
+ * @param {object} exitInfo
182
+ * @param {object} exitInfo.tagDetail - `{ name, line, col, index }` of the
183
+ * tag that triggered the exit.
184
+ * @param {object} exitInfo.matcher - Read-only matcher positioned at
185
+ * that tag at the moment exitIf fired.
186
+ * @param {number} exitInfo.depth - Nesting depth at exit (0 = root children).
187
+ */
188
+ onExit(exitInfo) {
189
+ // Base implementation: attach exit metadata to the output root so callers
190
+ // can tell the parse was intentionally truncated and where it stopped.
191
+ // Stored under __exitInfo to avoid colliding with any tag-derived key.
192
+ // Subclasses may override to suppress, transform, or log this information.
193
+ // if (this.value && typeof this.value === 'object') {
194
+ // Object.defineProperty(this.value, '__exitInfo', {
195
+ // value: {
196
+ // tag: exitInfo.tagDetail.name,
197
+ // line: exitInfo.tagDetail.line,
198
+ // col: exitInfo.tagDetail.col,
199
+ // index: exitInfo.tagDetail.index,
200
+ // depth: exitInfo.depth,
201
+ // },
202
+ // enumerable: false, // invisible to JSON.stringify and for-in
203
+ // configurable: true,
204
+ // writable: true,
205
+ // });
206
+ // }
207
+
208
+ //Do nothing
209
+ }
210
+ }
211
+
212
+ class Node {
213
+ constructor(tagname, options) {
214
+ this.tagname = tagname;
215
+ this.children = [];
216
+ const groupBy = options?.attributes?.groupBy ?? 'attributes';
217
+ this[groupBy] = {};
218
+ }
219
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,170 @@
1
+ export interface SkipOptions {
2
+ /** Skip XML declaration `<?xml ... ?>` from output. Default: false */
3
+ declaration?: boolean;
4
+ /** Skip processing instructions (other than declaration) from output. Default: false */
5
+ pi?: boolean;
6
+ /**
7
+ * Skip all attributes from output. When true (default), the `attributes`
8
+ * property on every entry is an empty object `{}`.
9
+ * Set to false to populate attributes.
10
+ * Default: true
11
+ */
12
+ attributes?: boolean;
13
+ /** Exclude CDATA sections entirely from output. Default: false */
14
+ cdata?: boolean;
15
+ /** Exclude comments entirely from output. Default: false */
16
+ comment?: boolean;
17
+ /**
18
+ * Strip namespace prefixes from tag and attribute names.
19
+ * E.g. `ns:tag` → `tag`, `xmlns:*` attributes are dropped.
20
+ * Default: false
21
+ */
22
+ nsPrefix?: boolean;
23
+ /** (future) Tag-level filtering — not yet implemented. Default: false */
24
+ tags?: boolean;
25
+ }
26
+
27
+ export interface NameForOptions {
28
+ /**
29
+ * Property name for inline text nodes in mixed content
30
+ * (i.e. text that appears alongside child elements in the same parent).
31
+ * These appear as `{ [text]: value }` entries in the children array.
32
+ * Default: '#text'
33
+ */
34
+ text?: string;
35
+ /**
36
+ * Property name for CDATA sections.
37
+ * When set, CDATA nodes appear as `{ [cdata]: value }` entries in the children array.
38
+ * When unset (default), CDATA content is merged into the node's `text` value.
39
+ */
40
+ cdata?: string;
41
+ /**
42
+ * Property name for XML comments.
43
+ * When unset (default), comments are omitted from output.
44
+ * Set e.g. '#comment' to capture them as `{ '#comment': value }` entries.
45
+ */
46
+ comment?: string;
47
+ }
48
+
49
+ export interface AttributeOptions {
50
+ /** Allow boolean (valueless) attributes — treated as `true`. Default: false */
51
+ booleanType?: boolean;
52
+ /**
53
+ * Property name under which all attributes are grouped on each entry, as a
54
+ * sibling alongside the tag-name key.
55
+ * Default: 'attributes'
56
+ */
57
+ groupBy?: string;
58
+ /** Prefix prepended to attribute names in output. Default: '@_' */
59
+ prefix?: string;
60
+ /** Suffix appended to attribute names in output. Default: '' */
61
+ suffix?: string;
62
+ /**
63
+ * Value parser chain for attribute values.
64
+ * Built-in names: 'entity', 'number', 'boolean', 'trim', 'currency'.
65
+ * Default: ['entity', 'number', 'boolean']
66
+ */
67
+ valueParsers?: Array<string | ValueParser>;
68
+ }
69
+
70
+ export interface TagOptions {
71
+ /**
72
+ * Value parser chain for tag text content.
73
+ * Built-in names: 'entity', 'boolean', 'number', 'trim', 'currency'.
74
+ * Default: ['entity', 'boolean', 'number']
75
+ */
76
+ valueParsers?: Array<string | ValueParser>;
77
+ }
78
+
79
+ export interface FactoryOptions {
80
+ /** Fine-grained control over which node types appear in output */
81
+ skip?: SkipOptions;
82
+
83
+ /** Property names used for special nodes in output */
84
+ nameFor?: NameForOptions;
85
+
86
+ /** Attribute parsing and representation options */
87
+ attributes?: AttributeOptions;
88
+
89
+ /** Tag parsing options including stop nodes and value parser chain */
90
+ tags?: TagOptions;
91
+
92
+ /**
93
+ * When true, text is always stored as a `{ [nameFor.text]: value }` child entry,
94
+ * even on pure leaf nodes (no mixed content required).
95
+ * Default: false — leaf text is stored as a `text` sibling property on the entry.
96
+ */
97
+ textInChild?: boolean;
98
+ }
99
+
100
+ /**
101
+ * A parsed XML entry as produced by SequentialBuilder.
102
+ *
103
+ * Structure:
104
+ * {
105
+ * [tagName]: Array<SequentialEntry>, // tag name directly points to children array
106
+ * [groupBy]?: Record<string, any>, // attributes (sibling property; present only when non-empty)
107
+ * text?: any // only present on leaf nodes (no child element entries)
108
+ * }
109
+ *
110
+ * Leaf node (text only, no child elements):
111
+ * { child: [], text: "Hello" } — wait, the key IS the tag name:
112
+ * { span: [], text: "Hello" }
113
+ *
114
+ * Empty tag:
115
+ * { br: [] }
116
+ *
117
+ * Tag with child elements:
118
+ * { div: [ ...childEntries ] }
119
+ *
120
+ * Tag with attributes:
121
+ * { item: [], attributes: { "@_id": 1 }, text: "val" }
122
+ *
123
+ * Mixed content (text interleaved with child elements):
124
+ * Inline text runs appear as `{ [nameFor.text]: value }` entries inside the array.
125
+ * The entry has no `text` property in that case.
126
+ *
127
+ * The overall `getOutput()` returns an array — always — even for a single root element.
128
+ */
129
+ export type SequentialEntry = Record<string, any>;
130
+
131
+ export interface SequentialBuilderInstance {
132
+ addElement(tag: { name: string }, matcher: any): void;
133
+ closeElement(matcher: any): void;
134
+ addValue(text: string, matcher: any): void;
135
+ addAttribute(name: string, value: any): void;
136
+ addComment(text: string): void;
137
+ addLiteral(text: string): void;
138
+ addDeclaration(): void;
139
+ addInstruction(name: string): void;
140
+ /**
141
+ * Called by the XML parser after the DOCTYPE block is read.
142
+ * Implementations forward entities to any registered value parser
143
+ * that implements addInputEntities().
144
+ */
145
+ addInputEntities(entities: object): void;
146
+ getOutput(): SequentialEntry[];
147
+ registeredValParsers: Record<string, ValueParser>;
148
+ /**
149
+ * Optional hook called by the parser when a stop node is fully collected.
150
+ * Delegates to the `options.onStopNode` callback when supplied.
151
+ */
152
+ onStopNode?(
153
+ tagDetail: { name: string; line: number; col: number; index: number },
154
+ rawContent: string,
155
+ matcher: any,
156
+ ): void;
157
+ }
158
+
159
+ /**
160
+ * A value parser transforms a value in the parsing chain.
161
+ */
162
+ export interface ValueParser {
163
+ parse(val: any, context?: { tagName: string; isAttribute: boolean; attrName?: string }): any;
164
+ }
165
+
166
+ export class SequentialBuilderFactory {
167
+ constructor(options?: Partial<FactoryOptions>);
168
+ getInstance(factoryOptions: FactoryOptions): SequentialBuilderInstance;
169
+ registerValueParser(name: string, parser: ValueParser): void;
170
+ }
package/src/index.js ADDED
@@ -0,0 +1 @@
1
+ export { default as SequentialBuilderFactory, SequentialBuilder } from './SequentialBuilder.js';