foo
319 |
320 | ==>
321 |
322 | Document(Element(OpenTag(StartTag,TagName,EndTag),Text,CloseTag(StartCloseTag,TagName,EndTag)))
323 |
324 | # Can handle lone close tags {"dialect": "noMatch"}
325 |
326 |
327 |
328 | ==>
329 |
330 | Document(CloseTag(StartCloseTag,TagName,EndTag))
331 |
332 | # Parses ampersands in attributes
333 |
334 |
335 |
336 | ==>
337 |
338 | Document(Element(SelfClosingTag(StartTag, TagName, Attribute(AttributeName, Is, AttributeValue(InvalidEntity)), EndTag)))
339 |
340 | # Supports self-closing dialect {"dialect": "selfClosing"}
341 |
342 |
343 |
344 | ==>
345 |
346 | Document(Element(
347 | OpenTag(StartTag,TagName,EndTag),
348 | Element(SelfClosingTag(StartTag,TagName,Attribute(AttributeName,Is,UnquotedAttributeValue),SelfClosingEndTag)),
349 | CloseTag(StartCloseTag,TagName,EndTag)))
350 |
351 | # Allows self-closing in foreign elements
352 |
353 |
354 |
355 | ==>
356 |
357 | Document(Element(OpenTag(StartTag,TagName,EndTag),
358 | Element(OpenTag(StartTag,TagName,EndTag),
359 | Element(SelfClosingTag(StartTag,TagName,SelfClosingEndTag)),
360 | CloseTag(StartCloseTag,TagName,EndTag)),
361 | CloseTag(StartCloseTag,TagName,EndTag)))
362 |
363 | # Parses multiple unfinished tags in a row
364 |
365 |
370 |
371 | Document(Element(OpenTag(StartTag,TagName,⚠),
372 | Element(OpenTag(StartTag,TagName,⚠),
373 | Element(OpenTag(StartTag,TagName,⚠),⚠),⚠),⚠))
374 |
375 | # Allows self-closing on special tags {"dialect": "selfClosing"}
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 | ==>
385 |
386 | Document(Element(
387 | OpenTag(StartTag,TagName,EndTag),
388 | Text,
389 | Element(SelfClosingTag(StartTag,TagName,SelfClosingEndTag)),
390 | Text,
391 | Element(SelfClosingTag(StartTag,TagName,SelfClosingEndTag)),
392 | Text,
393 | Element(SelfClosingTag(StartTag,TagName,SelfClosingEndTag)),
394 | Text,
395 | Element(SelfClosingTag(StartTag,TagName,SelfClosingEndTag)),
396 | Text,
397 | CloseTag(StartCloseTag,TagName,EndTag)))
398 |
--------------------------------------------------------------------------------
/test/test-html.js:
--------------------------------------------------------------------------------
1 | import {parser, configureNesting} from "../dist/index.js"
2 | import {parser as jsParser} from "@lezer/javascript"
3 | import {fileTests} from "@lezer/generator/dist/test"
4 |
5 | import * as fs from "fs"
6 | import * as path from "path"
7 | import {fileURLToPath} from "url"
8 | let caseDir = path.dirname(fileURLToPath(import.meta.url))
9 |
10 | let mixed = parser.configure({
11 | wrap: configureNesting([{
12 | tag: "script",
13 | attrs(attrs) {
14 | return !attrs.type || /^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^module$|^$/i.test(attrs.type)
15 | },
16 | parser: jsParser
17 | }])
18 | })
19 |
20 | for (let file of fs.readdirSync(caseDir)) {
21 | if (!/\.txt$/.test(file)) continue
22 | let name = /^[^\.]*/.exec(file)[0]
23 | describe(name, () => {
24 | let p = name == "mixed" ? mixed : parser
25 | for (let {name, run} of fileTests(fs.readFileSync(path.join(caseDir, file), "utf8"), file))
26 | it(name, () => run(p))
27 | })
28 | }
29 |
30 |
--------------------------------------------------------------------------------
/test/test-incremental.js:
--------------------------------------------------------------------------------
1 | import {parser as baseParser} from "../dist/index.js"
2 | import {TreeFragment} from "@lezer/common"
3 |
4 | let parser = baseParser.configure({bufferLength: 2})
5 |
6 | let r = n => Math.floor(Math.random() * n)
7 |
8 | let tags = ["p", "ul", "li", "div", "span", "th", "tr", "body", "head", "title", "dd", "code", "em", "strong"]
9 |
10 | function randomDoc(size) {
11 | let doc = ""
12 | if (!r(5)) doc += ""
13 | let scope = []
14 | for (let i = 0; i < size; i++) {
15 | let sel = r(20)
16 | if (sel < 5) {
17 | let tag = tags[r(tags.length)]
18 | doc += `<${tag}${r(2) ? " a=b" : ""}>`
19 | scope.push(tag)
20 | } else if (sel < 10 && scope.length) {
21 | let name = scope.pop()
22 | doc += `${r(5) ? name : "div"}>`
23 | } else if (sel == 10) {
24 | doc += `
![]()
`
25 | } else if (sel == 11) {
26 | doc += ""
27 | } else if (sel == 12) {
28 | doc += r(2) ? "&" : ""
29 | } else {
30 | for (let i = r(6) + 1; i >= 0; i--)
31 | doc += String.fromCharCode(97 + r(26))
32 | }
33 | }
34 | while (scope.length) {
35 | let name = scope.pop()
36 | if (r(5)) doc += `${name}>`
37 | }
38 | return doc
39 | }
40 |
41 | function check(doc, [tp, pos, txt], prevAST) {
42 | let change = {fromA: pos, toA: pos, fromB: pos, toB: pos}, newDoc
43 | if (tp == "insert") {
44 | newDoc = doc.slice(0, pos) + txt + doc.slice(pos)
45 | change.toA += txt.length
46 | } else if (tp == "del") {
47 | newDoc = doc.slice(0, pos) + doc.slice(pos + 1)
48 | change.toB++
49 | } else {
50 | newDoc = doc.slice(0, pos) + txt + doc.slice(pos + 1)
51 | change.toA += txt.length
52 | change.toB++
53 | }
54 | let fragments = TreeFragment.applyChanges(TreeFragment.addTree(prevAST || parser.parse(doc)), [change], 2)
55 | let ast = parser.parse(newDoc, fragments)
56 | let orig = parser.parse(newDoc)
57 | if (ast.toString() != orig.toString()) {
58 | throw new Error(`Mismatch:\n ${ast}\nvs\n ${orig}\ndocument: ${
59 | JSON.stringify(doc)}\naction: ${JSON.stringify([tp, pos, ch])}`)
60 | }
61 | return [newDoc, ast]
62 | }
63 |
64 | // Call this to just run random tests until a failing one is found.
65 | // Not directly called in the tests because there's a bunch of
66 | // circumstances in which uninteresting deviations in error recovery
67 | // will create differing parses, so results have to be manually
68 | // inspected.
69 | function generate() {
70 | for (let count = 0, size = 2;; size = Math.min(40, size + 1)) {
71 | let doc = randomDoc(size), prev = null
72 | for (let i = 0; i < 2; i++) {
73 | console.log("Attempt", ++count)
74 | let action = [["del", "insert", "replace"][r(3)], r(doc.length - 1), "<>/piabc "[r(9)]]
75 | ;([doc, prev] = check(doc, action, prev))
76 | }
77 | }
78 | }
79 |
80 | describe("Incremental parsing", () => {
81 | it("doesn't get confused by reused opening tags", () => {
82 | check("
mgnbni
", ["del", 29])
83 | })
84 |
85 | it("can handle a renamed opening tag after a self-closing", () => {
86 | check("
one two three four five six seven
eight", ["replace", 37, "a"])
87 | })
88 |
89 | it("is okay with nameless elements", () => {
90 | check("
![]()
<>body>", ["replace", 14, ">"])
91 | check("abcde<>fghij<", ["replace", 12, ">"])
92 | })
93 |
94 | it("doesn't get confused by an invalid close tag receiving a matching open tag", () => {
95 | check("
foo", ["insert", 0, "
"])
96 | })
97 | })
98 |
--------------------------------------------------------------------------------
/test/vue.txt:
--------------------------------------------------------------------------------
1 | # Parses Vue builtin directives
2 |
3 |
4 |
5 | ==>
6 |
7 | Document(
8 | Element(
9 | OpenTag(StartTag, TagName, Attribute(AttributeName, Is, AttributeValue), EndTag),
10 | CloseTag(StartCloseTag, TagName, EndTag)))
11 |
12 | # Parses Vue :is shorthand syntax
13 |
14 |
15 |
16 | ==>
17 |
18 | Document(
19 | Element(
20 | OpenTag(StartTag, TagName, Attribute(AttributeName, Is, AttributeValue),EndTag),
21 | CloseTag(StartCloseTag, TagName, EndTag)))
22 |
23 | # Parses Vue @click shorthand syntax
24 |
25 |
26 |
27 | ==>
28 |
29 | Document(
30 | Element(
31 | OpenTag(StartTag, TagName, Attribute(AttributeName, Is, AttributeValue), EndTag),
32 | Text,
33 | CloseTag(StartCloseTag, TagName, EndTag)))
34 |
35 | # Parses Vue @submit.prevent shorthand syntax
36 |
37 |
38 |
39 | ==>
40 |
41 | Document(
42 | Element(
43 | OpenTag(StartTag, TagName, Attribute(AttributeName, Is, AttributeValue), EndTag),
44 | CloseTag(StartCloseTag, TagName, EndTag)))
45 |
46 | # Parses Vue Dynamic Arguments
47 |
48 |
Link
49 |
50 | ==>
51 |
52 | Document(
53 | Element(
54 | OpenTag(StartTag, TagName, Attribute(AttributeName, Is, AttributeValue), EndTag),
55 | Text,
56 | CloseTag(StartCloseTag, TagName, EndTag)))
57 |
--------------------------------------------------------------------------------