|
@@ -0,0 +1,483 @@
|
|
|
|
|
+import { describe, expect, it } from "vitest"
|
|
|
|
|
+import {
|
|
|
|
|
+ type BlockRefDecoration,
|
|
|
|
|
+ getActiveDecorations,
|
|
|
|
|
+ type PageRefDecoration,
|
|
|
|
|
+ parseMarkdown,
|
|
|
|
|
+ type TagDecoration,
|
|
|
|
|
+} from "../markdown-parser"
|
|
|
|
|
+
|
|
|
|
|
+describe("parseMarkdown", () => {
|
|
|
|
|
+ it("parses bold", () => {
|
|
|
|
|
+ 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)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("parses italic", () => {
|
|
|
|
|
+ const result = parseMarkdown("*italic*")
|
|
|
|
|
+ expect(result.content).toHaveLength(1)
|
|
|
|
|
+ expect(result.content[0].type).toBe("em")
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("parses code", () => {
|
|
|
|
|
+ const result = parseMarkdown("`code`")
|
|
|
|
|
+ expect(result.content).toHaveLength(1)
|
|
|
|
|
+ expect(result.content[0].type).toBe("code")
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("parses strikethrough", () => {
|
|
|
|
|
+ const result = parseMarkdown("~~strike~~")
|
|
|
|
|
+ expect(result.content).toHaveLength(1)
|
|
|
|
|
+ expect(result.content[0].type).toBe("strike")
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("parses highlight", () => {
|
|
|
|
|
+ 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)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ 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)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("parses page reference [[Page Name]]", () => {
|
|
|
|
|
+ const result = parseMarkdown("[[Page Name]]")
|
|
|
|
|
+ expect(result.content).toHaveLength(1)
|
|
|
|
|
+ expect(result.content[0].type).toBe("page-ref")
|
|
|
|
|
+ const ref = result.content[0] as PageRefDecoration
|
|
|
|
|
+ expect(ref.pageName).toBe("Page Name")
|
|
|
|
|
+ expect(ref.alias).toBeUndefined()
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("parses page reference with alias [[Page Name|Alias]]", () => {
|
|
|
|
|
+ const result = parseMarkdown("[[Page Name|Alias]]")
|
|
|
|
|
+ expect(result.content).toHaveLength(1)
|
|
|
|
|
+ expect(result.content[0].type).toBe("page-ref")
|
|
|
|
|
+ const ref = result.content[0] as PageRefDecoration
|
|
|
|
|
+ expect(ref.pageName).toBe("Page Name")
|
|
|
|
|
+ expect(ref.alias).toBe("Alias")
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("parses block reference ((block-id))", () => {
|
|
|
|
|
+ const result = parseMarkdown("((block-id))")
|
|
|
|
|
+ expect(result.content).toHaveLength(1)
|
|
|
|
|
+ expect(result.content[0].type).toBe("block-ref")
|
|
|
|
|
+ const ref = result.content[0] as BlockRefDecoration
|
|
|
|
|
+ expect(ref.blockId).toBe("block-id")
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("parses tag #tag", () => {
|
|
|
|
|
+ const result = parseMarkdown("#tag")
|
|
|
|
|
+ expect(result.content).toHaveLength(1)
|
|
|
|
|
+ expect(result.content[0].type).toBe("tag")
|
|
|
|
|
+ const tag = result.content[0] as TagDecoration
|
|
|
|
|
+ expect(tag.tag).toBe("tag")
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("parses nested italic+bold", () => {
|
|
|
|
|
+ const result = parseMarkdown("***italicbold***")
|
|
|
|
|
+ expect(result.content).toHaveLength(2)
|
|
|
|
|
+ const types = result.content.map((t) => t.type)
|
|
|
|
|
+ expect(types).toContain("strong")
|
|
|
|
|
+ expect(types).toContain("em")
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("parses property syntax", () => {
|
|
|
|
|
+ const result = parseMarkdown("key:: value")
|
|
|
|
|
+ expect(result.properties).toHaveLength(1)
|
|
|
|
|
+ expect(result.properties[0].key).toBe("key")
|
|
|
|
|
+ expect(result.properties[0].value).toBe("value")
|
|
|
|
|
+ expect(result.properties[0].keyRange).toEqual([0, 3])
|
|
|
|
|
+ expect(result.properties[0].delimiterRange).toEqual([3, 5])
|
|
|
|
|
+ expect(result.properties[0].valueRange).toEqual([5, 11])
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("parses multiple tokens in one string", () => {
|
|
|
|
|
+ const result = parseMarkdown("**bold** and *italic*")
|
|
|
|
|
+ expect(result.content).toHaveLength(2)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("returns empty for plain text", () => {
|
|
|
|
|
+ const result = parseMarkdown("just plain text")
|
|
|
|
|
+ expect(result.content).toHaveLength(0)
|
|
|
|
|
+ expect(result.properties).toHaveLength(0)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("property with no value", () => {
|
|
|
|
|
+ const result = parseMarkdown("key::")
|
|
|
|
|
+ expect(result.properties).toHaveLength(1)
|
|
|
|
|
+ expect(result.properties[0].value).toBe("")
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("does not parse property mid-sentence", () => {
|
|
|
|
|
+ const result = parseMarkdown("some text key:: value")
|
|
|
|
|
+ expect(result.properties).toHaveLength(0)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("parses property only at start of text", () => {
|
|
|
|
|
+ const result = parseMarkdown("key:: value")
|
|
|
|
|
+ expect(result.properties).toHaveLength(1)
|
|
|
|
|
+ expect(result.properties[0].key).toBe("key")
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("does not parse property when text before it", () => {
|
|
|
|
|
+ const result = parseMarkdown("text key:: value")
|
|
|
|
|
+ expect(result.properties).toHaveLength(0)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("gracefully handles malformed markdown", () => {
|
|
|
|
|
+ const result = parseMarkdown("**unclosed")
|
|
|
|
|
+ expect(result.content).toHaveLength(0)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("gracefully handles nested malformed tokens", () => {
|
|
|
|
|
+ const result = parseMarkdown("**bold *italic**")
|
|
|
|
|
+ // Should not throw, returns what it can parse
|
|
|
|
|
+ expect(typeof result.content).toBe("object")
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("handles unmatched delimiters", () => {
|
|
|
|
|
+ const result = parseMarkdown("*no closing")
|
|
|
|
|
+ expect(result.content).toHaveLength(0)
|
|
|
|
|
+ })
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+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)
|
|
|
|
|
+ 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("shows highlight when cursor outside", () => {
|
|
|
|
|
+ const result = getActiveDecorations("^^highlight^^", 13)
|
|
|
|
|
+ expect(result.content).toHaveLength(1)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("hides highlight when cursor inside", () => {
|
|
|
|
|
+ const result = getActiveDecorations("^^highlight^^", 5)
|
|
|
|
|
+ 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("hides page ref when cursor inside", () => {
|
|
|
|
|
+ const result = getActiveDecorations("[[Page]]", 5)
|
|
|
|
|
+ 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("hides block ref when cursor inside", () => {
|
|
|
|
|
+ const result = getActiveDecorations("((block-id))", 5)
|
|
|
|
|
+ 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("hides tag when cursor inside", () => {
|
|
|
|
|
+ const result = getActiveDecorations("#tag", 2)
|
|
|
|
|
+ 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("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("unmatched block ref opening only", () => {
|
|
|
|
|
+ const result = parseMarkdown("((block")
|
|
|
|
|
+ expect(result.content).toHaveLength(0)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("unmatched tag no content", () => {
|
|
|
|
|
+ const result = parseMarkdown("#")
|
|
|
|
|
+ expect(result.content).toHaveLength(0)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("multiple unmatched delimiters", () => {
|
|
|
|
|
+ const result = parseMarkdown("**bold and *italic")
|
|
|
|
|
+ expect(result.content).toHaveLength(0)
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ describe("nested malformed tokens", () => {
|
|
|
|
|
+ it("italic containing unmatched bold - parses partial", () => {
|
|
|
|
|
+ const result = parseMarkdown("*text **bold*")
|
|
|
|
|
+ expect(result.content.length).toBeGreaterThanOrEqual(0)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("bold containing unmatched italic - parses partial", () => {
|
|
|
|
|
+ const result = parseMarkdown("**text *italic")
|
|
|
|
|
+ expect(result.content.length).toBeGreaterThanOrEqual(0)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("strikethrough containing unmatched bold - parses partial", () => {
|
|
|
|
|
+ const result = parseMarkdown("~~text **bold~~")
|
|
|
|
|
+ expect(result.content.length).toBeGreaterThanOrEqual(0)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("nested triple delimiters", () => {
|
|
|
|
|
+ const result = parseMarkdown("***text***")
|
|
|
|
|
+ expect(result.content.length).toBeGreaterThan(0)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("code inside bold", () => {
|
|
|
|
|
+ const result = parseMarkdown("**`code`**")
|
|
|
|
|
+ expect(result.content).toHaveLength(2)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("link inside bold", () => {
|
|
|
|
|
+ const result = parseMarkdown("**[text](url)**")
|
|
|
|
|
+ expect(result.content).toHaveLength(2)
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ describe("incomplete patterns at end of text", () => {
|
|
|
|
|
+ it("incomplete page ref at end", () => {
|
|
|
|
|
+ const result = parseMarkdown("text [[Page")
|
|
|
|
|
+ expect(result.content).toHaveLength(0)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("incomplete block ref at end", () => {
|
|
|
|
|
+ const result = parseMarkdown("text ((block")
|
|
|
|
|
+ expect(result.content).toHaveLength(0)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("incomplete highlight at end", () => {
|
|
|
|
|
+ const result = parseMarkdown("text ^^high")
|
|
|
|
|
+ expect(result.content).toHaveLength(0)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("tag at end of word not at boundary", () => {
|
|
|
|
|
+ const result = parseMarkdown("word#tag")
|
|
|
|
|
+ expect(result.content).toHaveLength(0)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("tag at boundary", () => {
|
|
|
|
|
+ const result = parseMarkdown("word #tag")
|
|
|
|
|
+ expect(result.content).toHaveLength(1)
|
|
|
|
|
+ expect(result.content[0].type).toBe("tag")
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ describe("whitespace variations", () => {
|
|
|
|
|
+ it("multiple spaces before tag", () => {
|
|
|
|
|
+ const result = parseMarkdown("text #tag")
|
|
|
|
|
+ expect(result.content).toHaveLength(1)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("tab before tag", () => {
|
|
|
|
|
+ const result = parseMarkdown("text\t#tag")
|
|
|
|
|
+ expect(result.content).toHaveLength(1)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ 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("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("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].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")
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ 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")
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ 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)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ describe("getActiveDecorations blocks", () => {
|
|
|
|
|
+ it("hides block markers when cursor inside block", () => {
|
|
|
|
|
+ const result = getActiveDecorations("TODO buy milk", 5)
|
|
|
|
|
+ expect(result.blocks).toHaveLength(0)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("shows block markers when cursor at start", () => {
|
|
|
|
|
+ const result = getActiveDecorations("TODO buy milk", 0)
|
|
|
|
|
+ expect(result.blocks).toHaveLength(1)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("shows heading when cursor at start", () => {
|
|
|
|
|
+ const result = getActiveDecorations("# Title", 0)
|
|
|
|
|
+ expect(result.blocks).toHaveLength(1)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("hides heading when cursor inside", () => {
|
|
|
|
|
+ const result = getActiveDecorations("# Title", 3)
|
|
|
|
|
+ expect(result.blocks).toHaveLength(0)
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+})
|