parse5 2.2.2 → 3.0.2

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.
@@ -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
@@ -7,46 +7,10 @@ var TransformStream = require('stream').Transform,
7
7
  ParserFeedbackSimulator = require('./parser_feedback_simulator'),
8
8
  mergeOptions = require('../common/merge_options');
9
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
- */
17
10
  var DEFAULT_OPTIONS = {
18
11
  locationInfo: false
19
12
  };
20
13
 
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
14
  var SAXParser = module.exports = function (options) {
51
15
  TransformStream.call(this);
52
16
 
@@ -90,33 +54,6 @@ SAXParser.prototype.end = function (chunk, encoding, callback) {
90
54
  TransformStream.prototype.end.call(this, chunk, encoding, callback);
91
55
  };
92
56
 
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
57
  SAXParser.prototype.stop = function () {
121
58
  this.stopped = true;
122
59
  };
@@ -156,74 +93,20 @@ SAXParser.prototype._handleToken = function (token) {
156
93
  this.currentTokenLocation = token.location;
157
94
 
158
95
  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
96
  this.emit('startTag', token.tagName, token.attrs, token.selfClosing, this.currentTokenLocation);
172
97
 
173
98
  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
99
  this.emit('endTag', token.tagName, this.currentTokenLocation);
185
100
 
186
101
  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
102
  this.emit('comment', token.data, this.currentTokenLocation);
198
103
 
199
104
  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
105
  this.emit('doctype', token.name, token.publicId, token.systemId, this.currentTokenLocation);
213
106
  };
214
107
 
215
108
  SAXParser.prototype._emitPendingText = function () {
216
109
  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
110
  this.emit('text', this.pendingText, this.currentTokenLocation);
228
111
  this.pendingText = null;
229
112
  }
@@ -1,180 +1,162 @@
1
- 'use strict';
2
-
3
- var defaultTreeAdapter = require('../tree_adapters/default'),
4
- doctype = require('../common/doctype'),
5
- mergeOptions = require('../common/merge_options'),
6
- HTML = require('../common/html');
7
-
8
- //Aliases
9
- var $ = HTML.TAG_NAMES,
10
- NS = HTML.NAMESPACES;
11
-
12
- //Default serializer options
13
- /**
14
- * @typedef {Object} SerializerOptions
15
- *
16
- * @property {TreeAdapter} [treeAdapter=parse5.treeAdapters.default] - Specifies input tree format.
17
- */
18
- var DEFAULT_OPTIONS = {
19
- treeAdapter: defaultTreeAdapter
20
- };
21
-
22
- //Escaping regexes
23
- var AMP_REGEX = /&/g,
24
- NBSP_REGEX = /\u00a0/g,
25
- DOUBLE_QUOTE_REGEX = /"/g,
26
- LT_REGEX = /</g,
27
- GT_REGEX = />/g;
28
-
29
- //Serializer
30
- var Serializer = module.exports = function (node, options) {
31
- this.options = mergeOptions(DEFAULT_OPTIONS, options);
32
- this.treeAdapter = this.options.treeAdapter;
33
-
34
- this.html = '';
35
- this.startNode = node;
36
- };
37
-
38
- // NOTE: exported as static method for the testing purposes
39
- Serializer.escapeString = function (str, attrMode) {
40
- str = str
41
- .replace(AMP_REGEX, '&amp;')
42
- .replace(NBSP_REGEX, '&nbsp;');
43
-
44
- if (attrMode)
45
- str = str.replace(DOUBLE_QUOTE_REGEX, '&quot;');
46
-
47
- else {
48
- str = str
49
- .replace(LT_REGEX, '&lt;')
50
- .replace(GT_REGEX, '&gt;');
51
- }
52
-
53
- return str;
54
- };
55
-
56
-
57
- //API
58
- Serializer.prototype.serialize = function () {
59
- this._serializeChildNodes(this.startNode);
60
-
61
- return this.html;
62
- };
63
-
64
-
65
- //Internals
66
- Serializer.prototype._serializeChildNodes = function (parentNode) {
67
- var childNodes = this.treeAdapter.getChildNodes(parentNode);
68
-
69
- if (childNodes) {
70
- for (var i = 0, cnLength = childNodes.length; i < cnLength; i++) {
71
- var currentNode = childNodes[i];
72
-
73
- if (this.treeAdapter.isElementNode(currentNode))
74
- this._serializeElement(currentNode);
75
-
76
- else if (this.treeAdapter.isTextNode(currentNode))
77
- this._serializeTextNode(currentNode);
78
-
79
- else if (this.treeAdapter.isCommentNode(currentNode))
80
- this._serializeCommentNode(currentNode);
81
-
82
- else if (this.treeAdapter.isDocumentTypeNode(currentNode))
83
- this._serializeDocumentTypeNode(currentNode);
84
- }
85
- }
86
- };
87
-
88
- Serializer.prototype._serializeElement = function (node) {
89
- var tn = this.treeAdapter.getTagName(node),
90
- ns = this.treeAdapter.getNamespaceURI(node);
91
-
92
- this.html += '<' + tn;
93
- this._serializeAttributes(node);
94
- this.html += '>';
95
-
96
- if (tn !== $.AREA && tn !== $.BASE && tn !== $.BASEFONT && tn !== $.BGSOUND && tn !== $.BR && tn !== $.BR &&
97
- tn !== $.COL && tn !== $.EMBED && tn !== $.FRAME && tn !== $.HR && tn !== $.IMG && tn !== $.INPUT &&
98
- tn !== $.KEYGEN && tn !== $.LINK && tn !== $.MENUITEM && tn !== $.META && tn !== $.PARAM && tn !== $.SOURCE &&
99
- tn !== $.TRACK && tn !== $.WBR) {
100
-
101
- if (tn === $.PRE || tn === $.TEXTAREA || tn === $.LISTING) {
102
- var firstChild = this.treeAdapter.getFirstChild(node);
103
-
104
- if (firstChild && this.treeAdapter.isTextNode(firstChild)) {
105
- var content = this.treeAdapter.getTextNodeContent(firstChild);
106
-
107
- if (content[0] === '\n')
108
- this.html += '\n';
109
- }
110
- }
111
-
112
- var childNodesHolder = tn === $.TEMPLATE && ns === NS.HTML ?
113
- this.treeAdapter.getTemplateContent(node) :
114
- node;
115
-
116
- this._serializeChildNodes(childNodesHolder);
117
- this.html += '</' + tn + '>';
118
- }
119
- };
120
-
121
- Serializer.prototype._serializeAttributes = function (node) {
122
- var attrs = this.treeAdapter.getAttrList(node);
123
-
124
- for (var i = 0, attrsLength = attrs.length; i < attrsLength; i++) {
125
- var attr = attrs[i],
126
- value = Serializer.escapeString(attr.value, true);
127
-
128
- this.html += ' ';
129
-
130
- if (!attr.namespace)
131
- this.html += attr.name;
132
-
133
- else if (attr.namespace === NS.XML)
134
- this.html += 'xml:' + attr.name;
135
-
136
- else if (attr.namespace === NS.XMLNS) {
137
- if (attr.name !== 'xmlns')
138
- this.html += 'xmlns:';
139
-
140
- this.html += attr.name;
141
- }
142
-
143
- else if (attr.namespace === NS.XLINK)
144
- this.html += 'xlink:' + attr.name;
145
-
146
- else
147
- this.html += attr.namespace + ':' + attr.name;
148
-
149
- this.html += '="' + value + '"';
150
- }
151
- };
152
-
153
- Serializer.prototype._serializeTextNode = function (node) {
154
- var content = this.treeAdapter.getTextNodeContent(node),
155
- parent = this.treeAdapter.getParentNode(node),
156
- parentTn = void 0;
157
-
158
- if (parent && this.treeAdapter.isElementNode(parent))
159
- parentTn = this.treeAdapter.getTagName(parent);
160
-
161
- if (parentTn === $.STYLE || parentTn === $.SCRIPT || parentTn === $.XMP || parentTn === $.IFRAME ||
162
- parentTn === $.NOEMBED || parentTn === $.NOFRAMES || parentTn === $.PLAINTEXT || parentTn === $.NOSCRIPT)
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
+ doctype = require('../common/doctype'),
5
+ mergeOptions = require('../common/merge_options'),
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, '&amp;')
37
+ .replace(NBSP_REGEX, '&nbsp;');
38
+
39
+ if (attrMode)
40
+ str = str.replace(DOUBLE_QUOTE_REGEX, '&quot;');
41
+
42
+ else {
43
+ str = str
44
+ .replace(LT_REGEX, '&lt;')
45
+ .replace(GT_REGEX, '&gt;');
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
+ };