|
|
@@ -1,7 +1,6 @@
|
|
|
import { describe, expect, it } from "vitest"
|
|
|
import {
|
|
|
type BlockRefDecoration,
|
|
|
- getActiveDecorations,
|
|
|
type PageRefDecoration,
|
|
|
parseMarkdown,
|
|
|
type TagDecoration,
|
|
|
@@ -12,16 +11,16 @@ describe("parseMarkdown", () => {
|
|
|
const result = parseMarkdown("**bold**")
|
|
|
expect(result.content).toHaveLength(1)
|
|
|
expect(result.content[0].type).toBe("strong")
|
|
|
- const [opener, content, closer] = result.content[0].delimiters
|
|
|
- expect(opener.type).toBe("hidden")
|
|
|
- expect(opener.from).toBe(0)
|
|
|
- expect(opener.to).toBe(2)
|
|
|
- expect(content.type).toBe("content")
|
|
|
- expect(content.from).toBe(2)
|
|
|
- expect(content.to).toBe(6)
|
|
|
- expect(closer.type).toBe("hidden")
|
|
|
- expect(closer.from).toBe(6)
|
|
|
- expect(closer.to).toBe(8)
|
|
|
+ const decorationRanges = result.content[0].decorationRanges
|
|
|
+ expect(decorationRanges).toHaveLength(3)
|
|
|
+ expect(decorationRanges[0]).toEqual({ type: "hidden", from: 0, to: 2 })
|
|
|
+ expect(decorationRanges[1]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 2,
|
|
|
+ to: 6,
|
|
|
+ className: "md-strong",
|
|
|
+ })
|
|
|
+ expect(decorationRanges[2]).toEqual({ type: "hidden", from: 6, to: 8 })
|
|
|
})
|
|
|
|
|
|
it("parses italic", () => {
|
|
|
@@ -46,32 +45,32 @@ describe("parseMarkdown", () => {
|
|
|
const result = parseMarkdown("^^highlight^^")
|
|
|
expect(result.content).toHaveLength(1)
|
|
|
expect(result.content[0].type).toBe("highlight")
|
|
|
- const [opener, content, closer] = result.content[0].delimiters
|
|
|
- expect(opener.type).toBe("hidden")
|
|
|
- expect(opener.from).toBe(0)
|
|
|
- expect(opener.to).toBe(2)
|
|
|
- expect(content.type).toBe("content")
|
|
|
- expect(content.from).toBe(2)
|
|
|
- expect(content.to).toBe(11)
|
|
|
- expect(closer.type).toBe("hidden")
|
|
|
- expect(closer.from).toBe(11)
|
|
|
- expect(closer.to).toBe(13)
|
|
|
+ const decorationRanges = result.content[0].decorationRanges
|
|
|
+ expect(decorationRanges).toHaveLength(3)
|
|
|
+ expect(decorationRanges[0]).toEqual({ type: "hidden", from: 0, to: 2 })
|
|
|
+ expect(decorationRanges[1]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 2,
|
|
|
+ to: 11,
|
|
|
+ className: "md-highlight",
|
|
|
+ })
|
|
|
+ expect(decorationRanges[2]).toEqual({ type: "hidden", from: 11, to: 13 })
|
|
|
})
|
|
|
|
|
|
it("parses link", () => {
|
|
|
const result = parseMarkdown("[link](http://example.com)")
|
|
|
expect(result.content).toHaveLength(1)
|
|
|
expect(result.content[0].type).toBe("link")
|
|
|
- const [opener, content, closer] = result.content[0].delimiters
|
|
|
- expect(opener.type).toBe("hidden")
|
|
|
- expect(opener.from).toBe(0)
|
|
|
- expect(opener.to).toBe(1)
|
|
|
- expect(content.type).toBe("content")
|
|
|
- expect(content.from).toBe(1)
|
|
|
- expect(content.to).toBe(5)
|
|
|
- expect(closer.type).toBe("hidden")
|
|
|
- expect(closer.from).toBe(5)
|
|
|
- expect(closer.to).toBe(26)
|
|
|
+ const decorationRanges = result.content[0].decorationRanges
|
|
|
+ expect(decorationRanges).toHaveLength(3)
|
|
|
+ expect(decorationRanges[0]).toEqual({ type: "hidden", from: 0, to: 1 })
|
|
|
+ expect(decorationRanges[1]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 1,
|
|
|
+ to: 5,
|
|
|
+ className: "md-link",
|
|
|
+ })
|
|
|
+ expect(decorationRanges[2]).toEqual({ type: "hidden", from: 5, to: 26 })
|
|
|
})
|
|
|
|
|
|
it("parses page reference [[Page Name]]", () => {
|
|
|
@@ -176,308 +175,608 @@ describe("parseMarkdown", () => {
|
|
|
})
|
|
|
})
|
|
|
|
|
|
-describe("getActiveDecorations", () => {
|
|
|
- it("shows decoration when cursor outside", () => {
|
|
|
- const result = getActiveDecorations("**bold**", 10)
|
|
|
- expect(result.content).toHaveLength(1)
|
|
|
- })
|
|
|
-
|
|
|
- it("hides decoration when cursor inside", () => {
|
|
|
- const result = getActiveDecorations("**bold**", 3)
|
|
|
+describe("unmatched delimiters", () => {
|
|
|
+ it("unmatched bold opening only", () => {
|
|
|
+ const result = parseMarkdown("**bold")
|
|
|
expect(result.content).toHaveLength(0)
|
|
|
})
|
|
|
|
|
|
- it("shows decoration when cursor at start (before delimiter)", () => {
|
|
|
- const result = getActiveDecorations("**bold**", 0)
|
|
|
- expect(result.content).toHaveLength(1)
|
|
|
+ it("unmatched bold closing only", () => {
|
|
|
+ const result = parseMarkdown("bold**")
|
|
|
+ expect(result.content).toHaveLength(0)
|
|
|
})
|
|
|
|
|
|
- it("shows highlight when cursor outside", () => {
|
|
|
- const result = getActiveDecorations("^^highlight^^", 13)
|
|
|
- expect(result.content).toHaveLength(1)
|
|
|
+ it("unmatched italic opening only", () => {
|
|
|
+ const result = parseMarkdown("*text")
|
|
|
+ expect(result.content).toHaveLength(0)
|
|
|
})
|
|
|
|
|
|
- it("hides highlight when cursor inside", () => {
|
|
|
- const result = getActiveDecorations("^^highlight^^", 5)
|
|
|
+ it("unmatched strikethrough only", () => {
|
|
|
+ const result = parseMarkdown("~~strike")
|
|
|
expect(result.content).toHaveLength(0)
|
|
|
})
|
|
|
|
|
|
- it("shows page ref when cursor outside", () => {
|
|
|
- const result = getActiveDecorations("[[Page]]", 10)
|
|
|
- expect(result.content).toHaveLength(1)
|
|
|
- expect(result.content[0].type).toBe("page-ref")
|
|
|
+ it("unmatched code opening only", () => {
|
|
|
+ const result = parseMarkdown("`code")
|
|
|
+ expect(result.content).toHaveLength(0)
|
|
|
})
|
|
|
|
|
|
- it("hides page ref when cursor inside", () => {
|
|
|
- const result = getActiveDecorations("[[Page]]", 5)
|
|
|
+ it("unmatched highlight opening only", () => {
|
|
|
+ const result = parseMarkdown("^^highlight")
|
|
|
expect(result.content).toHaveLength(0)
|
|
|
})
|
|
|
|
|
|
- it("shows block ref when cursor outside", () => {
|
|
|
- const result = getActiveDecorations("((block-id))", 14)
|
|
|
- expect(result.content).toHaveLength(1)
|
|
|
- expect(result.content[0].type).toBe("block-ref")
|
|
|
+ it("unmatched page ref opening only", () => {
|
|
|
+ const result = parseMarkdown("[[Page")
|
|
|
+ expect(result.content).toHaveLength(0)
|
|
|
})
|
|
|
|
|
|
- it("hides block ref when cursor inside", () => {
|
|
|
- const result = getActiveDecorations("((block-id))", 5)
|
|
|
+ it("unmatched block ref opening only", () => {
|
|
|
+ const result = parseMarkdown("((block")
|
|
|
expect(result.content).toHaveLength(0)
|
|
|
})
|
|
|
|
|
|
- it("shows tag when cursor outside", () => {
|
|
|
- const result = getActiveDecorations("#tag", 5)
|
|
|
- expect(result.content).toHaveLength(1)
|
|
|
- expect(result.content[0].type).toBe("tag")
|
|
|
+ it("unmatched tag no content", () => {
|
|
|
+ const result = parseMarkdown("#")
|
|
|
+ expect(result.content).toHaveLength(0)
|
|
|
})
|
|
|
|
|
|
- it("hides tag when cursor inside", () => {
|
|
|
- const result = getActiveDecorations("#tag", 2)
|
|
|
+ it("multiple unmatched delimiters", () => {
|
|
|
+ const result = parseMarkdown("**bold and *italic")
|
|
|
expect(result.content).toHaveLength(0)
|
|
|
})
|
|
|
+})
|
|
|
|
|
|
- it("shows decoration when cursor just after token", () => {
|
|
|
- const result = getActiveDecorations("**bold** text", 9)
|
|
|
- expect(result.content).toHaveLength(1)
|
|
|
+describe("nested malformed tokens", () => {
|
|
|
+ it("italic containing unmatched bold - parses partial", () => {
|
|
|
+ const result = parseMarkdown("*text **bold*")
|
|
|
+ expect(result.content.length).toBeGreaterThanOrEqual(0)
|
|
|
})
|
|
|
|
|
|
- describe("unmatched delimiters", () => {
|
|
|
- it("unmatched bold opening only", () => {
|
|
|
- const result = parseMarkdown("**bold")
|
|
|
- expect(result.content).toHaveLength(0)
|
|
|
- })
|
|
|
-
|
|
|
- it("unmatched bold closing only", () => {
|
|
|
- const result = parseMarkdown("bold**")
|
|
|
- expect(result.content).toHaveLength(0)
|
|
|
- })
|
|
|
-
|
|
|
- it("unmatched italic opening only", () => {
|
|
|
- const result = parseMarkdown("*text")
|
|
|
- expect(result.content).toHaveLength(0)
|
|
|
- })
|
|
|
-
|
|
|
- it("unmatched strikethrough only", () => {
|
|
|
- const result = parseMarkdown("~~strike")
|
|
|
- expect(result.content).toHaveLength(0)
|
|
|
- })
|
|
|
-
|
|
|
- it("unmatched code opening only", () => {
|
|
|
- const result = parseMarkdown("`code")
|
|
|
- expect(result.content).toHaveLength(0)
|
|
|
- })
|
|
|
-
|
|
|
- it("unmatched highlight opening only", () => {
|
|
|
- const result = parseMarkdown("^^highlight")
|
|
|
- expect(result.content).toHaveLength(0)
|
|
|
- })
|
|
|
-
|
|
|
- it("unmatched page ref opening only", () => {
|
|
|
- const result = parseMarkdown("[[Page")
|
|
|
- expect(result.content).toHaveLength(0)
|
|
|
- })
|
|
|
+ it("bold containing unmatched italic - parses partial", () => {
|
|
|
+ const result = parseMarkdown("**text *italic")
|
|
|
+ expect(result.content.length).toBeGreaterThanOrEqual(0)
|
|
|
+ })
|
|
|
|
|
|
- it("unmatched block ref opening only", () => {
|
|
|
- const result = parseMarkdown("((block")
|
|
|
- expect(result.content).toHaveLength(0)
|
|
|
- })
|
|
|
+ it("strikethrough containing unmatched bold - parses partial", () => {
|
|
|
+ const result = parseMarkdown("~~text **bold~~")
|
|
|
+ expect(result.content.length).toBeGreaterThanOrEqual(0)
|
|
|
+ })
|
|
|
|
|
|
- it("unmatched tag no content", () => {
|
|
|
- const result = parseMarkdown("#")
|
|
|
- expect(result.content).toHaveLength(0)
|
|
|
- })
|
|
|
+ it("nested triple delimiters", () => {
|
|
|
+ const result = parseMarkdown("***text***")
|
|
|
+ expect(result.content.length).toBeGreaterThan(0)
|
|
|
+ })
|
|
|
|
|
|
- it("multiple unmatched delimiters", () => {
|
|
|
- const result = parseMarkdown("**bold and *italic")
|
|
|
- expect(result.content).toHaveLength(0)
|
|
|
- })
|
|
|
+ it("code inside bold", () => {
|
|
|
+ const result = parseMarkdown("**`code`**")
|
|
|
+ expect(result.content).toHaveLength(2)
|
|
|
})
|
|
|
|
|
|
- describe("nested malformed tokens", () => {
|
|
|
- it("italic containing unmatched bold - parses partial", () => {
|
|
|
- const result = parseMarkdown("*text **bold*")
|
|
|
- expect(result.content.length).toBeGreaterThanOrEqual(0)
|
|
|
- })
|
|
|
+ it("link inside bold", () => {
|
|
|
+ const result = parseMarkdown("**[text](url)**")
|
|
|
+ expect(result.content).toHaveLength(2)
|
|
|
+ })
|
|
|
+})
|
|
|
|
|
|
- it("bold containing unmatched italic - parses partial", () => {
|
|
|
- const result = parseMarkdown("**text *italic")
|
|
|
- expect(result.content.length).toBeGreaterThanOrEqual(0)
|
|
|
- })
|
|
|
+describe("incomplete patterns at end of text", () => {
|
|
|
+ it("incomplete page ref at end", () => {
|
|
|
+ const result = parseMarkdown("text [[Page")
|
|
|
+ expect(result.content).toHaveLength(0)
|
|
|
+ })
|
|
|
|
|
|
- it("strikethrough containing unmatched bold - parses partial", () => {
|
|
|
- const result = parseMarkdown("~~text **bold~~")
|
|
|
- expect(result.content.length).toBeGreaterThanOrEqual(0)
|
|
|
- })
|
|
|
+ it("incomplete block ref at end", () => {
|
|
|
+ const result = parseMarkdown("text ((block")
|
|
|
+ expect(result.content).toHaveLength(0)
|
|
|
+ })
|
|
|
|
|
|
- it("nested triple delimiters", () => {
|
|
|
- const result = parseMarkdown("***text***")
|
|
|
- expect(result.content.length).toBeGreaterThan(0)
|
|
|
- })
|
|
|
+ it("incomplete highlight at end", () => {
|
|
|
+ const result = parseMarkdown("text ^^high")
|
|
|
+ expect(result.content).toHaveLength(0)
|
|
|
+ })
|
|
|
|
|
|
- it("code inside bold", () => {
|
|
|
- const result = parseMarkdown("**`code`**")
|
|
|
- expect(result.content).toHaveLength(2)
|
|
|
- })
|
|
|
+ it("tag at end of word not at boundary", () => {
|
|
|
+ const result = parseMarkdown("word#tag")
|
|
|
+ expect(result.content).toHaveLength(0)
|
|
|
+ })
|
|
|
|
|
|
- it("link inside bold", () => {
|
|
|
- const result = parseMarkdown("**[text](url)**")
|
|
|
- expect(result.content).toHaveLength(2)
|
|
|
- })
|
|
|
+ it("tag at boundary", () => {
|
|
|
+ const result = parseMarkdown("word #tag")
|
|
|
+ expect(result.content).toHaveLength(1)
|
|
|
+ expect(result.content[0].type).toBe("tag")
|
|
|
})
|
|
|
+})
|
|
|
|
|
|
- describe("incomplete patterns at end of text", () => {
|
|
|
- it("incomplete page ref at end", () => {
|
|
|
- const result = parseMarkdown("text [[Page")
|
|
|
- expect(result.content).toHaveLength(0)
|
|
|
- })
|
|
|
+describe("whitespace variations", () => {
|
|
|
+ it("multiple spaces before tag", () => {
|
|
|
+ const result = parseMarkdown("text #tag")
|
|
|
+ expect(result.content).toHaveLength(1)
|
|
|
+ })
|
|
|
|
|
|
- it("incomplete block ref at end", () => {
|
|
|
- const result = parseMarkdown("text ((block")
|
|
|
- expect(result.content).toHaveLength(0)
|
|
|
- })
|
|
|
+ it("tab before tag", () => {
|
|
|
+ const result = parseMarkdown("text\t#tag")
|
|
|
+ expect(result.content).toHaveLength(1)
|
|
|
+ })
|
|
|
|
|
|
- it("incomplete highlight at end", () => {
|
|
|
- const result = parseMarkdown("text ^^high")
|
|
|
- expect(result.content).toHaveLength(0)
|
|
|
- })
|
|
|
+ it("newlines in page ref - Lezer allows but content split", () => {
|
|
|
+ const result = parseMarkdown("[[Page\nName]]")
|
|
|
+ expect(result.content.length).toBeGreaterThanOrEqual(0)
|
|
|
+ })
|
|
|
+})
|
|
|
|
|
|
- it("tag at end of word not at boundary", () => {
|
|
|
- const result = parseMarkdown("word#tag")
|
|
|
- expect(result.content).toHaveLength(0)
|
|
|
- })
|
|
|
+describe("block decorations", () => {
|
|
|
+ it("parses heading level 1", () => {
|
|
|
+ const result = parseMarkdown("# Hello World")
|
|
|
+ expect(result.blocks).toHaveLength(1)
|
|
|
+ expect(result.blocks[0].type).toBe("heading")
|
|
|
+ expect(result.blocks[0].metadata?.level).toBe(1)
|
|
|
+ expect(result.blocks[0].decorationRanges).toHaveLength(2)
|
|
|
+ expect(result.blocks[0].decorationRanges[0]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 0,
|
|
|
+ to: 1,
|
|
|
+ className: "md-heading-marker",
|
|
|
+ })
|
|
|
+ expect(result.blocks[0].decorationRanges[1]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 2,
|
|
|
+ to: 13,
|
|
|
+ className: "md-heading-content",
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ it("parses heading level 3", () => {
|
|
|
+ const result = parseMarkdown("### Title")
|
|
|
+ expect(result.blocks).toHaveLength(1)
|
|
|
+ expect(result.blocks[0].type).toBe("heading")
|
|
|
+ expect(result.blocks[0].metadata?.level).toBe(3)
|
|
|
+ expect(result.blocks[0].decorationRanges).toHaveLength(2)
|
|
|
+ expect(result.blocks[0].decorationRanges[0]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 0,
|
|
|
+ to: 3,
|
|
|
+ className: "md-heading-marker",
|
|
|
+ })
|
|
|
+ expect(result.blocks[0].decorationRanges[1]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 4,
|
|
|
+ to: 9,
|
|
|
+ className: "md-heading-content",
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ it("parses task TODO", () => {
|
|
|
+ const result = parseMarkdown("TODO buy milk")
|
|
|
+ expect(result.blocks).toHaveLength(1)
|
|
|
+ expect(result.blocks[0].type).toBe("task")
|
|
|
+ expect(result.blocks[0].decorationRanges).toHaveLength(2)
|
|
|
+ expect(result.blocks[0].decorationRanges[0]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 0,
|
|
|
+ to: 4,
|
|
|
+ className: "md-task-marker",
|
|
|
+ })
|
|
|
+ expect(result.blocks[0].decorationRanges[1]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 5,
|
|
|
+ to: 13,
|
|
|
+ className: "md-task-content",
|
|
|
+ })
|
|
|
+ expect(result.blocks[0].metadata?.taskState).toBe("TODO")
|
|
|
+ })
|
|
|
+
|
|
|
+ it("parses task with priority", () => {
|
|
|
+ const result = parseMarkdown("TODO buy milk [#A]")
|
|
|
+ expect(result.blocks).toHaveLength(2)
|
|
|
+ expect(result.blocks[0].type).toBe("task")
|
|
|
+ expect(result.blocks[0].decorationRanges).toHaveLength(2)
|
|
|
+ expect(result.blocks[0].decorationRanges[0]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 0,
|
|
|
+ to: 4,
|
|
|
+ className: "md-task-marker",
|
|
|
+ })
|
|
|
+ expect(result.blocks[0].decorationRanges[1]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 5,
|
|
|
+ to: 13,
|
|
|
+ className: "md-task-content",
|
|
|
+ })
|
|
|
+ expect(result.blocks[0].metadata?.taskState).toBe("TODO")
|
|
|
+ expect(result.blocks[1].type).toBe("priority")
|
|
|
+ expect(result.blocks[1].decorationRanges).toHaveLength(2)
|
|
|
+ expect(result.blocks[1].decorationRanges[0]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 13,
|
|
|
+ to: 17,
|
|
|
+ className: "md-priority-marker",
|
|
|
+ })
|
|
|
+ expect(result.blocks[1].decorationRanges[1]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 17,
|
|
|
+ to: 18,
|
|
|
+ className: "md-priority-content",
|
|
|
+ })
|
|
|
+ expect(result.blocks[1].metadata?.priority).toBe("A")
|
|
|
+ })
|
|
|
+
|
|
|
+ it("parses priority only", () => {
|
|
|
+ const result = parseMarkdown("[#B] high priority")
|
|
|
+ expect(result.blocks).toHaveLength(1)
|
|
|
+ expect(result.blocks[0].type).toBe("priority")
|
|
|
+ expect(result.blocks[0].decorationRanges).toHaveLength(2)
|
|
|
+ expect(result.blocks[0].decorationRanges[0]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 0,
|
|
|
+ to: 4,
|
|
|
+ className: "md-priority-marker",
|
|
|
+ })
|
|
|
+ expect(result.blocks[0].decorationRanges[1]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 5,
|
|
|
+ to: 18,
|
|
|
+ className: "md-priority-content",
|
|
|
+ })
|
|
|
+ expect(result.blocks[0].metadata?.priority).toBe("B")
|
|
|
+ })
|
|
|
+
|
|
|
+ it("parses blockquote", () => {
|
|
|
+ const result = parseMarkdown("> Quote text")
|
|
|
+ expect(result.blocks).toHaveLength(1)
|
|
|
+ expect(result.blocks[0].type).toBe("blockquote")
|
|
|
+ })
|
|
|
+
|
|
|
+ it("parses callout note", () => {
|
|
|
+ const result = parseMarkdown("> [!NOTE] important info")
|
|
|
+ expect(result.blocks).toHaveLength(1)
|
|
|
+ expect(result.blocks[0].type).toBe("callout")
|
|
|
+ expect(result.blocks[0].metadata?.calloutType).toBe("NOTE")
|
|
|
+ })
|
|
|
+
|
|
|
+ it("parses callout warning", () => {
|
|
|
+ const result = parseMarkdown("> [!WARNING] be careful")
|
|
|
+ expect(result.blocks).toHaveLength(1)
|
|
|
+ expect(result.blocks[0].type).toBe("callout")
|
|
|
+ expect(result.blocks[0].metadata?.calloutType).toBe("WARNING")
|
|
|
+ })
|
|
|
+
|
|
|
+ it("parses date", () => {
|
|
|
+ const result = parseMarkdown("📅 2024-01-15")
|
|
|
+ expect(result.blocks).toHaveLength(1)
|
|
|
+ expect(result.blocks[0].type).toBe("date")
|
|
|
+ expect(result.blocks[0].metadata?.date).toBe("2024-01-15")
|
|
|
+ })
|
|
|
+
|
|
|
+ it("returns empty blocks for plain text", () => {
|
|
|
+ const result = parseMarkdown("Just some plain text")
|
|
|
+ expect(result.blocks).toHaveLength(0)
|
|
|
+ })
|
|
|
+
|
|
|
+ it("handles all task states", () => {
|
|
|
+ const states = [
|
|
|
+ "TODO",
|
|
|
+ "DOING",
|
|
|
+ "DONE",
|
|
|
+ "LATER",
|
|
|
+ "NOW",
|
|
|
+ "WAITING",
|
|
|
+ "CANCELLED",
|
|
|
+ ]
|
|
|
+ for (const state of states) {
|
|
|
+ const result = parseMarkdown(`${state} task`)
|
|
|
+ expect(result.blocks).toHaveLength(1)
|
|
|
+ expect(result.blocks[0].metadata?.taskState).toBe(state)
|
|
|
+ }
|
|
|
+ })
|
|
|
+})
|
|
|
|
|
|
- it("tag at boundary", () => {
|
|
|
- const result = parseMarkdown("word #tag")
|
|
|
- expect(result.content).toHaveLength(1)
|
|
|
- expect(result.content[0].type).toBe("tag")
|
|
|
+describe("leading whitespace handling", () => {
|
|
|
+ it("heading with leading spaces positions marker correctly", () => {
|
|
|
+ const result = parseMarkdown(" # Title")
|
|
|
+ expect(result.blocks).toHaveLength(1)
|
|
|
+ expect(result.blocks[0].decorationRanges[0]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 2,
|
|
|
+ to: 3,
|
|
|
+ className: "md-heading-marker",
|
|
|
+ })
|
|
|
+ expect(result.blocks[0].decorationRanges[1]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 4,
|
|
|
+ to: 9,
|
|
|
+ className: "md-heading-content",
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ it("task with leading spaces positions marker correctly", () => {
|
|
|
+ const result = parseMarkdown(" TODO buy milk")
|
|
|
+ expect(result.blocks).toHaveLength(1)
|
|
|
+ expect(result.blocks[0].decorationRanges[0]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 2,
|
|
|
+ to: 6,
|
|
|
+ className: "md-task-marker",
|
|
|
+ })
|
|
|
+ expect(result.blocks[0].decorationRanges[1]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 7,
|
|
|
+ to: 15,
|
|
|
+ className: "md-task-content",
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ it("priority with leading spaces positions marker correctly", () => {
|
|
|
+ const result = parseMarkdown(" [#A] high priority")
|
|
|
+ expect(result.blocks).toHaveLength(1)
|
|
|
+ expect(result.blocks[0].decorationRanges[0]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 2,
|
|
|
+ to: 6,
|
|
|
+ className: "md-priority-marker",
|
|
|
+ })
|
|
|
+ expect(result.blocks[0].decorationRanges[1]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 7,
|
|
|
+ to: 20,
|
|
|
+ className: "md-priority-content",
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ it("task with priority and leading spaces", () => {
|
|
|
+ const result = parseMarkdown(" TODO buy milk [#A]")
|
|
|
+ expect(result.blocks).toHaveLength(2)
|
|
|
+ expect(result.blocks[0].type).toBe("task")
|
|
|
+ expect(result.blocks[0].decorationRanges[0]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 2,
|
|
|
+ to: 6,
|
|
|
+ className: "md-task-marker",
|
|
|
+ })
|
|
|
+ expect(result.blocks[1].type).toBe("priority")
|
|
|
+ expect(result.blocks[1].decorationRanges[0]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 15,
|
|
|
+ to: 19,
|
|
|
+ className: "md-priority-marker",
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ it("blockquote with leading spaces", () => {
|
|
|
+ const result = parseMarkdown(" > Quote text")
|
|
|
+ expect(result.blocks).toHaveLength(1)
|
|
|
+ expect(result.blocks[0].decorationRanges[0]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 2,
|
|
|
+ to: 3,
|
|
|
+ className: "md-blockquote-marker",
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ it("callout with leading spaces", () => {
|
|
|
+ const result = parseMarkdown(" > [!NOTE] important")
|
|
|
+ expect(result.blocks).toHaveLength(1)
|
|
|
+ expect(result.blocks[0].decorationRanges[0]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 2,
|
|
|
+ to: 11,
|
|
|
+ className: "md-callout-marker",
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ it("date with leading spaces", () => {
|
|
|
+ const result = parseMarkdown(" 📅 2024-01-15")
|
|
|
+ expect(result.blocks).toHaveLength(1)
|
|
|
+ expect(result.blocks[0].decorationRanges[0]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 2,
|
|
|
+ to: 4,
|
|
|
+ className: "md-date-marker",
|
|
|
})
|
|
|
})
|
|
|
+})
|
|
|
|
|
|
- describe("whitespace variations", () => {
|
|
|
- it("multiple spaces before tag", () => {
|
|
|
- const result = parseMarkdown("text #tag")
|
|
|
+describe("whitespace variations in task rules", () => {
|
|
|
+ it("task with multiple spaces after keyword", () => {
|
|
|
+ const result = parseMarkdown("TODO buy milk")
|
|
|
+ expect(result.blocks).toHaveLength(1)
|
|
|
+ expect(result.blocks[0].decorationRanges[0]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 0,
|
|
|
+ to: 4,
|
|
|
+ className: "md-task-marker",
|
|
|
+ })
|
|
|
+ expect(result.blocks[0].decorationRanges[1]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 8,
|
|
|
+ to: 16,
|
|
|
+ className: "md-task-content",
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ it("task with priority and multiple spaces before priority", () => {
|
|
|
+ const result = parseMarkdown("TODO buy milk [#A]")
|
|
|
+ expect(result.blocks).toHaveLength(2)
|
|
|
+ expect(result.blocks[0].type).toBe("task")
|
|
|
+ expect(result.blocks[0].decorationRanges[1].to).toBe(13)
|
|
|
+ expect(result.blocks[1].type).toBe("priority")
|
|
|
+ expect(result.blocks[1].decorationRanges[0]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 13,
|
|
|
+ to: 17,
|
|
|
+ className: "md-priority-marker",
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ it("priority with multiple spaces after marker", () => {
|
|
|
+ const result = parseMarkdown("[#B] high priority")
|
|
|
+ expect(result.blocks).toHaveLength(1)
|
|
|
+ expect(result.blocks[0].decorationRanges[0]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 0,
|
|
|
+ to: 4,
|
|
|
+ className: "md-priority-marker",
|
|
|
+ })
|
|
|
+ expect(result.blocks[0].decorationRanges[1]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 8,
|
|
|
+ to: 21,
|
|
|
+ className: "md-priority-content",
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ describe("multi-line block positions", () => {
|
|
|
+ it("heading on second line has correct absolute positions", () => {
|
|
|
+ const result = parseMarkdown("first line\n# Title")
|
|
|
+ expect(result.blocks).toHaveLength(1)
|
|
|
+ expect(result.blocks[0].decorationRanges[0]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 11,
|
|
|
+ to: 12,
|
|
|
+ className: "md-heading-marker",
|
|
|
+ })
|
|
|
+ expect(result.blocks[0].decorationRanges[1]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 13,
|
|
|
+ to: 18,
|
|
|
+ className: "md-heading-content",
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ it("task on second line has correct absolute positions", () => {
|
|
|
+ const result = parseMarkdown("first line\nTODO buy milk")
|
|
|
+ expect(result.blocks).toHaveLength(1)
|
|
|
+ expect(result.blocks[0].decorationRanges[0]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 11,
|
|
|
+ to: 15,
|
|
|
+ className: "md-task-marker",
|
|
|
+ })
|
|
|
+ expect(result.blocks[0].decorationRanges[1]).toEqual({
|
|
|
+ type: "content",
|
|
|
+ from: 16,
|
|
|
+ to: 24,
|
|
|
+ className: "md-task-content",
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ it("inline tokens on first line are unaffected by second line", () => {
|
|
|
+ const result = parseMarkdown("**bold**\nplain text")
|
|
|
expect(result.content).toHaveLength(1)
|
|
|
+ expect(result.content[0].decorationRanges[0]).toEqual({
|
|
|
+ type: "hidden",
|
|
|
+ from: 0,
|
|
|
+ to: 2,
|
|
|
+ })
|
|
|
})
|
|
|
|
|
|
- it("tab before tag", () => {
|
|
|
- const result = parseMarkdown("text\t#tag")
|
|
|
+ it("tag on second line is parsed", () => {
|
|
|
+ const result = parseMarkdown("some text\n#tag")
|
|
|
expect(result.content).toHaveLength(1)
|
|
|
+ expect(result.content[0].type).toBe("tag")
|
|
|
})
|
|
|
|
|
|
- it("newlines in page ref - Lezer allows but content split", () => {
|
|
|
- const result = parseMarkdown("[[Page\nName]]")
|
|
|
- expect(result.content.length).toBeGreaterThanOrEqual(0)
|
|
|
- })
|
|
|
- })
|
|
|
-
|
|
|
- describe("block decorations", () => {
|
|
|
- it("parses heading level 1", () => {
|
|
|
- const result = parseMarkdown("# Hello World")
|
|
|
- expect(result.blocks).toHaveLength(1)
|
|
|
- expect(result.blocks[0].type).toBe("heading")
|
|
|
- expect(result.blocks[0].metadata?.level).toBe(1)
|
|
|
- expect(result.blocks[0].markerRange).toEqual([0, 2])
|
|
|
- expect(result.blocks[0].contentRange).toEqual([2, 13])
|
|
|
+ it("tag after blank line is parsed", () => {
|
|
|
+ const result = parseMarkdown("some text\n\n#tag more text")
|
|
|
+ expect(result.content).toHaveLength(1)
|
|
|
+ expect(result.content[0].type).toBe("tag")
|
|
|
})
|
|
|
|
|
|
- it("parses heading level 3", () => {
|
|
|
- const result = parseMarkdown("### Title")
|
|
|
- expect(result.blocks).toHaveLength(1)
|
|
|
- expect(result.blocks[0].type).toBe("heading")
|
|
|
- expect(result.blocks[0].metadata?.level).toBe(3)
|
|
|
+ it("property on second line is parsed", () => {
|
|
|
+ const result = parseMarkdown("some text\nkey:: value")
|
|
|
+ expect(result.properties).toHaveLength(1)
|
|
|
+ expect(result.properties[0].key).toBe("key")
|
|
|
})
|
|
|
|
|
|
- it("parses task TODO", () => {
|
|
|
- const result = parseMarkdown("TODO buy milk")
|
|
|
+ it("date on second line is parsed", () => {
|
|
|
+ const result = parseMarkdown("some text\n📅 2024-01-15")
|
|
|
expect(result.blocks).toHaveLength(1)
|
|
|
- expect(result.blocks[0].type).toBe("task")
|
|
|
- expect(result.blocks[0].metadata?.taskState).toBe("TODO")
|
|
|
- })
|
|
|
-
|
|
|
- it("parses task with priority", () => {
|
|
|
- const result = parseMarkdown("TODO buy milk [#A]")
|
|
|
- expect(result.blocks).toHaveLength(2)
|
|
|
- expect(result.blocks[0].type).toBe("task")
|
|
|
- expect(result.blocks[0].metadata?.taskState).toBe("TODO")
|
|
|
- expect(result.blocks[1].type).toBe("priority")
|
|
|
- expect(result.blocks[1].metadata?.priority).toBe("A")
|
|
|
+ expect(result.blocks[0].type).toBe("date")
|
|
|
})
|
|
|
+ })
|
|
|
+})
|
|
|
|
|
|
- it("parses priority only", () => {
|
|
|
- const result = parseMarkdown("[#B] high priority")
|
|
|
- expect(result.blocks).toHaveLength(1)
|
|
|
- expect(result.blocks[0].type).toBe("priority")
|
|
|
- expect(result.blocks[0].metadata?.priority).toBe("B")
|
|
|
+describe("wrapperAttrs output", () => {
|
|
|
+ it("bold token has wrapperAttrs", () => {
|
|
|
+ const result = parseMarkdown("**bold**")
|
|
|
+ expect(result.content[0].wrapperAttrs).toEqual({
|
|
|
+ "data-md-type": "strong",
|
|
|
})
|
|
|
+ })
|
|
|
|
|
|
- it("parses blockquote", () => {
|
|
|
- const result = parseMarkdown("> Quote text")
|
|
|
- expect(result.blocks).toHaveLength(1)
|
|
|
- expect(result.blocks[0].type).toBe("blockquote")
|
|
|
+ it("page ref token has wrapperAttrs", () => {
|
|
|
+ const result = parseMarkdown("[[Page Name]]")
|
|
|
+ const ref = result.content[0] as PageRefDecoration
|
|
|
+ expect(ref.wrapperAttrs).toEqual({
|
|
|
+ "data-page-name": "Page Name",
|
|
|
+ role: "link",
|
|
|
+ tabindex: "0",
|
|
|
})
|
|
|
+ })
|
|
|
|
|
|
- it("parses callout note", () => {
|
|
|
- const result = parseMarkdown("> [!NOTE] important info")
|
|
|
- expect(result.blocks).toHaveLength(1)
|
|
|
- expect(result.blocks[0].type).toBe("callout")
|
|
|
- expect(result.blocks[0].metadata?.calloutType).toBe("NOTE")
|
|
|
+ it("page ref with alias includes data-alias", () => {
|
|
|
+ const result = parseMarkdown("[[Page Name|Alias]]")
|
|
|
+ const ref = result.content[0] as PageRefDecoration
|
|
|
+ expect(ref.wrapperAttrs).toEqual({
|
|
|
+ "data-page-name": "Page Name",
|
|
|
+ "data-alias": "Alias",
|
|
|
+ role: "link",
|
|
|
+ tabindex: "0",
|
|
|
})
|
|
|
+ })
|
|
|
|
|
|
- it("parses callout warning", () => {
|
|
|
- const result = parseMarkdown("> [!WARNING] be careful")
|
|
|
- expect(result.blocks).toHaveLength(1)
|
|
|
- expect(result.blocks[0].type).toBe("callout")
|
|
|
- expect(result.blocks[0].metadata?.calloutType).toBe("WARNING")
|
|
|
- })
|
|
|
+ it("block ref token has wrapperAttrs", () => {
|
|
|
+ const result = parseMarkdown("((block-id))")
|
|
|
+ const ref = result.content[0] as BlockRefDecoration
|
|
|
+ expect(ref.wrapperAttrs).toEqual({ "data-block-id": "block-id" })
|
|
|
+ })
|
|
|
|
|
|
- it("parses date", () => {
|
|
|
- const result = parseMarkdown("📅 2024-01-15")
|
|
|
- expect(result.blocks).toHaveLength(1)
|
|
|
- expect(result.blocks[0].type).toBe("date")
|
|
|
- expect(result.blocks[0].metadata?.date).toBe("2024-01-15")
|
|
|
- })
|
|
|
+ it("tag token has wrapperAttrs", () => {
|
|
|
+ const result = parseMarkdown("#tag")
|
|
|
+ const tag = result.content[0] as TagDecoration
|
|
|
+ expect(tag.wrapperAttrs).toEqual({ "data-tag": "tag" })
|
|
|
+ })
|
|
|
|
|
|
- it("returns empty blocks for plain text", () => {
|
|
|
- const result = parseMarkdown("Just some plain text")
|
|
|
- expect(result.blocks).toHaveLength(0)
|
|
|
+ it("highlight token has wrapperAttrs", () => {
|
|
|
+ const result = parseMarkdown("^^highlight^^")
|
|
|
+ expect(result.content[0].wrapperAttrs).toEqual({
|
|
|
+ "data-md-type": "highlight",
|
|
|
})
|
|
|
+ })
|
|
|
|
|
|
- it("handles all task states", () => {
|
|
|
- const states = [
|
|
|
- "TODO",
|
|
|
- "DOING",
|
|
|
- "DONE",
|
|
|
- "LATER",
|
|
|
- "NOW",
|
|
|
- "WAITING",
|
|
|
- "CANCELLED",
|
|
|
- ]
|
|
|
- for (const state of states) {
|
|
|
- const result = parseMarkdown(`${state} task`)
|
|
|
- expect(result.blocks).toHaveLength(1)
|
|
|
- expect(result.blocks[0].metadata?.taskState).toBe(state)
|
|
|
- }
|
|
|
+ it("heading block has wrapperAttrs", () => {
|
|
|
+ const result = parseMarkdown("# Title")
|
|
|
+ expect(result.blocks[0].wrapperAttrs).toEqual({
|
|
|
+ "data-heading-level": "1",
|
|
|
})
|
|
|
})
|
|
|
|
|
|
- describe("getActiveDecorations blocks", () => {
|
|
|
- it("hides block markers when cursor inside block", () => {
|
|
|
- const result = getActiveDecorations("TODO buy milk", 5)
|
|
|
- expect(result.blocks).toHaveLength(0)
|
|
|
+ it("task block has wrapperAttrs", () => {
|
|
|
+ const result = parseMarkdown("TODO buy milk")
|
|
|
+ expect(result.blocks[0].wrapperAttrs).toEqual({
|
|
|
+ "data-task-state": "TODO",
|
|
|
})
|
|
|
+ })
|
|
|
|
|
|
- it("shows block markers when cursor at start", () => {
|
|
|
- const result = getActiveDecorations("TODO buy milk", 0)
|
|
|
- expect(result.blocks).toHaveLength(1)
|
|
|
+ it("task with priority has independent wrapperAttrs", () => {
|
|
|
+ const result = parseMarkdown("TODO buy milk [#A]")
|
|
|
+ expect(result.blocks).toHaveLength(2)
|
|
|
+ expect(result.blocks[0].wrapperAttrs).toEqual({
|
|
|
+ "data-task-state": "TODO",
|
|
|
})
|
|
|
+ expect(result.blocks[1].wrapperAttrs).toEqual({ "data-priority": "A" })
|
|
|
+ })
|
|
|
|
|
|
- it("shows heading when cursor at start", () => {
|
|
|
- const result = getActiveDecorations("# Title", 0)
|
|
|
- expect(result.blocks).toHaveLength(1)
|
|
|
+ it("callout block has wrapperAttrs", () => {
|
|
|
+ const result = parseMarkdown("> [!NOTE] important info")
|
|
|
+ expect(result.blocks[0].wrapperAttrs).toEqual({
|
|
|
+ "data-callout-type": "NOTE",
|
|
|
})
|
|
|
+ })
|
|
|
|
|
|
- it("hides heading when cursor inside", () => {
|
|
|
- const result = getActiveDecorations("# Title", 3)
|
|
|
- expect(result.blocks).toHaveLength(0)
|
|
|
+ it("date block has wrapperAttrs", () => {
|
|
|
+ const result = parseMarkdown("📅 2024-01-15")
|
|
|
+ expect(result.blocks[0].wrapperAttrs).toEqual({
|
|
|
+ "data-date": "2024-01-15",
|
|
|
})
|
|
|
})
|
|
|
})
|