parse5-sax-parser 7.0.0 → 8.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -1
- package/dist/dev-null-stream.d.ts +0 -2
- package/dist/dev-null-stream.js +0 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.js +6 -1
- package/dist/parser-feedback-simulator.d.ts +7 -1
- package/dist/parser-feedback-simulator.js +95 -30
- package/package.json +20 -8
package/README.md
CHANGED
|
@@ -16,9 +16,25 @@
|
|
|
16
16
|
<br>
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
19
|
-
📖 <a href="https://parse5.js.org/modules/
|
|
19
|
+
📖 <a href="https://parse5.js.org/modules/parse5-sax-parser.html"><b>Documentation</b></a> 📖
|
|
20
20
|
</p>
|
|
21
21
|
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
Handle HTML tokens as they arrive, without building a document tree.
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
import { Readable } from 'node:stream';
|
|
28
|
+
import { pipeline } from 'node:stream/promises';
|
|
29
|
+
import { SAXParser } from 'parse5-sax-parser';
|
|
30
|
+
|
|
31
|
+
const parser = new SAXParser();
|
|
32
|
+
const text = [];
|
|
33
|
+
parser.on('text', (token) => text.push(token.text));
|
|
34
|
+
await pipeline(Readable.from(['<p>Hello, <b>world!</b></p>']), parser);
|
|
35
|
+
console.log(text.join('')); // Hello, world!
|
|
36
|
+
```
|
|
37
|
+
|
|
22
38
|
---
|
|
23
39
|
|
|
24
40
|
<p align="center">
|
package/dist/dev-null-stream.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
1
|
import { Transform } from 'node:stream';
|
|
3
2
|
import type { Tokenizer, TokenHandler, Token } from 'parse5';
|
|
4
3
|
import { ParserFeedbackSimulator } from './parser-feedback-simulator.js';
|
|
@@ -39,6 +38,7 @@ export interface SAXParserOptions {
|
|
|
39
38
|
*/
|
|
40
39
|
export declare class SAXParser extends Transform implements TokenHandler {
|
|
41
40
|
protected options: SAXParserOptions;
|
|
41
|
+
/** @internal */
|
|
42
42
|
protected parserFeedbackSimulator: ParserFeedbackSimulator;
|
|
43
43
|
private pendingText;
|
|
44
44
|
private lastChunkWritten;
|
|
@@ -151,4 +151,3 @@ export interface SAXParser {
|
|
|
151
151
|
*/
|
|
152
152
|
on(event: string, handler: (...args: any[]) => void): this;
|
|
153
153
|
}
|
|
154
|
-
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -95,11 +95,17 @@ export class SAXParser extends Transform {
|
|
|
95
95
|
_transformChunk(chunk) {
|
|
96
96
|
if (!this.stopped) {
|
|
97
97
|
this.tokenizer.write(chunk, this.lastChunkWritten);
|
|
98
|
+
this.tokenizer.flushCurrentCharacterToken();
|
|
98
99
|
}
|
|
99
100
|
return chunk;
|
|
100
101
|
}
|
|
101
102
|
/** @internal */
|
|
102
103
|
onCharacter({ chars, location }) {
|
|
104
|
+
// After a chunk-boundary flush the tokenizer continues with an empty text token, which arrives here without
|
|
105
|
+
// characters. It is dropped unless its source range covers something (CDATA delimiters, a tag dropped at
|
|
106
|
+
// EOF), which the rewriting stream still has to forward.
|
|
107
|
+
if (chars.length === 0 && (!location || location.startOffset === location.endOffset))
|
|
108
|
+
return;
|
|
103
109
|
if (this.pendingText === null) {
|
|
104
110
|
this.pendingText = { text: chars, sourceCodeLocation: location };
|
|
105
111
|
}
|
|
@@ -189,4 +195,3 @@ export class SAXParser extends Transform {
|
|
|
189
195
|
}
|
|
190
196
|
}
|
|
191
197
|
}
|
|
192
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -5,6 +5,13 @@ import { Tokenizer, type TokenizerOptions, type TokenHandler, Token } from 'pars
|
|
|
5
5
|
export declare class ParserFeedbackSimulator implements TokenHandler {
|
|
6
6
|
private handler;
|
|
7
7
|
private namespaceStack;
|
|
8
|
+
/** The name of the tag that opened each namespace frame, lowercase as in end tags; its end tag closes the frame. */
|
|
9
|
+
private frameTags;
|
|
10
|
+
/**
|
|
11
|
+
* Above 0 while the current node is the element that opened the last frame; each further count is a frame below
|
|
12
|
+
* whose element is the parent of the next frame's, so the current node stays known as the frames close.
|
|
13
|
+
*/
|
|
14
|
+
private directFrames;
|
|
8
15
|
inForeignContent: boolean;
|
|
9
16
|
skipNextNewLine: boolean;
|
|
10
17
|
tokenizer: Tokenizer;
|
|
@@ -29,4 +36,3 @@ export declare class ParserFeedbackSimulator implements TokenHandler {
|
|
|
29
36
|
/** @internal */
|
|
30
37
|
onEndTag(token: Token.TagToken): void;
|
|
31
38
|
}
|
|
32
|
-
//# sourceMappingURL=parser-feedback-simulator.d.ts.map
|
|
@@ -9,10 +9,17 @@ export class ParserFeedbackSimulator {
|
|
|
9
9
|
constructor(options, handler) {
|
|
10
10
|
this.handler = handler;
|
|
11
11
|
this.namespaceStack = [];
|
|
12
|
+
/** The name of the tag that opened each namespace frame, lowercase as in end tags; its end tag closes the frame. */
|
|
13
|
+
this.frameTags = [];
|
|
14
|
+
/**
|
|
15
|
+
* Above 0 while the current node is the element that opened the last frame; each further count is a frame below
|
|
16
|
+
* whose element is the parent of the next frame's, so the current node stays known as the frames close.
|
|
17
|
+
*/
|
|
18
|
+
this.directFrames = 0;
|
|
12
19
|
this.inForeignContent = false;
|
|
13
20
|
this.skipNextNewLine = false;
|
|
14
21
|
this.tokenizer = new Tokenizer(options, this);
|
|
15
|
-
this._enterNamespace(html.NS.HTML);
|
|
22
|
+
this._enterNamespace(html.NS.HTML, '');
|
|
16
23
|
}
|
|
17
24
|
/** @internal */
|
|
18
25
|
onNullCharacter(token) {
|
|
@@ -30,8 +37,9 @@ export class ParserFeedbackSimulator {
|
|
|
30
37
|
}
|
|
31
38
|
/** @internal */
|
|
32
39
|
onWhitespaceCharacter(token) {
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
const { skipNextNewLine } = this;
|
|
41
|
+
this.skipNextNewLine = false;
|
|
42
|
+
if (skipNextNewLine && token.chars.charCodeAt(0) === LINE_FEED_CODE_POINT) {
|
|
35
43
|
if (token.chars.length === 1) {
|
|
36
44
|
return;
|
|
37
45
|
}
|
|
@@ -60,14 +68,19 @@ export class ParserFeedbackSimulator {
|
|
|
60
68
|
this.handler.onEof(token);
|
|
61
69
|
}
|
|
62
70
|
//Namespace stack mutations
|
|
63
|
-
_enterNamespace(namespace) {
|
|
64
|
-
this.namespaceStack.
|
|
71
|
+
_enterNamespace(namespace, tagName) {
|
|
72
|
+
this.namespaceStack.push(namespace);
|
|
73
|
+
this.frameTags.push(tagName);
|
|
74
|
+
this.directFrames++;
|
|
65
75
|
this.inForeignContent = namespace !== html.NS.HTML;
|
|
66
76
|
this.tokenizer.inForeignNode = this.inForeignContent;
|
|
67
77
|
}
|
|
68
78
|
_leaveCurrentNamespace() {
|
|
69
|
-
this.namespaceStack.
|
|
70
|
-
this.
|
|
79
|
+
this.namespaceStack.pop();
|
|
80
|
+
this.frameTags.pop();
|
|
81
|
+
if (this.directFrames > 0)
|
|
82
|
+
this.directFrames--;
|
|
83
|
+
this.inForeignContent = this.namespaceStack[this.namespaceStack.length - 1] !== html.NS.HTML;
|
|
71
84
|
this.tokenizer.inForeignNode = this.inForeignContent;
|
|
72
85
|
}
|
|
73
86
|
//Token handlers
|
|
@@ -102,13 +115,31 @@ export class ParserFeedbackSimulator {
|
|
|
102
115
|
/** @internal */
|
|
103
116
|
onStartTag(token) {
|
|
104
117
|
let tn = token.tagID;
|
|
118
|
+
const frames = this.namespaceStack.length;
|
|
119
|
+
const ns = this.namespaceStack[frames - 1];
|
|
105
120
|
switch (tn) {
|
|
106
|
-
case $.SVG:
|
|
107
|
-
|
|
121
|
+
case $.SVG:
|
|
122
|
+
case $.MATH: {
|
|
123
|
+
const target = tn === $.SVG ? html.NS.SVG : html.NS.MATHML;
|
|
124
|
+
// Foreign content keeps these in its namespace, except svg in annotation-xml. Unless the current node is
|
|
125
|
+
// the frame's element, it may be annotation-xml or outside the frame: switch, as a guess nothing builds on.
|
|
126
|
+
if (ns === html.NS.HTML || ns === target) {
|
|
127
|
+
this._enterNamespace(target, token.tagName);
|
|
128
|
+
}
|
|
129
|
+
else if (this.directFrames === 0) {
|
|
130
|
+
this._enterNamespace(target, token.tagName);
|
|
131
|
+
this.directFrames = 0;
|
|
132
|
+
}
|
|
108
133
|
break;
|
|
109
134
|
}
|
|
110
|
-
case $.
|
|
111
|
-
|
|
135
|
+
case $.MGLYPH:
|
|
136
|
+
case $.MALIGNMARK: {
|
|
137
|
+
// A MathML text integration point keeps these children in MathML.
|
|
138
|
+
if (this.directFrames > 0 &&
|
|
139
|
+
this.namespaceStack[frames - 2] === html.NS.MATHML &&
|
|
140
|
+
foreignContent.isIntegrationPoint(html.getTagID(this.frameTags[frames - 1]), html.NS.MATHML, [], html.NS.MATHML)) {
|
|
141
|
+
this._enterNamespace(html.NS.MATHML, token.tagName);
|
|
142
|
+
}
|
|
112
143
|
break;
|
|
113
144
|
}
|
|
114
145
|
default:
|
|
@@ -116,10 +147,14 @@ export class ParserFeedbackSimulator {
|
|
|
116
147
|
}
|
|
117
148
|
if (this.inForeignContent) {
|
|
118
149
|
if (foreignContent.causesExit(token)) {
|
|
119
|
-
|
|
150
|
+
// Tree construction pops foreign elements up to an HTML element or integration point.
|
|
151
|
+
while (this.inForeignContent)
|
|
152
|
+
this._leaveCurrentNamespace();
|
|
153
|
+
this.directFrames = 0;
|
|
120
154
|
}
|
|
121
155
|
else {
|
|
122
|
-
const
|
|
156
|
+
const { tagName } = token;
|
|
157
|
+
const currentNs = this.namespaceStack[this.namespaceStack.length - 1];
|
|
123
158
|
if (currentNs === html.NS.MATHML) {
|
|
124
159
|
foreignContent.adjustTokenMathMLAttrs(token);
|
|
125
160
|
}
|
|
@@ -129,12 +164,25 @@ export class ParserFeedbackSimulator {
|
|
|
129
164
|
}
|
|
130
165
|
foreignContent.adjustTokenXMLAttrs(token);
|
|
131
166
|
tn = token.tagID;
|
|
132
|
-
|
|
133
|
-
|
|
167
|
+
// Unless the switch above opened a frame for it, the element is an integration point or a new current node.
|
|
168
|
+
if (!token.selfClosing && this.namespaceStack.length === frames) {
|
|
169
|
+
if (foreignContent.isIntegrationPoint(tn, currentNs, token.attrs)) {
|
|
170
|
+
this._enterNamespace(html.NS.HTML, tagName);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
this.directFrames = 0;
|
|
174
|
+
}
|
|
134
175
|
}
|
|
135
176
|
}
|
|
136
177
|
}
|
|
137
178
|
else {
|
|
179
|
+
// An HTML element named like the integration point closes before it does, so it gets a frame of its own.
|
|
180
|
+
if (token.tagName === this.frameTags[frames - 1]) {
|
|
181
|
+
this._enterNamespace(html.NS.HTML, token.tagName);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
this.directFrames = 0;
|
|
185
|
+
}
|
|
138
186
|
switch (tn) {
|
|
139
187
|
case $.PRE:
|
|
140
188
|
case $.TEXTAREA:
|
|
@@ -152,33 +200,50 @@ export class ParserFeedbackSimulator {
|
|
|
152
200
|
}
|
|
153
201
|
this._ensureTokenizerMode(tn);
|
|
154
202
|
}
|
|
203
|
+
// Tree construction pops a self-closing foreign element at once, so a frame it opened ends with the tag.
|
|
204
|
+
if (token.selfClosing && this.inForeignContent && this.namespaceStack.length > frames) {
|
|
205
|
+
this._leaveCurrentNamespace();
|
|
206
|
+
}
|
|
155
207
|
this.handler.onStartTag(token);
|
|
156
208
|
}
|
|
157
209
|
/** @internal */
|
|
158
210
|
onEndTag(token) {
|
|
159
|
-
|
|
211
|
+
const tn = token.tagID;
|
|
212
|
+
const frames = this.frameTags.length;
|
|
160
213
|
if (!this.inForeignContent) {
|
|
161
|
-
|
|
162
|
-
if (
|
|
163
|
-
const adjustedTagName = foreignContent.SVG_TAG_NAMES_ADJUSTMENT_MAP.get(token.tagName);
|
|
164
|
-
if (adjustedTagName) {
|
|
165
|
-
tn = html.getTagID(adjustedTagName);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
//NOTE: check for exit from integration point
|
|
169
|
-
if (foreignContent.isIntegrationPoint(tn, previousNs, token.attrs)) {
|
|
214
|
+
// Leaving an integration point: only the end tag of the element that opened the HTML frame closes it.
|
|
215
|
+
if (token.tagName === this.frameTags[frames - 1]) {
|
|
170
216
|
this._leaveCurrentNamespace();
|
|
171
217
|
}
|
|
218
|
+
else {
|
|
219
|
+
this.directFrames = 0;
|
|
220
|
+
}
|
|
172
221
|
}
|
|
173
|
-
else if (
|
|
174
|
-
|
|
175
|
-
this.
|
|
222
|
+
else if (tn === $.P || tn === $.BR) {
|
|
223
|
+
// These end tags leave foreign content, as the start tags of `causesExit` do.
|
|
224
|
+
while (this.inForeignContent)
|
|
225
|
+
this._leaveCurrentNamespace();
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
// The frame's own end tag closes it; an mglyph or malignmark frame sits directly in its text integration
|
|
229
|
+
// point, a MathML element, so the end tags of that element and of the next frame's below close it too.
|
|
230
|
+
const tag = this.frameTags[frames - 1];
|
|
231
|
+
const last = tag === html.TAG_NAMES.MGLYPH || tag === html.TAG_NAMES.MALIGNMARK ? frames - 3 : frames - 1;
|
|
232
|
+
let i = frames - 1;
|
|
233
|
+
while (i >= last && token.tagName !== this.frameTags[i])
|
|
234
|
+
i--;
|
|
235
|
+
if (i >= last) {
|
|
236
|
+
while (this.frameTags.length > i)
|
|
237
|
+
this._leaveCurrentNamespace();
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
this.directFrames = 0;
|
|
241
|
+
}
|
|
176
242
|
}
|
|
177
243
|
// NOTE: adjust end tag name as well for consistency
|
|
178
|
-
if (this.namespaceStack[
|
|
244
|
+
if (this.namespaceStack[this.namespaceStack.length - 1] === html.NS.SVG) {
|
|
179
245
|
foreignContent.adjustTokenSVGTagName(token);
|
|
180
246
|
}
|
|
181
247
|
this.handler.onEndTag(token);
|
|
182
248
|
}
|
|
183
249
|
}
|
|
184
|
-
//# sourceMappingURL=parser-feedback-simulator.js.map
|
package/package.json
CHANGED
|
@@ -2,10 +2,15 @@
|
|
|
2
2
|
"name": "parse5-sax-parser",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"description": "Streaming SAX-style HTML parser.",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "8.0.1",
|
|
6
6
|
"author": "Ivan Nikulin <ifaaan@gmail.com> (https://github.com/inikulin)",
|
|
7
|
-
"contributors":
|
|
8
|
-
|
|
7
|
+
"contributors": [
|
|
8
|
+
"James Garbutt (https://github.com/43081j)",
|
|
9
|
+
"Felix Boehm (https://github.com/fb55)",
|
|
10
|
+
"Ivan Nikulin (https://github.com/inikulin)",
|
|
11
|
+
"Titus (https://github.com/wooorm)"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://parse5.js.org",
|
|
9
14
|
"funding": "https://github.com/inikulin/parse5?sponsor=1",
|
|
10
15
|
"keywords": [
|
|
11
16
|
"parse5",
|
|
@@ -18,16 +23,23 @@
|
|
|
18
23
|
"main": "dist/index.js",
|
|
19
24
|
"module": "dist/index.js",
|
|
20
25
|
"types": "dist/index.d.ts",
|
|
21
|
-
"exports":
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"default": "./dist/index.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
22
31
|
"dependencies": {
|
|
23
|
-
"parse5": "^
|
|
32
|
+
"parse5": "^8.1.1"
|
|
24
33
|
},
|
|
25
34
|
"repository": {
|
|
26
35
|
"type": "git",
|
|
27
36
|
"url": "git://github.com/inikulin/parse5.git"
|
|
28
37
|
},
|
|
29
38
|
"files": [
|
|
30
|
-
"dist",
|
|
31
|
-
"
|
|
32
|
-
]
|
|
39
|
+
"dist/**/*.js",
|
|
40
|
+
"dist/**/*.d.ts"
|
|
41
|
+
],
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"parse5-test-utils": "1.0.0"
|
|
44
|
+
}
|
|
33
45
|
}
|