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 CHANGED
@@ -16,9 +16,25 @@
16
16
  <br>
17
17
 
18
18
  <p align="center">
19
- 📖 <a href="https://parse5.js.org/modules/parse5_sax_parser.html"><b>Documentation</b></a> 📖
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">
@@ -1,6 +1,4 @@
1
- /// <reference types="node" />
2
1
  import { Writable } from 'node:stream';
3
2
  export declare class DevNullStream extends Writable {
4
3
  _write(_chunk: string, _encoding: string, cb: () => void): void;
5
4
  }
6
- //# sourceMappingURL=dev-null-stream.d.ts.map
@@ -4,4 +4,3 @@ export class DevNullStream extends Writable {
4
4
  cb();
5
5
  }
6
6
  }
7
- //# sourceMappingURL=dev-null-stream.js.map
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
- if (this.skipNextNewLine && token.chars.charCodeAt(0) === LINE_FEED_CODE_POINT) {
34
- this.skipNextNewLine = false;
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.unshift(namespace);
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.shift();
70
- this.inForeignContent = this.namespaceStack[0] !== html.NS.HTML;
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
- this._enterNamespace(html.NS.SVG);
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 $.MATH: {
111
- this._enterNamespace(html.NS.MATHML);
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
- this._leaveCurrentNamespace();
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 currentNs = this.namespaceStack[0];
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
- if (!token.selfClosing && foreignContent.isIntegrationPoint(tn, currentNs, token.attrs)) {
133
- this._enterNamespace(html.NS.HTML);
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
- let tn = token.tagID;
211
+ const tn = token.tagID;
212
+ const frames = this.frameTags.length;
160
213
  if (!this.inForeignContent) {
161
- const previousNs = this.namespaceStack[1];
162
- if (previousNs === html.NS.SVG) {
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 ((tn === $.SVG && this.namespaceStack[0] === html.NS.SVG) ||
174
- (tn === $.MATH && this.namespaceStack[0] === html.NS.MATHML)) {
175
- this._leaveCurrentNamespace();
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[0] === html.NS.SVG) {
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": "7.0.0",
5
+ "version": "8.0.1",
6
6
  "author": "Ivan Nikulin <ifaaan@gmail.com> (https://github.com/inikulin)",
7
- "contributors": "https://github.com/inikulin/parse5/graphs/contributors",
8
- "homepage": "https://github.com/inikulin/parse5",
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": "./dist/index.js",
26
+ "exports": {
27
+ ".": {
28
+ "default": "./dist/index.js"
29
+ }
30
+ },
22
31
  "dependencies": {
23
- "parse5": "^7.0.0"
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
- "!*.map"
32
- ]
39
+ "dist/**/*.js",
40
+ "dist/**/*.d.ts"
41
+ ],
42
+ "devDependencies": {
43
+ "parse5-test-utils": "1.0.0"
44
+ }
33
45
  }