import "jest"; import { Mark, Node, Schema } from "prosemirror-model"; import { markFactoryFactory, nodeFactoryFactory, RefsNode } from "../composing"; import * as pquery from "../pquery"; import { Bias } from "../types"; import "./expect.toMatchMark"; const schema = new Schema({ nodes: { doc: { content: "group1+" }, bq: { group: "group1", content: "p+" }, p: { group: "group1", content: "(text|img)*", marks: "bold color" }, img: { inline: true }, text: {} }, marks: { bold: {}, color: { attrs: { name: {} } } } }); const b = markFactoryFactory(schema.marks.bold); const bq = nodeFactoryFactory(schema.nodes.bq); const color = (name: string) => markFactoryFactory(schema.marks.color, { name }); const img = nodeFactoryFactory(schema.nodes.img); const doc = nodeFactoryFactory(schema.nodes.doc); const p = nodeFactoryFactory(schema.nodes.p); describe(pquery.adjacentMarkBounds.name, () => { function test(name: string, { refMap, node }: RefsNode, mark: Mark, bias?: Bias) { it(name, () => { const pos = refMap.get("x"); const boundsStart = refMap.get("<"); const boundsEnd = refMap.get(">"); if (pos === undefined) { throw new Error("Missing position reference, did you forget `{x}` ?"); } const actual = pquery.adjacentMarkBounds(node.resolve(pos), mark.type, bias); if (boundsStart === undefined || boundsEnd === undefined) { expect(actual).toBe(null); } else { expect(actual!.mark).toMatchMark(mark); expect(actual!.from).toBe(boundsStart); expect(actual!.to).toBe(boundsEnd); } }); } const g = color("green"); const r = color("red"); const gTarget = schema.marks.color.create({ name: "green" }); const rTarget = schema.marks.color.create({ name: "red" }); const bTarget = schema.marks.bold.create(); for (const { target, mark, name } of [ { target: gTarget, mark: g, name: "mark with attrs" }, { target: bTarget, mark: b, name: "mark without attrs" } ]) { describe(name, () => { for (const { bias, name } of [{ bias: Bias.BACKWARD, name: "backward" }, { bias: Bias.FORWARD, name: "forward" }]) { describe(`bias=${name}`, () => { test("not adjacent to mark", doc(p("aaaaa{x}bbbbb")), target, bias); test("in mark", doc(p(mark("{<}aaaaa{x}bbbbb{>}"))), target, bias); test("not adjacent to mark, followed by a mark", doc(p("aaa{x}aa", mark("bbbbb"))), target, bias); test("not adjacent to mark, preceded by a mark", doc(p(mark("aaaaa"), "bbb{x}bb")), target, bias); test("adjacent to preceding mark", doc(p(mark("{<}aaaaa{>}"), "{x}", "bbbbb")), target, bias); test("adjacent to multiple preceding mark", doc(p(mark("{<}aa"), mark("aaa{>}"), "{x}", "bbbbb")), target, bias); test("adjacent to proceeding mark", doc(p("aaaaa", "{x}", mark("{<}bbbbb{>}"))), target, bias); test("adjacent to multiple proceeding mark", doc(p("aaaaa", "{x}", mark("{<}bb"), mark("bbb{>}"))), target, bias); }); } }); } describe(`bias=forward`, () => { test("adjacent to multiple different", doc(p(r("aaaaa"), "{x}", g("{<}bbbbb{>}"))), gTarget, Bias.FORWARD); }); describe(`bias=backward`, () => { test("adjacent to multiple different", doc(p(r("{<}aaaaa{>}"), "{x}", g("bbbbb"))), rTarget, Bias.BACKWARD); }); test("defaults to bias=backward", doc(p(r("{<}aaaaa{>}"), "{x}", g("bbbbb"))), rTarget); }); describe(pquery.findForward.name, () => { function test({ refMap, node }: RefsNode) { const expected = []; // Gather together resolved positions corresponding to the numbered refs. // It's assumed there are no gaps in the refs. for (let i = 1; ; i++) { const key = `${i}`; const pos = refMap.get(key); if (pos !== undefined) { expected.push(pos); } else { break; } } // Exhaust the walk forward by returning true, but capture the position of // nodes in the process so it can be compared to the expected positions. const actual: number[] = []; pquery.findForward(node.resolve(refMap.get("^")!), (_node, pos) => { actual.push(pos); return false; }); expect(actual).toMatchObject(expected); } it("from start considers empty p", () => test(doc("{^}", "{1}", p()))); it("from start considers multiple empty p", () => test(doc("{^}", "{1}", p(), "{2}", p()))); it("from start considers text nodes", () => test(doc("{^}", "{1}", p("{2}", "hello world")))); it("from start considers adjacent text nodes", () => test(doc("{^}", "{1}", p("{2}", "hello", "{3}", b("world"))))); it("from in p considers forwards", () => test(doc(p("hello{^}world"), "{1}", p("{2}", "world")))); }); describe(pquery.closest.name, () => { function test({ refMap, node }: RefsNode, predicate: (node: Node) => boolean) { const query = refMap.get("?")!; const expectedPos = refMap.get("pos")!; const expectedStart = refMap.get("start")!; const result = pquery.closest(node.resolve(query), predicate); expect(result).not.toBeNull(); expect(result!.start).toBe(expectedStart); expect(result!.pos).toBe(expectedPos); expect(result!.node).toBe(node.nodeAt(expectedPos)); expect(result!.depth).toBe(node.resolve(result!.pos).depth); } it("finds matching parent", () => test(doc("{pos}", p("{start}he{?}llo")), n => n.type === schema.nodes.p)); it("finds matching grant-parent", () => test(doc("{pos}", bq("{start}", p("he{?}llo"))), n => n.type === schema.nodes.bq)); }); describe(pquery.contiguousTextRange.name, () => { function test({ refMap, node }: RefsNode) { const from = refMap.get("<")!; const to = refMap.get(">")!; const expectedFrom = refMap.get("from")!; const expectedTo = refMap.get("to")!; expect(pquery.contiguousTextRange(node.resolve(from), node.resolve(to))).toMatchObject({ from: expectedFrom, to: expectedTo }); } function testNull({ refMap, node }: RefsNode) { const from = refMap.get("<")!; const to = refMap.get(">")!; expect(pquery.contiguousTextRange(node.resolve(from), node.resolve(to))).toBe(null); } it("empty selection, empty textblock", () => test(doc(p("{from}{<}{>}{to}")))); it("empty selection at end of textblock", () => test(doc(p("hello world{from}{<}{>}{to}")))); it("entire string in block", () => test(doc(p("{from}{<}hello world{>}{to}")))); it("substring string in block", () => test(doc(p("he{from}{<}llo wor{>}{to}ld")))); it("contains mark", () => test(doc(p("hel{from}{<}l", b("o w"), "o{>}{to}rld")))); it("starts in mark", () => test(doc(p(b("hel{from}{<}lo w"), "o{>}{to}rld")))); it("ends in mark", () => test(doc(p("hel{from}{<}lo", b("wo{>}{to}rld"))))); it("inside mark", () => test(doc(p("he", b("l{from}{<}lo wo{>}{to}r"), "ld")))); it("does not cross inlines", () => test(doc(p("hel{from}{<}lo {to}", img(), "wo{>}rld")))); it("returns null if from > to", () => testNull(doc(p("hel{>}lo wo{<}rld")))); it("does not cross blocks", () => test(doc(p("hel{<}{from}lo{to}"), p("wo{>}rld")))); it("handles empty", () => test(doc(p("hel{<}{from}{to}{>}lo world")))); it("returns null for different documents", () => { const { node: doc1, refMap: doc1refMap } = doc(p("hel{<}lo world")); const { node: doc2, refMap: doc2refMap } = doc(p("hello wor{>}ld")); const $from = doc1.resolve(doc1refMap.get("<")!); const $to = doc2.resolve(doc2refMap.get(">")!); expect(pquery.contiguousTextRange($from, $to)).toBe(null); }); it("returns null if start outside a text block", () => testNull(doc("{<}", p("hello wor{>}ld")))); it("handles ending outside a text block", () => test(doc(p("hel{<}{from}lo world{to}"), "{>}"))); });