OLD | NEW |
(Empty) | |
| 1 /// Additional feature tests that aren't based on test data. |
| 2 library parser_feature_test; |
| 3 |
| 4 import 'package:unittest/unittest.dart'; |
| 5 import 'package:html/dom.dart'; |
| 6 import 'package:html/parser.dart'; |
| 7 import 'package:html/src/constants.dart'; |
| 8 import 'package:html/src/encoding_parser.dart'; |
| 9 import 'package:html/src/treebuilder.dart'; |
| 10 import 'package:source_span/source_span.dart'; |
| 11 |
| 12 main() { |
| 13 _testElementSpans(); |
| 14 test('doctype is cloneable', () { |
| 15 var doc = parse('<!doctype HTML>'); |
| 16 DocumentType doctype = doc.nodes[0]; |
| 17 expect(doctype.clone(false).toString(), '<!DOCTYPE html>'); |
| 18 }); |
| 19 |
| 20 test('line counter', () { |
| 21 // http://groups.google.com/group/html5lib-discuss/browse_frm/thread/f4f00e4
a2f26d5c0 |
| 22 var doc = parse("<pre>\nx\n>\n</pre>"); |
| 23 expect(doc.body.innerHtml, "<pre>x\n>\n</pre>"); |
| 24 }); |
| 25 |
| 26 test('namespace html elements on', () { |
| 27 var doc = new HtmlParser('', tree: new TreeBuilder(true)).parse(); |
| 28 expect(doc.nodes[0].namespaceUri, Namespaces.html); |
| 29 }); |
| 30 |
| 31 test('namespace html elements off', () { |
| 32 var doc = new HtmlParser('', tree: new TreeBuilder(false)).parse(); |
| 33 expect(doc.nodes[0].namespaceUri, null); |
| 34 }); |
| 35 |
| 36 test('parse error spans - full', () { |
| 37 var parser = new HtmlParser(''' |
| 38 <!DOCTYPE html> |
| 39 <html> |
| 40 <body> |
| 41 <!DOCTYPE html> |
| 42 </body> |
| 43 </html> |
| 44 ''', generateSpans: true, sourceUrl: 'ParseError'); |
| 45 var doc = parser.parse(); |
| 46 expect(doc.body.outerHtml, '<body>\n \n \n\n</body>'); |
| 47 expect(parser.errors.length, 1); |
| 48 ParseError error = parser.errors[0]; |
| 49 expect(error.errorCode, 'unexpected-doctype'); |
| 50 |
| 51 // Note: these values are 0-based, but the printed format is 1-based. |
| 52 expect(error.span.start.line, 3); |
| 53 expect(error.span.end.line, 3); |
| 54 expect(error.span.start.column, 2); |
| 55 expect(error.span.end.column, 17); |
| 56 expect(error.span.text, '<!DOCTYPE html>'); |
| 57 |
| 58 expect(error.toString(), ''' |
| 59 On line 4, column 3 of ParseError: Unexpected DOCTYPE. Ignored. |
| 60 <!DOCTYPE html> |
| 61 ^^^^^^^^^^^^^^^'''); |
| 62 }); |
| 63 |
| 64 test('parse error spans - minimal', () { |
| 65 var parser = new HtmlParser(''' |
| 66 <!DOCTYPE html> |
| 67 <html> |
| 68 <body> |
| 69 <!DOCTYPE html> |
| 70 </body> |
| 71 </html> |
| 72 '''); |
| 73 var doc = parser.parse(); |
| 74 expect(doc.body.outerHtml, '<body>\n \n \n\n</body>'); |
| 75 expect(parser.errors.length, 1); |
| 76 ParseError error = parser.errors[0]; |
| 77 expect(error.errorCode, 'unexpected-doctype'); |
| 78 expect(error.span.start.line, 3); |
| 79 // Note: error position is at the end, not the beginning |
| 80 expect(error.span.start.column, 17); |
| 81 }); |
| 82 |
| 83 test('text spans should have the correct length', () { |
| 84 var textContent = '\n hello {{name}}'; |
| 85 var html = '<body><div>$textContent</div>'; |
| 86 var doc = parse(html, generateSpans: true); |
| 87 Text text = doc.body.nodes[0].nodes[0]; |
| 88 expect(text, new isInstanceOf<Text>()); |
| 89 expect(text.data, textContent); |
| 90 expect(text.sourceSpan.start.offset, html.indexOf(textContent)); |
| 91 expect(text.sourceSpan.length, textContent.length); |
| 92 }); |
| 93 |
| 94 test('attribute spans', () { |
| 95 var text = '<element name="x-foo" extends="x-bar" constructor="Foo">'; |
| 96 var doc = parse(text, generateSpans: true); |
| 97 var elem = doc.querySelector('element'); |
| 98 expect(elem.sourceSpan.start.offset, 0); |
| 99 expect(elem.sourceSpan.end.offset, text.length); |
| 100 expect(elem.sourceSpan.text, text); |
| 101 |
| 102 expect(elem.attributeSpans['quux'], null); |
| 103 |
| 104 var span = elem.attributeSpans['extends']; |
| 105 expect(span.start.offset, text.indexOf('extends')); |
| 106 expect(span.text, 'extends="x-bar"'); |
| 107 }); |
| 108 |
| 109 test('attribute value spans', () { |
| 110 var text = '<element name="x-foo" extends="x-bar" constructor="Foo">'; |
| 111 var doc = parse(text, generateSpans: true); |
| 112 var elem = doc.querySelector('element'); |
| 113 |
| 114 expect(elem.attributeValueSpans['quux'], null); |
| 115 |
| 116 var span = elem.attributeValueSpans['extends']; |
| 117 expect(span.start.offset, text.indexOf('x-bar')); |
| 118 expect(span.text, 'x-bar'); |
| 119 }); |
| 120 |
| 121 test('attribute spans if no attributes', () { |
| 122 var text = '<element>'; |
| 123 var doc = parse(text, generateSpans: true); |
| 124 var elem = doc.querySelector('element'); |
| 125 |
| 126 expect(elem.attributeSpans['quux'], null); |
| 127 expect(elem.attributeValueSpans['quux'], null); |
| 128 }); |
| 129 |
| 130 test('attribute spans if no attribute value', () { |
| 131 var text = '<foo template>'; |
| 132 var doc = parse(text, generateSpans: true); |
| 133 var elem = doc.querySelector('foo'); |
| 134 |
| 135 expect( |
| 136 elem.attributeSpans['template'].start.offset, text.indexOf('template')); |
| 137 expect(elem.attributeValueSpans.containsKey('template'), false); |
| 138 }); |
| 139 |
| 140 test('attribute spans null if code parsed without spans', () { |
| 141 var text = '<element name="x-foo" extends="x-bar" constructor="Foo">'; |
| 142 var doc = parse(text); |
| 143 var elem = doc.querySelector('element'); |
| 144 expect(elem.sourceSpan, null); |
| 145 expect(elem.attributeSpans['quux'], null); |
| 146 expect(elem.attributeSpans['extends'], null); |
| 147 }); |
| 148 |
| 149 test('void element innerHTML', () { |
| 150 var doc = parse('<div></div>'); |
| 151 expect(doc.body.innerHtml, '<div></div>'); |
| 152 doc = parse('<body><script></script></body>'); |
| 153 expect(doc.body.innerHtml, '<script></script>'); |
| 154 doc = parse('<br>'); |
| 155 expect(doc.body.innerHtml, '<br>'); |
| 156 doc = parse('<br><foo><bar>'); |
| 157 expect(doc.body.innerHtml, '<br><foo><bar></bar></foo>'); |
| 158 }); |
| 159 |
| 160 test('empty document has html, body, and head', () { |
| 161 var doc = parse(''); |
| 162 var html = '<html><head></head><body></body></html>'; |
| 163 expect(doc.outerHtml, html); |
| 164 expect(doc.documentElement.outerHtml, html); |
| 165 expect(doc.head.outerHtml, '<head></head>'); |
| 166 expect(doc.body.outerHtml, '<body></body>'); |
| 167 }); |
| 168 |
| 169 test('strange table case', () { |
| 170 var doc = parse('<table><tbody><foo>').body; |
| 171 expect(doc.innerHtml, '<foo></foo><table><tbody></tbody></table>'); |
| 172 }); |
| 173 |
| 174 group('html serialization', () { |
| 175 test('attribute order', () { |
| 176 // Note: the spec only requires a stable order. |
| 177 // However, we preserve the input order via LinkedHashMap |
| 178 var body = parse('<foo d=1 a=2 c=3 b=4>').body; |
| 179 expect(body.innerHtml, '<foo d="1" a="2" c="3" b="4"></foo>'); |
| 180 expect(body.querySelector('foo').attributes.remove('a'), '2'); |
| 181 expect(body.innerHtml, '<foo d="1" c="3" b="4"></foo>'); |
| 182 body.querySelector('foo').attributes['a'] = '0'; |
| 183 expect(body.innerHtml, '<foo d="1" c="3" b="4" a="0"></foo>'); |
| 184 }); |
| 185 |
| 186 test('escaping Text node in <script>', () { |
| 187 Element e = parseFragment('<script>a && b</script>').firstChild; |
| 188 expect(e.outerHtml, '<script>a && b</script>'); |
| 189 }); |
| 190 |
| 191 test('escaping Text node in <span>', () { |
| 192 Element e = parseFragment('<span>a && b</span>').firstChild; |
| 193 expect(e.outerHtml, '<span>a && b</span>'); |
| 194 }); |
| 195 |
| 196 test('Escaping attributes', () { |
| 197 Element e = parseFragment('<div class="a<b>">').firstChild; |
| 198 expect(e.outerHtml, '<div class="a<b>"></div>'); |
| 199 e = parseFragment('<div class=\'a"b\'>').firstChild; |
| 200 expect(e.outerHtml, '<div class="a"b"></div>'); |
| 201 }); |
| 202 |
| 203 test('Escaping non-breaking space', () { |
| 204 var text = '<span>foO\u00A0bar</span>'; |
| 205 expect(text.codeUnitAt(text.indexOf('O') + 1), 0xA0); |
| 206 Element e = parseFragment(text).firstChild; |
| 207 expect(e.outerHtml, '<span>foO bar</span>'); |
| 208 }); |
| 209 |
| 210 test('Newline after <pre>', () { |
| 211 Element e = parseFragment('<pre>\n\nsome text</span>').firstChild; |
| 212 expect((e.firstChild as Text).data, '\nsome text'); |
| 213 expect(e.outerHtml, '<pre>\n\nsome text</pre>'); |
| 214 |
| 215 e = parseFragment('<pre>\nsome text</span>').firstChild; |
| 216 expect((e.firstChild as Text).data, 'some text'); |
| 217 expect(e.outerHtml, '<pre>some text</pre>'); |
| 218 }); |
| 219 |
| 220 test('xml namespaces', () { |
| 221 // Note: this is a nonsensical example, but it triggers the behavior |
| 222 // we're looking for with attribute names in foreign content. |
| 223 var doc = parse(''' |
| 224 <body> |
| 225 <svg> |
| 226 <desc xlink:type="simple" |
| 227 xlink:href="http://example.com/logo.png" |
| 228 xlink:show="new"></desc> |
| 229 '''); |
| 230 var n = doc.querySelector('desc'); |
| 231 var keys = n.attributes.keys.toList(); |
| 232 expect(keys[0], new isInstanceOf<AttributeName>()); |
| 233 expect(keys[0].prefix, 'xlink'); |
| 234 expect(keys[0].namespace, 'http://www.w3.org/1999/xlink'); |
| 235 expect(keys[0].name, 'type'); |
| 236 |
| 237 expect(n.outerHtml, '<desc xlink:type="simple" ' |
| 238 'xlink:href="http://example.com/logo.png" xlink:show="new"></desc>'); |
| 239 }); |
| 240 }); |
| 241 |
| 242 test('error printing without spans', () { |
| 243 var parser = new HtmlParser('foo'); |
| 244 var doc = parser.parse(); |
| 245 expect(doc.body.innerHtml, 'foo'); |
| 246 expect(parser.errors.length, 1); |
| 247 expect(parser.errors[0].errorCode, 'expected-doctype-but-got-chars'); |
| 248 expect(parser.errors[0].message, |
| 249 'Unexpected non-space characters. Expected DOCTYPE.'); |
| 250 expect(parser.errors[0].toString(), |
| 251 'ParserError on line 1, column 4: Unexpected non-space characters. ' |
| 252 'Expected DOCTYPE.\n' |
| 253 'foo\n' |
| 254 ' ^'); |
| 255 }); |
| 256 |
| 257 test('Element.text', () { |
| 258 var doc = parseFragment('<div>foo<div>bar</div>baz</div>'); |
| 259 var e = doc.firstChild; |
| 260 var text = e.firstChild; |
| 261 expect((text as Text).data, 'foo'); |
| 262 expect(e.text, 'foobarbaz'); |
| 263 |
| 264 e.text = 'FOO'; |
| 265 expect(e.nodes.length, 1); |
| 266 expect(e.firstChild, isNot(text), reason: 'should create a new tree'); |
| 267 expect((e.firstChild as Text).data, 'FOO'); |
| 268 expect(e.text, 'FOO'); |
| 269 }); |
| 270 |
| 271 test('Text.text', () { |
| 272 var doc = parseFragment('<div>foo<div>bar</div>baz</div>'); |
| 273 var e = doc.firstChild; |
| 274 Text text = e.firstChild; |
| 275 expect(text.data, 'foo'); |
| 276 expect(text.text, 'foo'); |
| 277 |
| 278 text.text = 'FOO'; |
| 279 expect(text.data, 'FOO'); |
| 280 expect(e.text, 'FOObarbaz'); |
| 281 expect(text.text, 'FOO'); |
| 282 }); |
| 283 |
| 284 test('Comment.text', () { |
| 285 var doc = parseFragment('<div><!--foo-->bar</div>'); |
| 286 var e = doc.firstChild; |
| 287 var c = e.firstChild; |
| 288 expect((c as Comment).data, 'foo'); |
| 289 expect(c.text, 'foo'); |
| 290 expect(e.text, 'bar'); |
| 291 |
| 292 c.text = 'qux'; |
| 293 expect(c.data, 'qux'); |
| 294 expect(c.text, 'qux'); |
| 295 expect(e.text, 'bar'); |
| 296 }); |
| 297 |
| 298 test('foreignObject end tag', () { |
| 299 var p = new HtmlParser(''' |
| 300 <svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" |
| 301 version="1.1"> |
| 302 <foreignObject width="320px" height="200px"> |
| 303 <x-flow></x-flow> |
| 304 </foreignObject> |
| 305 </svg>'''); |
| 306 var doc = p.parseFragment(); |
| 307 expect(p.errors, isEmpty); |
| 308 var svg = doc.querySelector('svg'); |
| 309 expect(svg.children[0].children[0].localName, 'x-flow'); |
| 310 }); |
| 311 |
| 312 group('Encoding pre-parser', () { |
| 313 getEncoding(s) => new EncodingParser(s.codeUnits).getEncoding(); |
| 314 |
| 315 test('gets encoding from meta charset', () { |
| 316 expect(getEncoding('<meta charset="utf-16">'), 'utf-16'); |
| 317 }); |
| 318 |
| 319 test('gets encoding from meta in head', () { |
| 320 expect(getEncoding('<head><meta charset="utf-16">'), 'utf-16'); |
| 321 }); |
| 322 |
| 323 test('skips comments', () { |
| 324 expect(getEncoding('<!--comment--><meta charset="utf-16">'), 'utf-16'); |
| 325 }); |
| 326 |
| 327 test('stops if no match', () { |
| 328 // missing closing tag |
| 329 expect(getEncoding('<meta charset="utf-16"'), null); |
| 330 }); |
| 331 |
| 332 test('ignores whitespace', () { |
| 333 expect(getEncoding(' <meta charset="utf-16">'), 'utf-16'); |
| 334 }); |
| 335 }); |
| 336 } |
| 337 |
| 338 _testElementSpans() { |
| 339 assertSpan(SourceSpan span, int offset, int end, String text) { |
| 340 expect(span, isNotNull); |
| 341 expect(span.start.offset, offset); |
| 342 expect(span.end.offset, end); |
| 343 expect(span.text, text); |
| 344 } |
| 345 |
| 346 group('element spans', () { |
| 347 test('html and body', () { |
| 348 var text = '<html><body>123</body></html>'; |
| 349 var doc = parse(text, generateSpans: true); |
| 350 { |
| 351 var elem = doc.querySelector('html'); |
| 352 assertSpan(elem.sourceSpan, 0, 6, '<html>'); |
| 353 assertSpan(elem.endSourceSpan, 22, 29, '</html>'); |
| 354 } |
| 355 { |
| 356 var elem = doc.querySelector('body'); |
| 357 assertSpan(elem.sourceSpan, 6, 12, '<body>'); |
| 358 assertSpan(elem.endSourceSpan, 15, 22, '</body>'); |
| 359 } |
| 360 }); |
| 361 |
| 362 test('normal', () { |
| 363 var text = '<div><element><span></span></element></div>'; |
| 364 var doc = parse(text, generateSpans: true); |
| 365 var elem = doc.querySelector('element'); |
| 366 assertSpan(elem.sourceSpan, 5, 14, '<element>'); |
| 367 assertSpan(elem.endSourceSpan, 27, 37, '</element>'); |
| 368 }); |
| 369 |
| 370 test('block', () { |
| 371 var text = '<div>123</div>'; |
| 372 var doc = parse(text, generateSpans: true); |
| 373 var elem = doc.querySelector('div'); |
| 374 assertSpan(elem.sourceSpan, 0, 5, '<div>'); |
| 375 assertSpan(elem.endSourceSpan, 8, 14, '</div>'); |
| 376 }); |
| 377 |
| 378 test('form', () { |
| 379 var text = '<form>123</form>'; |
| 380 var doc = parse(text, generateSpans: true); |
| 381 var elem = doc.querySelector('form'); |
| 382 assertSpan(elem.sourceSpan, 0, 6, '<form>'); |
| 383 assertSpan(elem.endSourceSpan, 9, 16, '</form>'); |
| 384 }); |
| 385 |
| 386 test('p explicit end', () { |
| 387 var text = '<p>123</p>'; |
| 388 var doc = parse(text, generateSpans: true); |
| 389 var elem = doc.querySelector('p'); |
| 390 assertSpan(elem.sourceSpan, 0, 3, '<p>'); |
| 391 assertSpan(elem.endSourceSpan, 6, 10, '</p>'); |
| 392 }); |
| 393 |
| 394 test('p implicit end', () { |
| 395 var text = '<div><p>123<p>456</div>'; |
| 396 var doc = parse(text, generateSpans: true); |
| 397 var elem = doc.querySelector('p'); |
| 398 assertSpan(elem.sourceSpan, 5, 8, '<p>'); |
| 399 expect(elem.endSourceSpan, isNull); |
| 400 }); |
| 401 |
| 402 test('li', () { |
| 403 var text = '<li>123</li>'; |
| 404 var doc = parse(text, generateSpans: true); |
| 405 var elem = doc.querySelector('li'); |
| 406 assertSpan(elem.sourceSpan, 0, 4, '<li>'); |
| 407 assertSpan(elem.endSourceSpan, 7, 12, '</li>'); |
| 408 }); |
| 409 |
| 410 test('heading', () { |
| 411 var text = '<h1>123</h1>'; |
| 412 var doc = parse(text, generateSpans: true); |
| 413 var elem = doc.querySelector('h1'); |
| 414 assertSpan(elem.sourceSpan, 0, 4, '<h1>'); |
| 415 assertSpan(elem.endSourceSpan, 7, 12, '</h1>'); |
| 416 }); |
| 417 |
| 418 test('formatting', () { |
| 419 var text = '<b>123</b>'; |
| 420 var doc = parse(text, generateSpans: true); |
| 421 var elem = doc.querySelector('b'); |
| 422 assertSpan(elem.sourceSpan, 0, 3, '<b>'); |
| 423 assertSpan(elem.endSourceSpan, 6, 10, '</b>'); |
| 424 }); |
| 425 |
| 426 test('table tbody', () { |
| 427 var text = '<table><tbody> </tbody></table>'; |
| 428 var doc = parse(text, generateSpans: true); |
| 429 { |
| 430 var elem = doc.querySelector('tbody'); |
| 431 assertSpan(elem.sourceSpan, 7, 14, '<tbody>'); |
| 432 assertSpan(elem.endSourceSpan, 16, 24, '</tbody>'); |
| 433 } |
| 434 }); |
| 435 |
| 436 test('table tr td', () { |
| 437 var text = '<table><tr><td>123</td></tr></table>'; |
| 438 var doc = parse(text, generateSpans: true); |
| 439 { |
| 440 var elem = doc.querySelector('table'); |
| 441 assertSpan(elem.sourceSpan, 0, 7, '<table>'); |
| 442 assertSpan(elem.endSourceSpan, 28, 36, '</table>'); |
| 443 } |
| 444 { |
| 445 var elem = doc.querySelector('tr'); |
| 446 assertSpan(elem.sourceSpan, 7, 11, '<tr>'); |
| 447 assertSpan(elem.endSourceSpan, 23, 28, '</tr>'); |
| 448 } |
| 449 { |
| 450 var elem = doc.querySelector('td'); |
| 451 assertSpan(elem.sourceSpan, 11, 15, '<td>'); |
| 452 assertSpan(elem.endSourceSpan, 18, 23, '</td>'); |
| 453 } |
| 454 }); |
| 455 |
| 456 test('select optgroup option', () { |
| 457 var text = '<select><optgroup><option>123</option></optgroup></select>'; |
| 458 var doc = parse(text, generateSpans: true); |
| 459 { |
| 460 var elem = doc.querySelector('select'); |
| 461 assertSpan(elem.sourceSpan, 0, 8, '<select>'); |
| 462 assertSpan(elem.endSourceSpan, 49, 58, '</select>'); |
| 463 } |
| 464 { |
| 465 var elem = doc.querySelector('optgroup'); |
| 466 assertSpan(elem.sourceSpan, 8, 18, '<optgroup>'); |
| 467 assertSpan(elem.endSourceSpan, 38, 49, '</optgroup>'); |
| 468 } |
| 469 { |
| 470 var elem = doc.querySelector('option'); |
| 471 assertSpan(elem.sourceSpan, 18, 26, '<option>'); |
| 472 assertSpan(elem.endSourceSpan, 29, 38, '</option>'); |
| 473 } |
| 474 }); |
| 475 }); |
| 476 } |
OLD | NEW |