parse5 2.2.3 → 3.0.3
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/LICENSE +19 -19
- package/README.md +40 -40
- package/lib/common/doctype.js +156 -137
- package/lib/common/foreign_content.js +1 -1
- package/lib/common/html.js +272 -266
- package/lib/common/unicode.js +47 -47
- package/lib/extensions/location_info/open_element_stack_mixin.js +34 -0
- package/lib/extensions/location_info/parser_mixin.js +213 -0
- package/lib/extensions/location_info/tokenizer_mixin.js +117 -0
- package/lib/extensions/position_tracking/preprocessor_mixin.js +72 -0
- package/lib/index.d.ts +1077 -0
- package/lib/index.js +44 -108
- package/lib/parser/formatting_element_list.js +167 -167
- package/lib/parser/index.js +2819 -2817
- package/lib/parser/open_element_stack.js +395 -395
- package/lib/parser/parser_stream.js +76 -0
- package/lib/parser/plain_text_conversion_stream.js +20 -0
- package/lib/sax/dev_null_stream.js +14 -14
- package/lib/sax/index.js +7 -119
- package/lib/serializer/index.js +162 -180
- package/lib/serializer/{stream.js → serializer_stream.js} +28 -49
- package/lib/tokenizer/index.js +65 -22
- package/lib/tokenizer/named_entity_data.js +5 -0
- package/lib/tokenizer/preprocessor.js +0 -8
- package/lib/tree_adapters/default.js +209 -578
- package/lib/tree_adapters/htmlparser2.js +338 -327
- package/lib/{common → utils}/merge_options.js +13 -13
- package/lib/utils/mixin.js +18 -0
- package/package.json +13 -14
- package/lib/location_info/parser_mixin.js +0 -217
- package/lib/location_info/tokenizer_mixin.js +0 -169
- package/lib/parser/stream.js +0 -140
- package/lib/tokenizer/named_entity_trie.js +0 -6
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var WritableStream = require('stream').Writable,
|
|
4
|
+
inherits = require('util').inherits,
|
|
5
|
+
Parser = require('./index');
|
|
6
|
+
|
|
7
|
+
var ParserStream = module.exports = function (options) {
|
|
8
|
+
WritableStream.call(this);
|
|
9
|
+
|
|
10
|
+
this.parser = new Parser(options);
|
|
11
|
+
|
|
12
|
+
this.lastChunkWritten = false;
|
|
13
|
+
this.writeCallback = null;
|
|
14
|
+
this.pausedByScript = false;
|
|
15
|
+
|
|
16
|
+
this.document = this.parser.treeAdapter.createDocument();
|
|
17
|
+
|
|
18
|
+
this.pendingHtmlInsertions = [];
|
|
19
|
+
|
|
20
|
+
this._resume = this._resume.bind(this);
|
|
21
|
+
this._documentWrite = this._documentWrite.bind(this);
|
|
22
|
+
this._scriptHandler = this._scriptHandler.bind(this);
|
|
23
|
+
|
|
24
|
+
this.parser._bootstrap(this.document, null);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
inherits(ParserStream, WritableStream);
|
|
28
|
+
|
|
29
|
+
//WritableStream implementation
|
|
30
|
+
ParserStream.prototype._write = function (chunk, encoding, callback) {
|
|
31
|
+
this.writeCallback = callback;
|
|
32
|
+
this.parser.tokenizer.write(chunk.toString('utf8'), this.lastChunkWritten);
|
|
33
|
+
this._runParsingLoop();
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
ParserStream.prototype.end = function (chunk, encoding, callback) {
|
|
37
|
+
this.lastChunkWritten = true;
|
|
38
|
+
WritableStream.prototype.end.call(this, chunk || '', encoding, callback);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
//Scriptable parser implementation
|
|
42
|
+
ParserStream.prototype._runParsingLoop = function () {
|
|
43
|
+
this.parser.runParsingLoopForCurrentChunk(this.writeCallback, this._scriptHandler);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
ParserStream.prototype._resume = function () {
|
|
47
|
+
if (!this.pausedByScript)
|
|
48
|
+
throw new Error('Parser was already resumed');
|
|
49
|
+
|
|
50
|
+
while (this.pendingHtmlInsertions.length) {
|
|
51
|
+
var html = this.pendingHtmlInsertions.pop();
|
|
52
|
+
|
|
53
|
+
this.parser.tokenizer.insertHtmlAtCurrentPos(html);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this.pausedByScript = false;
|
|
57
|
+
|
|
58
|
+
//NOTE: keep parsing if we don't wait for the next input chunk
|
|
59
|
+
if (this.parser.tokenizer.active)
|
|
60
|
+
this._runParsingLoop();
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
ParserStream.prototype._documentWrite = function (html) {
|
|
64
|
+
if (!this.parser.stopped)
|
|
65
|
+
this.pendingHtmlInsertions.push(html);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
ParserStream.prototype._scriptHandler = function (scriptElement) {
|
|
69
|
+
if (this.listeners('script').length) {
|
|
70
|
+
this.pausedByScript = true;
|
|
71
|
+
this.emit('script', scriptElement, this._documentWrite, this._resume);
|
|
72
|
+
}
|
|
73
|
+
else
|
|
74
|
+
this._runParsingLoop();
|
|
75
|
+
};
|
|
76
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var ParserStream = require('./parser_stream'),
|
|
4
|
+
inherits = require('util').inherits,
|
|
5
|
+
$ = require('../common/html').TAG_NAMES;
|
|
6
|
+
|
|
7
|
+
var PlainTextConversionStream = module.exports = function (options) {
|
|
8
|
+
ParserStream.call(this, options);
|
|
9
|
+
|
|
10
|
+
// NOTE: see https://html.spec.whatwg.org/#read-text
|
|
11
|
+
this.parser._insertFakeElement($.HTML);
|
|
12
|
+
this.parser._insertFakeElement($.HEAD);
|
|
13
|
+
this.parser.openElements.pop();
|
|
14
|
+
this.parser._insertFakeElement($.BODY);
|
|
15
|
+
this.parser._insertFakeElement($.PRE);
|
|
16
|
+
this.parser.treeAdapter.insertText(this.parser.openElements.current, '\n');
|
|
17
|
+
this.parser.switchToPlaintextParsing();
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
inherits(PlainTextConversionStream, ParserStream);
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var WritableStream = require('stream').Writable,
|
|
4
|
-
util = require('util');
|
|
5
|
-
|
|
6
|
-
var DevNullStream = module.exports = function () {
|
|
7
|
-
WritableStream.call(this);
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
util.inherits(DevNullStream, WritableStream);
|
|
11
|
-
|
|
12
|
-
DevNullStream.prototype._write = function (chunk, encoding, cb) {
|
|
13
|
-
cb();
|
|
14
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var WritableStream = require('stream').Writable,
|
|
4
|
+
util = require('util');
|
|
5
|
+
|
|
6
|
+
var DevNullStream = module.exports = function () {
|
|
7
|
+
WritableStream.call(this);
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
util.inherits(DevNullStream, WritableStream);
|
|
11
|
+
|
|
12
|
+
DevNullStream.prototype._write = function (chunk, encoding, cb) {
|
|
13
|
+
cb();
|
|
14
|
+
};
|
package/lib/sax/index.js
CHANGED
|
@@ -4,55 +4,24 @@ var TransformStream = require('stream').Transform,
|
|
|
4
4
|
DevNullStream = require('./dev_null_stream'),
|
|
5
5
|
inherits = require('util').inherits,
|
|
6
6
|
Tokenizer = require('../tokenizer'),
|
|
7
|
+
LocationInfoTokenizerMixin = require('../extensions/location_info/tokenizer_mixin'),
|
|
7
8
|
ParserFeedbackSimulator = require('./parser_feedback_simulator'),
|
|
8
|
-
mergeOptions = require('../
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* @typedef {Object} SAXParserOptions
|
|
12
|
-
*
|
|
13
|
-
* @property {Boolean} [locationInfo=false] - Enables source code location information for the tokens.
|
|
14
|
-
* When enabled, each token event handler will receive {@link LocationInfo} (or {@link StartTagLocationInfo})
|
|
15
|
-
* object as its last argument.
|
|
16
|
-
*/
|
|
9
|
+
mergeOptions = require('../utils/merge_options');
|
|
10
|
+
|
|
17
11
|
var DEFAULT_OPTIONS = {
|
|
18
12
|
locationInfo: false
|
|
19
13
|
};
|
|
20
14
|
|
|
21
|
-
/**
|
|
22
|
-
* Streaming [SAX]{@link https://en.wikipedia.org/wiki/Simple_API_for_XML}-style HTML parser.
|
|
23
|
-
* A [transform stream](https://nodejs.org/api/stream.html#stream_class_stream_transform)
|
|
24
|
-
* (which means you can pipe *through* it, see example).
|
|
25
|
-
* @class SAXParser
|
|
26
|
-
* @memberof parse5
|
|
27
|
-
* @instance
|
|
28
|
-
* @extends stream.Transform
|
|
29
|
-
* @param {SAXParserOptions} options - Parsing options.
|
|
30
|
-
* @example
|
|
31
|
-
* var parse5 = require('parse5');
|
|
32
|
-
* var http = require('http');
|
|
33
|
-
* var fs = require('fs');
|
|
34
|
-
*
|
|
35
|
-
* var file = fs.createWriteStream('/home/google.com.html');
|
|
36
|
-
* var parser = new parse5.SAXParser();
|
|
37
|
-
*
|
|
38
|
-
* parser.on('text', function(text) {
|
|
39
|
-
* // Handle page text content
|
|
40
|
-
* ...
|
|
41
|
-
* });
|
|
42
|
-
*
|
|
43
|
-
* http.get('http://google.com', function(res) {
|
|
44
|
-
* // SAXParser is the Transform stream, which means you can pipe
|
|
45
|
-
* // through it. So, you can analyze page content and, e.g., save it
|
|
46
|
-
* // to the file at the same time:
|
|
47
|
-
* res.pipe(parser).pipe(file);
|
|
48
|
-
* });
|
|
49
|
-
*/
|
|
50
15
|
var SAXParser = module.exports = function (options) {
|
|
51
16
|
TransformStream.call(this);
|
|
52
17
|
|
|
53
18
|
this.options = mergeOptions(DEFAULT_OPTIONS, options);
|
|
54
19
|
|
|
55
20
|
this.tokenizer = new Tokenizer(options);
|
|
21
|
+
|
|
22
|
+
if (this.options.locationInfo)
|
|
23
|
+
new LocationInfoTokenizerMixin(this.tokenizer);
|
|
24
|
+
|
|
56
25
|
this.parserFeedbackSimulator = new ParserFeedbackSimulator(this.tokenizer);
|
|
57
26
|
|
|
58
27
|
this.pendingText = null;
|
|
@@ -90,33 +59,6 @@ SAXParser.prototype.end = function (chunk, encoding, callback) {
|
|
|
90
59
|
TransformStream.prototype.end.call(this, chunk, encoding, callback);
|
|
91
60
|
};
|
|
92
61
|
|
|
93
|
-
/**
|
|
94
|
-
* Stops parsing. Useful if you want the parser to stop consuming CPU time once you've obtained the desired info
|
|
95
|
-
* from the input stream. Doesn't prevent piping, so that data will flow through the parser as usual.
|
|
96
|
-
*
|
|
97
|
-
* @function stop
|
|
98
|
-
* @memberof parse5#SAXParser
|
|
99
|
-
* @instance
|
|
100
|
-
* @example
|
|
101
|
-
* var parse5 = require('parse5');
|
|
102
|
-
* var http = require('http');
|
|
103
|
-
* var fs = require('fs');
|
|
104
|
-
*
|
|
105
|
-
* var file = fs.createWriteStream('/home/google.com.html');
|
|
106
|
-
* var parser = new parse5.SAXParser();
|
|
107
|
-
*
|
|
108
|
-
* parser.on('doctype', function(name, publicId, systemId) {
|
|
109
|
-
* // Process doctype info ans stop parsing
|
|
110
|
-
* ...
|
|
111
|
-
* parser.stop();
|
|
112
|
-
* });
|
|
113
|
-
*
|
|
114
|
-
* http.get('http://google.com', function(res) {
|
|
115
|
-
* // Despite the fact that parser.stop() was called whole
|
|
116
|
-
* // content of the page will be written to the file
|
|
117
|
-
* res.pipe(parser).pipe(file);
|
|
118
|
-
* });
|
|
119
|
-
*/
|
|
120
62
|
SAXParser.prototype.stop = function () {
|
|
121
63
|
this.stopped = true;
|
|
122
64
|
};
|
|
@@ -156,74 +98,20 @@ SAXParser.prototype._handleToken = function (token) {
|
|
|
156
98
|
this.currentTokenLocation = token.location;
|
|
157
99
|
|
|
158
100
|
if (token.type === Tokenizer.START_TAG_TOKEN)
|
|
159
|
-
/**
|
|
160
|
-
* Raised when the parser encounters a start tag.
|
|
161
|
-
* @event startTag
|
|
162
|
-
* @memberof parse5#SAXParser
|
|
163
|
-
* @instance
|
|
164
|
-
* @type {Function}
|
|
165
|
-
* @param {String} name - Tag name.
|
|
166
|
-
* @param {Array} attrs - List of attributes in the `{ name: String, value: String, prefix?: String }` form.
|
|
167
|
-
* @param {Boolean} selfClosing - Indicates if the tag is self-closing.
|
|
168
|
-
* @param {StartTagLocationInfo} [location] - Start tag source code location info.
|
|
169
|
-
* Available if location info is enabled in {@link SAXParserOptions}.
|
|
170
|
-
*/
|
|
171
101
|
this.emit('startTag', token.tagName, token.attrs, token.selfClosing, this.currentTokenLocation);
|
|
172
102
|
|
|
173
103
|
else if (token.type === Tokenizer.END_TAG_TOKEN)
|
|
174
|
-
/**
|
|
175
|
-
* Raised then parser encounters an end tag.
|
|
176
|
-
* @event endTag
|
|
177
|
-
* @memberof parse5#SAXParser
|
|
178
|
-
* @instance
|
|
179
|
-
* @type {Function}
|
|
180
|
-
* @param {String} name - Tag name.
|
|
181
|
-
* @param {LocationInfo} [location] - End tag source code location info.
|
|
182
|
-
* Available if location info is enabled in {@link SAXParserOptions}.
|
|
183
|
-
*/
|
|
184
104
|
this.emit('endTag', token.tagName, this.currentTokenLocation);
|
|
185
105
|
|
|
186
106
|
else if (token.type === Tokenizer.COMMENT_TOKEN)
|
|
187
|
-
/**
|
|
188
|
-
* Raised then parser encounters a comment.
|
|
189
|
-
* @event comment
|
|
190
|
-
* @memberof parse5#SAXParser
|
|
191
|
-
* @instance
|
|
192
|
-
* @type {Function}
|
|
193
|
-
* @param {String} text - Comment text.
|
|
194
|
-
* @param {LocationInfo} [location] - Comment source code location info.
|
|
195
|
-
* Available if location info is enabled in {@link SAXParserOptions}.
|
|
196
|
-
*/
|
|
197
107
|
this.emit('comment', token.data, this.currentTokenLocation);
|
|
198
108
|
|
|
199
109
|
else if (token.type === Tokenizer.DOCTYPE_TOKEN)
|
|
200
|
-
/**
|
|
201
|
-
* Raised then parser encounters a [document type declaration]{@link https://en.wikipedia.org/wiki/Document_type_declaration}.
|
|
202
|
-
* @event doctype
|
|
203
|
-
* @memberof parse5#SAXParser
|
|
204
|
-
* @instance
|
|
205
|
-
* @type {Function}
|
|
206
|
-
* @param {String} name - Document type name.
|
|
207
|
-
* @param {String} publicId - Document type public identifier.
|
|
208
|
-
* @param {String} systemId - Document type system identifier.
|
|
209
|
-
* @param {LocationInfo} [location] - Document type declaration source code location info.
|
|
210
|
-
* Available if location info is enabled in {@link SAXParserOptions}.
|
|
211
|
-
*/
|
|
212
110
|
this.emit('doctype', token.name, token.publicId, token.systemId, this.currentTokenLocation);
|
|
213
111
|
};
|
|
214
112
|
|
|
215
113
|
SAXParser.prototype._emitPendingText = function () {
|
|
216
114
|
if (this.pendingText !== null) {
|
|
217
|
-
/**
|
|
218
|
-
* Raised then parser encounters text content.
|
|
219
|
-
* @event text
|
|
220
|
-
* @memberof parse5#SAXParser
|
|
221
|
-
* @instance
|
|
222
|
-
* @type {Function}
|
|
223
|
-
* @param {String} text - Text content.
|
|
224
|
-
* @param {LocationInfo} [location] - Text content code location info.
|
|
225
|
-
* Available if location info is enabled in {@link SAXParserOptions}.
|
|
226
|
-
*/
|
|
227
115
|
this.emit('text', this.pendingText, this.currentTokenLocation);
|
|
228
116
|
this.pendingText = null;
|
|
229
117
|
}
|
package/lib/serializer/index.js
CHANGED
|
@@ -1,180 +1,162 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var defaultTreeAdapter = require('../tree_adapters/default'),
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
HTML = require('../common/html');
|
|
7
|
-
|
|
8
|
-
//Aliases
|
|
9
|
-
var $ = HTML.TAG_NAMES,
|
|
10
|
-
NS = HTML.NAMESPACES;
|
|
11
|
-
|
|
12
|
-
//Default serializer options
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
var
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
this.html += attr.name;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
this.html += content;
|
|
165
|
-
|
|
166
|
-
else
|
|
167
|
-
this.html += Serializer.escapeString(content, false);
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
Serializer.prototype._serializeCommentNode = function (node) {
|
|
171
|
-
this.html += '<!--' + this.treeAdapter.getCommentNodeContent(node) + '-->';
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
Serializer.prototype._serializeDocumentTypeNode = function (node) {
|
|
175
|
-
var name = this.treeAdapter.getDocumentTypeNodeName(node),
|
|
176
|
-
publicId = this.treeAdapter.getDocumentTypeNodePublicId(node),
|
|
177
|
-
systemId = this.treeAdapter.getDocumentTypeNodeSystemId(node);
|
|
178
|
-
|
|
179
|
-
this.html += '<' + doctype.serializeContent(name, publicId, systemId) + '>';
|
|
180
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var defaultTreeAdapter = require('../tree_adapters/default'),
|
|
4
|
+
mergeOptions = require('../utils/merge_options'),
|
|
5
|
+
doctype = require('../common/doctype'),
|
|
6
|
+
HTML = require('../common/html');
|
|
7
|
+
|
|
8
|
+
//Aliases
|
|
9
|
+
var $ = HTML.TAG_NAMES,
|
|
10
|
+
NS = HTML.NAMESPACES;
|
|
11
|
+
|
|
12
|
+
//Default serializer options
|
|
13
|
+
var DEFAULT_OPTIONS = {
|
|
14
|
+
treeAdapter: defaultTreeAdapter
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
//Escaping regexes
|
|
18
|
+
var AMP_REGEX = /&/g,
|
|
19
|
+
NBSP_REGEX = /\u00a0/g,
|
|
20
|
+
DOUBLE_QUOTE_REGEX = /"/g,
|
|
21
|
+
LT_REGEX = /</g,
|
|
22
|
+
GT_REGEX = />/g;
|
|
23
|
+
|
|
24
|
+
//Serializer
|
|
25
|
+
var Serializer = module.exports = function (node, options) {
|
|
26
|
+
this.options = mergeOptions(DEFAULT_OPTIONS, options);
|
|
27
|
+
this.treeAdapter = this.options.treeAdapter;
|
|
28
|
+
|
|
29
|
+
this.html = '';
|
|
30
|
+
this.startNode = node;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// NOTE: exported as static method for the testing purposes
|
|
34
|
+
Serializer.escapeString = function (str, attrMode) {
|
|
35
|
+
str = str
|
|
36
|
+
.replace(AMP_REGEX, '&')
|
|
37
|
+
.replace(NBSP_REGEX, ' ');
|
|
38
|
+
|
|
39
|
+
if (attrMode)
|
|
40
|
+
str = str.replace(DOUBLE_QUOTE_REGEX, '"');
|
|
41
|
+
|
|
42
|
+
else {
|
|
43
|
+
str = str
|
|
44
|
+
.replace(LT_REGEX, '<')
|
|
45
|
+
.replace(GT_REGEX, '>');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return str;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
//API
|
|
53
|
+
Serializer.prototype.serialize = function () {
|
|
54
|
+
this._serializeChildNodes(this.startNode);
|
|
55
|
+
|
|
56
|
+
return this.html;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
//Internals
|
|
61
|
+
Serializer.prototype._serializeChildNodes = function (parentNode) {
|
|
62
|
+
var childNodes = this.treeAdapter.getChildNodes(parentNode);
|
|
63
|
+
|
|
64
|
+
if (childNodes) {
|
|
65
|
+
for (var i = 0, cnLength = childNodes.length; i < cnLength; i++) {
|
|
66
|
+
var currentNode = childNodes[i];
|
|
67
|
+
|
|
68
|
+
if (this.treeAdapter.isElementNode(currentNode))
|
|
69
|
+
this._serializeElement(currentNode);
|
|
70
|
+
|
|
71
|
+
else if (this.treeAdapter.isTextNode(currentNode))
|
|
72
|
+
this._serializeTextNode(currentNode);
|
|
73
|
+
|
|
74
|
+
else if (this.treeAdapter.isCommentNode(currentNode))
|
|
75
|
+
this._serializeCommentNode(currentNode);
|
|
76
|
+
|
|
77
|
+
else if (this.treeAdapter.isDocumentTypeNode(currentNode))
|
|
78
|
+
this._serializeDocumentTypeNode(currentNode);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
Serializer.prototype._serializeElement = function (node) {
|
|
84
|
+
var tn = this.treeAdapter.getTagName(node),
|
|
85
|
+
ns = this.treeAdapter.getNamespaceURI(node);
|
|
86
|
+
|
|
87
|
+
this.html += '<' + tn;
|
|
88
|
+
this._serializeAttributes(node);
|
|
89
|
+
this.html += '>';
|
|
90
|
+
|
|
91
|
+
if (tn !== $.AREA && tn !== $.BASE && tn !== $.BASEFONT && tn !== $.BGSOUND && tn !== $.BR && tn !== $.BR &&
|
|
92
|
+
tn !== $.COL && tn !== $.EMBED && tn !== $.FRAME && tn !== $.HR && tn !== $.IMG && tn !== $.INPUT &&
|
|
93
|
+
tn !== $.KEYGEN && tn !== $.LINK && tn !== $.MENUITEM && tn !== $.META && tn !== $.PARAM && tn !== $.SOURCE &&
|
|
94
|
+
tn !== $.TRACK && tn !== $.WBR) {
|
|
95
|
+
|
|
96
|
+
var childNodesHolder = tn === $.TEMPLATE && ns === NS.HTML ?
|
|
97
|
+
this.treeAdapter.getTemplateContent(node) :
|
|
98
|
+
node;
|
|
99
|
+
|
|
100
|
+
this._serializeChildNodes(childNodesHolder);
|
|
101
|
+
this.html += '</' + tn + '>';
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
Serializer.prototype._serializeAttributes = function (node) {
|
|
106
|
+
var attrs = this.treeAdapter.getAttrList(node);
|
|
107
|
+
|
|
108
|
+
for (var i = 0, attrsLength = attrs.length; i < attrsLength; i++) {
|
|
109
|
+
var attr = attrs[i],
|
|
110
|
+
value = Serializer.escapeString(attr.value, true);
|
|
111
|
+
|
|
112
|
+
this.html += ' ';
|
|
113
|
+
|
|
114
|
+
if (!attr.namespace)
|
|
115
|
+
this.html += attr.name;
|
|
116
|
+
|
|
117
|
+
else if (attr.namespace === NS.XML)
|
|
118
|
+
this.html += 'xml:' + attr.name;
|
|
119
|
+
|
|
120
|
+
else if (attr.namespace === NS.XMLNS) {
|
|
121
|
+
if (attr.name !== 'xmlns')
|
|
122
|
+
this.html += 'xmlns:';
|
|
123
|
+
|
|
124
|
+
this.html += attr.name;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
else if (attr.namespace === NS.XLINK)
|
|
128
|
+
this.html += 'xlink:' + attr.name;
|
|
129
|
+
|
|
130
|
+
else
|
|
131
|
+
this.html += attr.namespace + ':' + attr.name;
|
|
132
|
+
|
|
133
|
+
this.html += '="' + value + '"';
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
Serializer.prototype._serializeTextNode = function (node) {
|
|
138
|
+
var content = this.treeAdapter.getTextNodeContent(node),
|
|
139
|
+
parent = this.treeAdapter.getParentNode(node),
|
|
140
|
+
parentTn = void 0;
|
|
141
|
+
|
|
142
|
+
if (parent && this.treeAdapter.isElementNode(parent))
|
|
143
|
+
parentTn = this.treeAdapter.getTagName(parent);
|
|
144
|
+
|
|
145
|
+
if (parentTn === $.STYLE || parentTn === $.SCRIPT || parentTn === $.XMP || parentTn === $.IFRAME ||
|
|
146
|
+
parentTn === $.NOEMBED || parentTn === $.NOFRAMES || parentTn === $.PLAINTEXT || parentTn === $.NOSCRIPT)
|
|
147
|
+
|
|
148
|
+
this.html += content;
|
|
149
|
+
|
|
150
|
+
else
|
|
151
|
+
this.html += Serializer.escapeString(content, false);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
Serializer.prototype._serializeCommentNode = function (node) {
|
|
155
|
+
this.html += '<!--' + this.treeAdapter.getCommentNodeContent(node) + '-->';
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
Serializer.prototype._serializeDocumentTypeNode = function (node) {
|
|
159
|
+
var name = this.treeAdapter.getDocumentTypeNodeName(node);
|
|
160
|
+
|
|
161
|
+
this.html += '<' + doctype.serializeContent(name, null, null) + '>';
|
|
162
|
+
};
|