Diffdown is a real-time collaborative Markdown editor/previewer built on the AT Protocol diffdown.com
at 9a6ba7fadf312bda2dc714899aa76bcb8f00d5aa 97 lines 3.2 kB view raw
1import {parser as baseParser} from "../dist/index.js" 2import {TreeFragment} from "@lezer/common" 3 4let parser = baseParser.configure({bufferLength: 2}) 5 6let r = n => Math.floor(Math.random() * n) 7 8let tags = ["p", "ul", "li", "div", "span", "th", "tr", "body", "head", "title", "dd", "code", "em", "strong"] 9 10function randomDoc(size) { 11 let doc = "" 12 if (!r(5)) doc += "<!doctype html>" 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 += `<img>` 25 } else if (sel == 11) { 26 doc += "<script>a()</script>" 27 } else if (sel == 12) { 28 doc += r(2) ? "&amp;" : "<!--@-->" 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 41function 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. 69function 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 80describe("Incremental parsing", () => { 81 it("doesn't get confused by reused opening tags", () => { 82 check("<code><code>mgnbni</code></code>", ["del", 29]) 83 }) 84 85 it("can handle a renamed opening tag after a self-closing", () => { 86 check("<p>one two three four five six seven<p>eight", ["replace", 37, "a"]) 87 }) 88 89 it("is okay with nameless elements", () => { 90 check("<body><code><img></code><>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("<div><p>foo</body>", ["insert", 0, "<body>"]) 96 }) 97})