Premier commit déjà bien avancé

This commit is contained in:
2025-11-10 18:33:24 +01:00
commit db4f0508cb
652 changed files with 440521 additions and 0 deletions

View File

@ -0,0 +1,14 @@
import {Tree} from "@lezer/common"
export function compareTree(a: Tree, b: Tree) {
let curA = a.cursor(), curB = b.cursor()
for (;;) {
let mismatch = null, next = false
if (curA.type != curB.type) mismatch = `Node type mismatch (${curA.name} vs ${curB.name})`
else if (curA.from != curB.from) mismatch = `Start pos mismatch for ${curA.name}: ${curA.from} vs ${curB.from}`
else if (curA.to != curB.to) mismatch = `End pos mismatch for ${curA.name}: ${curA.to} vs ${curB.to}`
else if ((next = curA.next()) != curB.next()) mismatch = `Tree size mismatch`
if (mismatch) throw new Error(`${mismatch}\n ${a}\n ${b}`)
if (!next) break
}
}

79
frontend/node_modules/@lezer/markdown/test/spec.ts generated vendored Normal file
View File

@ -0,0 +1,79 @@
import {Tree} from "@lezer/common"
import {MarkdownParser} from ".."
const abbrev: {[abbr: string]: string} = {
__proto__: null as any,
CB: "CodeBlock",
FC: "FencedCode",
Q: "Blockquote",
HR: "HorizontalRule",
BL: "BulletList",
OL: "OrderedList",
LI: "ListItem",
H1: "ATXHeading1",
H2: "ATXHeading2",
H3: "ATXHeading3",
H4: "ATXHeading4",
H5: "ATXHeading5",
H6: "ATXHeading6",
SH1: "SetextHeading1",
SH2: "SetextHeading2",
HB: "HTMLBlock",
PI: "ProcessingInstructionBlock",
CMB: "CommentBlock",
LR: "LinkReference",
P: "Paragraph",
Esc: "Escape",
Ent: "Entity",
BR: "HardBreak",
Em: "Emphasis",
St: "StrongEmphasis",
Ln: "Link",
Al: "Autolink",
Im: "Image",
C: "InlineCode",
HT: "HTMLTag",
CM: "Comment",
Pi: "ProcessingInstruction",
h: "HeaderMark",
q: "QuoteMark",
l: "ListMark",
L: "LinkMark",
e: "EmphasisMark",
c: "CodeMark",
cI: "CodeInfo",
cT: "CodeText",
LT: "LinkTitle",
LL: "LinkLabel"
}
export class SpecParser {
constructor(readonly parser: MarkdownParser, readonly localAbbrev?: {[name: string]: string}) {}
type(name: string) {
name = (this.localAbbrev && this.localAbbrev[name]) || abbrev[name] || name
return this.parser.nodeSet.types.find(t => t.name == name)?.id
}
parse(spec: string, specName: string) {
let doc = "", buffer = [], stack: number[] = []
for (let pos = 0; pos < spec.length; pos++) {
let ch = spec[pos]
if (ch == "{") {
let name = /^(\w+):/.exec(spec.slice(pos + 1)), tag = name && this.type(name[1])
if (tag == null) throw new Error(`Invalid node opening mark at ${pos} in ${specName}`)
pos += name![0].length
stack.push(tag, doc.length, buffer.length)
} else if (ch == "}") {
if (!stack.length) throw new Error(`Mismatched node close mark at ${pos} in ${specName}`)
let bufStart = stack.pop()!, from = stack.pop()!, type = stack.pop()!
buffer.push(type, from, doc.length, 4 + buffer.length - bufStart)
} else {
doc += ch
}
}
if (stack.length) throw new Error(`Unclosed node in ${specName}`)
return {tree: Tree.build({buffer, nodeSet: this.parser.nodeSet, topID: this.type("Document")!, length: doc.length}), doc}
}
}

View File

@ -0,0 +1,269 @@
import {parser as cmParser, GFM, Subscript, Superscript, Emoji} from "../dist/index.js"
import {compareTree} from "./compare-tree.js"
import {SpecParser} from "./spec.js"
const parser = cmParser.configure([GFM, Subscript, Superscript, Emoji])
const specParser = new SpecParser(parser, {
__proto__: null as any,
Th: "Strikethrough",
tm: "StrikethroughMark",
TB: "Table",
TH: "TableHeader",
TR: "TableRow",
TC: "TableCell",
tb: "TableDelimiter",
T: "Task",
t: "TaskMarker",
Sub: "Subscript",
sub: "SubscriptMark",
Sup: "Superscript",
sup: "SuperscriptMark",
ji: "Emoji"
})
function test(name: string, spec: string, p = parser) {
it(name, () => {
let {tree, doc} = specParser.parse(spec, name)
compareTree(p.parse(doc), tree)
})
}
describe("Extension", () => {
test("Tables (example 198)", `
{TB:{TH:{tb:|} {TC:foo} {tb:|} {TC:bar} {tb:|}}
{tb:| --- | --- |}
{TR:{tb:|} {TC:baz} {tb:|} {TC:bim} {tb:|}}}`)
test("Tables (example 199)", `
{TB:{TH:{tb:|} {TC:abc} {tb:|} {TC:defghi} {tb:|}}
{tb::-: | -----------:}
{TR:{TC:bar} {tb:|} {TC:baz}}}`)
test("Tables (example 200)", `
{TB:{TH:{tb:|} {TC:f{Esc:\\|}oo} {tb:|}}
{tb:| ------ |}
{TR:{tb:|} {TC:b {C:{c:\`}\\|{c:\`}} az} {tb:|}}
{TR:{tb:|} {TC:b {St:{e:**}{Esc:\\|}{e:**}} im} {tb:|}}}`)
test("Tables (example 201)", `
{TB:{TH:{tb:|} {TC:abc} {tb:|} {TC:def} {tb:|}}
{tb:| --- | --- |}
{TR:{tb:|} {TC:bar} {tb:|} {TC:baz} {tb:|}}}
{Q:{q:>} {P:bar}}`)
test("Tables (example 202)", `
{TB:{TH:{tb:|} {TC:abc} {tb:|} {TC:def} {tb:|}}
{tb:| --- | --- |}
{TR:{tb:|} {TC:bar} {tb:|} {TC:baz} {tb:|}}
{TR:{TC:bar}}}
{P:bar}`)
test("Tables (example 203)", `
{P:| abc | def |
| --- |
| bar |}`)
test("Tables (example 204)", `
{TB:{TH:{tb:|} {TC:abc} {tb:|} {TC:def} {tb:|}}
{tb:| --- | --- |}
{TR:{tb:|} {TC:bar} {tb:|}}
{TR:{tb:|} {TC:bar} {tb:|} {TC:baz} {tb:|} {TC:boo} {tb:|}}}`)
test("Tables (example 205)", `
{TB:{TH:{tb:|} {TC:abc} {tb:|} {TC:def} {tb:|}}
{tb:| --- | --- |}}`)
test("Tables (in blockquote)", `
{Q:{q:>} {TB:{TH:{tb:|} {TC:one} {tb:|} {TC:two} {tb:|}}
{q:>} {tb:| --- | --- |}
{q:>} {TR:{tb:|} {TC:123} {tb:|} {TC:456} {tb:|}}}
{q:>}
{q:>} {P:Okay}}`)
test("Tables (empty header)", `
{TB:{TH:{tb:|} {tb:|} {tb:|}}
{tb:| :-: | :-: |}
{TR:{tb:|} {TC:One} {tb:|} {TC:Two} {tb:|}}}`)
test("Tables (end paragraph)", `
{P:Hello}
{TB:{TH:{tb:|} {TC:foo} {tb:|} {TC:bar} {tb:|}}
{tb:| --- | --- |}
{TR:{tb:|} {TC:baz} {tb:|} {TC:bim} {tb:|}}}`)
test("Tables (invalid tables don't end paragraph)", `
{P:Hello
| abc | def |
| --- |
| bar |}`)
test("Task list (example 279)", `
{BL:{LI:{l:-} {T:{t:[ ]} foo}}
{LI:{l:-} {T:{t:[x]} bar}}}`)
test("Task list (example 280)", `
{BL:{LI:{l:-} {T:{t:[x]} foo}
{BL:{LI:{l:-} {T:{t:[ ]} bar}}
{LI:{l:-} {T:{t:[x]} baz}}}}
{LI:{l:-} {T:{t:[ ]} bim}}}`)
test("Autolink (example 622)", `
{P:{URL:www.commonmark.org}}`)
test("Autolink (example 623)", `
{P:Visit {URL:www.commonmark.org/help} for more information.}`)
test("Autolink (example 624)", `
{P:Visit {URL:www.commonmark.org}.}
{P:Visit {URL:www.commonmark.org/a.b}.}`)
test("Autolink (example 625)", `
{P:{URL:www.google.com/search?q=Markup+(business)}}
{P:{URL:www.google.com/search?q=Markup+(business)}))}
{P:({URL:www.google.com/search?q=Markup+(business)})}
{P:({URL:www.google.com/search?q=Markup+(business)}}`)
test("Autolink (example 626)", `
{P:{URL:www.google.com/search?q=(business))+ok}}`)
test("Autolink (example 627)", `
{P:{URL:www.google.com/search?q=commonmark&hl=en}}
{P:{URL:www.google.com/search?q=commonmark}{Entity:&hl;}}`)
test("Autolink (example 628)", `
{P:{URL:www.commonmark.org/he}<lp}`)
test("Autolink (example 629)", `
{P:{URL:http://commonmark.org}}
{P:(Visit {URL:https://encrypted.google.com/search?q=Markup+(business)})}`)
test("Autolink (example 630)", `
{P:{URL:foo@bar.baz}}`)
test("Autolink (example 631)", `
{P:hello@mail+xyz.example isn't valid, but {URL:hello+xyz@mail.example} is.}`)
test("Autolink (example 632)", `
{P:{URL:a.b-c_d@a.b}}
{P:{URL:a.b-c_d@a.b}.}
{P:a.b-c_d@a.b-}
{P:a.b-c_d@a.b_}`)
test("Autolink (example 633)", `
{P:{URL:mailto:foo@bar.baz}}
{P:{URL:mailto:a.b-c_d@a.b}}
{P:{URL:mailto:a.b-c_d@a.b}.}
{P:{URL:mailto:a.b-c_d@a.b}/}
{P:mailto:a.b-c_d@a.b-}
{P:mailto:a.b-c_d@a.b_}
{P:{URL:xmpp:foo@bar.baz}}
{P:{URL:xmpp:foo@bar.baz}.}`)
test("Autolink (example 634)", `
{P:{URL:xmpp:foo@bar.baz/txt}}
{P:{URL:xmpp:foo@bar.baz/txt@bin}}
{P:{URL:xmpp:foo@bar.baz/txt@bin.com}}`)
test("Autolink (example 635)", `
{P:{URL:xmpp:foo@bar.baz/txt}/bin}`)
test("Task list (in ordered list)", `
{OL:{LI:{l:1.} {T:{t:[X]} Okay}}}`)
test("Task list (versus table)", `
{BL:{LI:{l:-} {TB:{TH:{TC:[ ] foo} {tb:|} {TC:bar}}
{tb:--- | ---}}}}`)
test("Task list (versus setext header)", `
{OL:{LI:{l:1.} {SH1:{Ln:{L:[}X{L:]}} foo
{h:===}}}}`)
test("Strikethrough (example 491)", `
{P:{Th:{tm:~~}Hi{tm:~~}} Hello, world!}`)
test("Strikethrough (example 492)", `
{P:This ~~has a}
{P:new paragraph~~.}`)
test("Strikethrough (nested)", `
{P:Nesting {St:{e:**}with {Th:{tm:~~}emphasis{tm:~~}}{e:**}}.}`)
test("Strikethrough (overlapping)", `
{P:One {St:{e:**}two ~~three{e:**}} four~~}
{P:One {Th:{tm:~~}two **three{tm:~~}} four**}`)
test("Strikethrough (escaped)", `
{P:A {Esc:\\~}~b c~~}`)
test("Strikethrough around spaces", `
{P:One ~~ two~~ three {Th:{tm:~~}.foo.{tm:~~}} a~~.foo.~~a {Th:{tm:~~}blah{tm:~~}}.}`)
test("Subscript", `
{P:One {Sub:{sub:~}two{sub:~}} {Em:{e:*}one {Sub:{sub:~}two{sub:~}}{e:*}}}`)
test("Subscript (no spaces)", `
{P:One ~two three~}`)
test("Subscript (escapes)", `
{P:One {Sub:{sub:~}two{Esc:\\ }th{Esc:\\~}ree{sub:~}}}`)
test("Superscript", `
{P:One {Sup:{sup:^}two{sup:^}} {Em:{e:*}one {Sup:{sup:^}two{sup:^}}{e:*}}}`)
test("Superscript (no spaces)", `
{P:One ^two three^}`)
test("Superscript (escapes)", `
{P:One {Sup:{sup:^}two{Esc:\\ }th{Esc:\\^}ree{sup:^}}}`)
test("Emoji", `
{P:Hello {ji::smile:} {ji::100:}}`)
test("Emoji (format)", `
{P:Hello :smi le: :1.00: ::}`)
test("Disable syntax", `
{BL:{LI:{l:-} {P:List still {Em:{e:*}works{e:*}}}}}
{P:> No quote, no ^sup^}
{P:No setext either
===}`, parser.configure({remove: ["Superscript", "Blockquote", "SetextHeading"]}))
test("Autolink (.co.uk)", `
{P:{URL:www.blah.co.uk/path}}`)
test("Autolink (email .co.uk)", `
{P:{URL:foo@bar.co.uk}}`)
test("Autolink (http://www.foo-bar.com/)", `
{P:{URL:http://www.foo-bar.com/}}`)
test("Autolink (exclude underscores)", `
{P:http://www.foo_/ http://foo_.com}`)
test("Autolink (in image)", `
{P:{Im:{L:![}Link: {URL:http://foo.com/}{L:]}{L:(}{URL:x.jpg}{L:)}}}`)
})

View File

@ -0,0 +1,259 @@
import {Tree, TreeFragment} from "@lezer/common"
import ist from "ist"
import {parser} from "../dist/index.js"
import {compareTree} from "./compare-tree.js"
let doc1 = `
Header
---
One **two**
three *four*
five.
> Start of quote
>
> 1. Nested list
>
> 2. More content
> inside the [list][link]
>
> Continued item
>
> ~~~
> Block of code
> ~~~
>
> 3. And so on
[link]: /ref
[another]: /one
And a final paragraph.
***
The end.
`
type ChangeSpec = {from: number, to?: number, insert?: string}[]
class State {
constructor(readonly doc: string,
readonly tree: Tree,
readonly fragments: readonly TreeFragment[]) {}
static start(doc: string) {
let tree = parser.parse(doc)
return new State(doc, tree, TreeFragment.addTree(tree))
}
update(changes: ChangeSpec, reparse = true) {
let changed = [], doc = this.doc, off = 0
for (let {from, to = from, insert = ""} of changes) {
doc = doc.slice(0, from) + insert + doc.slice(to)
changed.push({fromA: from - off, toA: to - off, fromB: from, toB: from + insert.length})
off += insert.length - (to - from)
}
let fragments = TreeFragment.applyChanges(this.fragments, changed, 2)
if (!reparse) return new State(doc, Tree.empty, fragments)
let tree = parser.parse(doc, fragments)
return new State(doc, tree, TreeFragment.addTree(tree, fragments))
}
}
let _state1: State | null = null, state1 = () => _state1 || (_state1 = State.start(doc1))
function overlap(a: Tree, b: Tree) {
let inA = new Set<Tree>(), shared = 0, sharingTo = 0
for (let cur = a.cursor(); cur.next();) if (cur.tree) inA.add(cur.tree)
for (let cur = b.cursor(); cur.next();) if (cur.tree && inA.has(cur.tree) && cur.type.is("Block") && cur.from >= sharingTo) {
shared += cur.to - cur.from
sharingTo = cur.to
}
return Math.round(shared * 100 / b.length)
}
function testChange(change: ChangeSpec, reuse = 10) {
let state = state1().update(change)
compareTree(state.tree, parser.parse(state.doc))
if (reuse) ist(overlap(state.tree, state1().tree), reuse, ">")
}
describe("Markdown incremental parsing", () => {
it("can produce the proper tree", () => {
// Replace 'three' with 'bears'
let state = state1().update([{from: 24, to: 29, insert: "bears"}])
compareTree(state.tree, state1().tree)
})
it("reuses nodes from the previous parse", () => {
// Replace 'three' with 'bears'
let state = state1().update([{from: 24, to: 29, insert: "bears"}])
ist(overlap(state1().tree, state.tree), 80, ">")
})
it("can reuse content for a change in a block context", () => {
// Replace 'content' with 'monkeys'
let state = state1().update([{from: 92, to: 99, insert: "monkeys"}])
compareTree(state.tree, state1().tree)
ist(overlap(state1().tree, state.tree), 20, ">")
})
it("can handle deleting a quote mark", () => testChange([{from: 82, to: 83}]))
it("can handle adding to a quoted block", () => testChange([{from: 37, insert: "> "}, {from: 45, insert: "> "}]))
it("can handle a change in a post-linkref paragraph", () => testChange([{from: 249, to: 251}]))
it("can handle a change in a paragraph-adjacent linkrefs", () => testChange([{from: 230, to: 231}]))
it("can deal with multiple changes applied separately", () => {
let state = state1().update([{from: 190, to: 191}], false).update([{from: 30, insert: "hi\n\nyou"}])
compareTree(state.tree, parser.parse(state.doc))
ist(overlap(state.tree, state1().tree), 20, ">")
})
it("works when a change happens directly after a block", () => testChange([{from: 150, to: 167}]))
it("works when a change deletes a blank line after a paragraph", () => testChange([{from: 207, to: 213}]))
it("doesn't get confused by removing paragraph-breaking markup", () => testChange([{from: 264, to: 265}]))
function r(n: number) { return Math.floor(Math.random() * n) }
function rStr(len: number) {
let result = "", chars = "\n>x-"
while (result.length < len) result += chars[r(chars.length)]
return result
}
it("survives random changes", () => {
for (let i = 0, l = doc1.length; i < 20; i++) {
let c = 1 + r(4), changes = []
for (let i = 0, rFrom = 0; i < c; i++) {
let rTo = rFrom + Math.floor((l - rFrom) / (c - i))
let from = rFrom + r(rTo - rFrom - 1), to = r(2) == 1 ? from : from + r(Math.min(rTo - from, 20))
let iR = r(3), insert = iR == 0 && from != to ? "" : iR == 1 ? "\n\n" : rStr(r(5) + 1)
changes.push({from, to, insert})
l += insert.length - (to - from)
rFrom = to + insert.length
}
testChange(changes, 0)
}
})
it("can handle large documents", () => {
let doc = doc1.repeat(50)
let state = State.start(doc)
let newState = state.update([{from: doc.length >> 1, insert: "a\n\nb"}])
ist(overlap(state.tree, newState.tree), 90, ">")
})
it("properly re-parses a continued indented code block", () => {
let state = State.start(`
One paragraph to create a bit of string length here
Code
Block
Another paragraph that is long enough to create a fragment
`).update([{from: 76, insert: " "}])
compareTree(state.tree, parser.parse(state.doc))
})
it("properly re-parses a continued list", () => {
let state = State.start(`
One paragraph to create a bit of string length here
* List
More content
Another paragraph that is long enough to create a fragment
`).update([{from: 65, insert: " * "}])
compareTree(state.tree, parser.parse(state.doc))
})
it("can recover from incremental parses that stop in the middle of a list", () => {
let doc = `
1. I am a list item with ***some* emphasized
content inside** and the parser hopefully stops
parsing after me.
2. Oh no the list continues.
`
let parse = parser.startParse(doc), tree
parse.advance()
ist(parse.parsedPos, doc.length, "<")
parse.stopAt(parse.parsedPos)
while (!(tree = parse.advance())) {}
let state = new State(doc, tree, TreeFragment.addTree(tree)).update([])
ist(state.tree.topNode.lastChild!.from, 1)
})
it("can reuse list items", () => {
let start = State.start(" - List item\n".repeat(100))
let state = start.update([{from: 18, to: 19}])
ist(overlap(start.tree, state.tree), 80, ">")
})
it("returns a tree starting at the first range", () => {
let result = parser.parse("foo\n\nbar", [], [{from: 5, to: 8}])
ist(result.toString(), "Document(Paragraph)")
ist(result.length, 3)
ist(result.positions[0], 0)
})
it("Allows gaps in the input", () => {
let doc = `
The first X *y* X<
>X paragraph.
- And *a X<*>X list*
`
let tree = parser.parse(doc, [], [{from: 0, to: 11}, {from: 12, to: 17}, {from: 23, to: 46}, {from: 51, to: 58}])
ist(tree.toString(),
"Document(Paragraph(Emphasis(EmphasisMark,EmphasisMark)),BulletList(ListItem(ListMark,Paragraph(Emphasis(EmphasisMark,EmphasisMark)))))")
ist(tree.length, doc.length)
let top = tree.topNode, first = top.firstChild!
ist(first.name, "Paragraph")
ist(first.from, 1)
ist(first.to, 34)
let last = top.lastChild!.lastChild!.lastChild!, em = last.lastChild!
ist(last.name, "Paragraph")
ist(last.from, 39)
ist(last.to, 57)
ist(em.name, "Emphasis")
ist(em.from, 43)
ist(em.to, 57)
})
it("can reuse nodes at the end of the document", () => {
let doc = `* List item
~~~js
function foo() {
return false
}
~~~
`
let tree = parser.parse(doc)
let ins = 11
let doc2 = doc.slice(0, ins) + "\n* " + doc.slice(ins)
let fragments = TreeFragment.applyChanges(TreeFragment.addTree(tree), [{fromA: ins, toA: ins, fromB: ins, toB: ins + 3}])
let tree2 = parser.parse(doc2, fragments)
ist(tree2.topNode.lastChild!.tree, tree.topNode.lastChild!.tree)
})
it("places reused nodes at the right position when there are gaps before them", () => {
let doc = " {{}}\nb\n{{}}"
let ast1 = parser.parse(doc, undefined, [{from: 0, to: 1}, {from: 5, to: 8}])
let frag = TreeFragment.applyChanges(TreeFragment.addTree(ast1), [{fromA: 0, toA: 0, fromB: 0, toB: 1}])
let ast2 = parser.parse(" " + doc, frag, [{from: 0, to: 2}, {from: 6, to: 9}])
ist(ast2.toString(), "Document(Paragraph)")
let p = ast2.topNode.firstChild!
ist(p.from, 7)
ist(p.to, 8)
})
})

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,86 @@
import {Tree, NodeProp} from "@lezer/common"
import {parser as html} from "@lezer/html"
import ist from "ist"
import {parser, parseCode} from "../dist/index.js"
let full = parser.configure(parseCode({
codeParser(str) { return !str || str == "html" ? html : null },
htmlParser: html.configure({dialect: "noMatch"})
}))
function findMounts(tree: Tree) {
let result = []
for (let cur = tree.cursor(); cur.next();) {
let mount = cur.tree?.prop(NodeProp.mounted)
if (mount) result.push({at: cur.from, mount})
}
return result
}
function test(doc: string, ...nest: [string, ...number[]][]) {
return () => {
let tree = full.parse(doc), mounts = findMounts(tree)
ist(mounts.length, nest.length)
nest.forEach(([repr, ...ranges], i) => {
let {mount, at} = mounts[i]
ist(mount.tree.toString(), "Document(" + repr + ")")
ist(mount.overlay!.map(r => (r.from + at) + "," + (r.to + at)).join(), ranges.join())
})
}
}
describe("Code parsing", () => {
it("parses HTML blocks", test(`
Paragraph
<div id=x>
Hello &amp; goodbye
</div>`, ["Element(OpenTag(StartTag,TagName,Attribute(AttributeName,Is,UnquotedAttributeValue),EndTag),Text,EntityReference,Text,CloseTag(StartCloseTag,TagName,EndTag))", 12, 51]))
it("parses inline HTML", test(
`Paragraph with <em>inline tags</em> in it.`,
["Element(OpenTag(StartTag,TagName,EndTag))", 15, 19],
["CloseTag(StartCloseTag,TagName,EndTag)", 30, 35]))
it("parses indented code", test(`
Paragraph.
<!doctype html>
Hi
`, ["DoctypeDecl,Text", 17, 33, 37, 39]))
it("parses fenced code", test(`
Okay
~~~
<p>
Hey
</p>
~~~`, ["Element(OpenTag(StartTag,TagName,EndTag),Text,CloseTag(StartCloseTag,TagName,EndTag))", 11, 25]))
it("allows gaps in fenced code", test(`
- >~~~
><!doctype html>
>yay
> ~~~`, ["DoctypeDecl,Text", 11, 27, 30, 33]))
it("passes fenced code info", test(`
~~~html
&raquo;
~~~
~~~python
False
~~~`, ["EntityReference", 9, 16]))
it("can parse disjoint ranges", () => {
let tree = parser.parse(`==foo\n==\n==ba==r\n==`, undefined,
[{from: 2, to: 6}, {from: 8, to: 9}, {from: 11, to: 13}, {from: 15, to: 17}])
ist(tree.toString(), "Document(Paragraph,Paragraph)")
ist(tree.length, 15)
ist(tree.topNode.firstChild!.from, 0)
ist(tree.topNode.firstChild!.to, 3)
ist(tree.topNode.lastChild!.from, 9)
ist(tree.topNode.lastChild!.to, 14)
})
})

View File

@ -0,0 +1,12 @@
{
"compilerOptions": {
"lib": ["es2017"],
"noImplicitReturns": true,
"noUnusedLocals": true,
"strict": true,
"target": "es6",
"newLine": "lf",
"moduleResolution": "node"
},
"include": ["*.ts"]
}