Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 35 additions & 25 deletions packages/core/src/renderables/Markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export class MarkdownRenderable extends Renderable {
return style
}

private createChunk(text: string, group: string): TextChunk {
private createChunk(text: string, group: string, link?: { url: string }): TextChunk {
const style = this.getStyle(group) || this.getStyle("default")
return {
__isChunk: true,
Expand All @@ -152,6 +152,7 @@ export class MarkdownRenderable extends Renderable {
dim: style.dim,
})
: 0,
link,
}
}

Expand Down Expand Up @@ -221,36 +222,40 @@ export class MarkdownRenderable extends Renderable {
}
break

case "link":
case "link": {
const linkHref = { url: token.href }
if (this._conceal) {
for (const child of token.tokens) {
this.renderInlineTokenWithStyle(child as MarkedToken, chunks, "markup.link.label")
this.renderInlineTokenWithStyle(child as MarkedToken, chunks, "markup.link.label", linkHref)
}
chunks.push(this.createChunk(" (", "markup.link"))
chunks.push(this.createChunk(token.href, "markup.link.url"))
chunks.push(this.createChunk(")", "markup.link"))
chunks.push(this.createChunk(" (", "markup.link", linkHref))
chunks.push(this.createChunk(token.href, "markup.link.url", linkHref))
chunks.push(this.createChunk(")", "markup.link", linkHref))
} else {
chunks.push(this.createChunk("[", "markup.link"))
chunks.push(this.createChunk("[", "markup.link", linkHref))
for (const child of token.tokens) {
this.renderInlineTokenWithStyle(child as MarkedToken, chunks, "markup.link.label")
this.renderInlineTokenWithStyle(child as MarkedToken, chunks, "markup.link.label", linkHref)
}
chunks.push(this.createChunk("](", "markup.link"))
chunks.push(this.createChunk(token.href, "markup.link.url"))
chunks.push(this.createChunk(")", "markup.link"))
chunks.push(this.createChunk("](", "markup.link", linkHref))
chunks.push(this.createChunk(token.href, "markup.link.url", linkHref))
chunks.push(this.createChunk(")", "markup.link", linkHref))
}
break
}

case "image":
case "image": {
const imageHref = { url: token.href }
if (this._conceal) {
chunks.push(this.createChunk(token.text || "image", "markup.link.label"))
chunks.push(this.createChunk(token.text || "image", "markup.link.label", imageHref))
} else {
chunks.push(this.createChunk("![", "markup.link"))
chunks.push(this.createChunk(token.text || "", "markup.link.label"))
chunks.push(this.createChunk("](", "markup.link"))
chunks.push(this.createChunk(token.href, "markup.link.url"))
chunks.push(this.createChunk(")", "markup.link"))
chunks.push(this.createChunk("![", "markup.link", imageHref))
chunks.push(this.createChunk(token.text || "", "markup.link.label", imageHref))
chunks.push(this.createChunk("](", "markup.link", imageHref))
chunks.push(this.createChunk(token.href, "markup.link.url", imageHref))
chunks.push(this.createChunk(")", "markup.link", imageHref))
}
break
}

case "br":
chunks.push(this.createDefaultChunk("\n"))
Expand All @@ -266,23 +271,28 @@ export class MarkdownRenderable extends Renderable {
}
}

private renderInlineTokenWithStyle(token: MarkedToken, chunks: TextChunk[], styleGroup: string): void {
private renderInlineTokenWithStyle(
token: MarkedToken,
chunks: TextChunk[],
styleGroup: string,
link?: { url: string },
): void {
switch (token.type) {
case "text":
chunks.push(this.createChunk(token.text, styleGroup))
chunks.push(this.createChunk(token.text, styleGroup, link))
break

case "escape":
chunks.push(this.createChunk(token.text, styleGroup))
chunks.push(this.createChunk(token.text, styleGroup, link))
break

case "codespan":
if (this._conceal) {
chunks.push(this.createChunk(token.text, "markup.raw"))
chunks.push(this.createChunk(token.text, "markup.raw", link))
} else {
chunks.push(this.createChunk("`", "markup.raw"))
chunks.push(this.createChunk(token.text, "markup.raw"))
chunks.push(this.createChunk("`", "markup.raw"))
chunks.push(this.createChunk("`", "markup.raw", link))
chunks.push(this.createChunk(token.text, "markup.raw", link))
chunks.push(this.createChunk("`", "markup.raw", link))
}
break

Expand Down
75 changes: 75 additions & 0 deletions packages/core/src/renderables/__tests__/Markdown.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { test, expect, beforeEach, afterEach } from "bun:test"
import { MarkdownRenderable } from "../Markdown"
import { TextRenderable } from "../Text"
import { SyntaxStyle } from "../../syntax-style"
import { RGBA } from "../../lib/RGBA"
import { createTestRenderer, type TestRenderer } from "../../testing"
Expand Down Expand Up @@ -1703,3 +1704,77 @@ The table alignment uses:
}
expect(renderTime).toBeLessThan(10)
})

// OSC 8 link metadata tests

test("link chunks include link metadata for OSC 8 hyperlinks (conceal=true)", async () => {
const md = new MarkdownRenderable(renderer, {
id: "markdown",
content: "Check [Google](https://google.com) out",
syntaxStyle,
conceal: true,
})

renderer.root.add(md)
await renderOnce()

const textRenderable = md._blockStates[0]?.renderable as TextRenderable
const chunks = textRenderable.content.chunks
const linkChunks = chunks.filter((c) => c.link?.url === "https://google.com")

expect(linkChunks.length).toBeGreaterThan(0)
expect(linkChunks.some((c) => c.text === "Google")).toBe(true)
expect(linkChunks.some((c) => c.text === "https://google.com")).toBe(true)
})

test("link chunks include link metadata (conceal=false)", async () => {
const md = new MarkdownRenderable(renderer, {
id: "markdown",
content: "Check [Google](https://google.com) out",
syntaxStyle,
conceal: false,
})

renderer.root.add(md)
await renderOnce()

const textRenderable = md._blockStates[0]?.renderable as TextRenderable
const chunks = textRenderable.content.chunks
const linkChunks = chunks.filter((c) => c.link?.url === "https://google.com")

expect(linkChunks.length).toBeGreaterThan(0)
expect(linkChunks.some((c) => c.text === "Google")).toBe(true)
expect(linkChunks.some((c) => c.text === "https://google.com")).toBe(true)
})

test("image chunks include link metadata", async () => {
const md = new MarkdownRenderable(renderer, {
id: "markdown",
content: "![alt](https://example.com/img.png)",
syntaxStyle,
conceal: true,
})

renderer.root.add(md)
await renderOnce()

const textRenderable = md._blockStates[0]?.renderable as TextRenderable
const chunks = textRenderable.content.chunks
const linkChunks = chunks.filter((c) => c.link?.url === "https://example.com/img.png")
expect(linkChunks.length).toBeGreaterThan(0)
})

test("non-link text does not have link metadata", async () => {
const md = new MarkdownRenderable(renderer, {
id: "markdown",
content: "No links here, just **bold** text.",
syntaxStyle,
})

renderer.root.add(md)
await renderOnce()

const textRenderable = md._blockStates[0]?.renderable as TextRenderable
const chunks = textRenderable.content.chunks
expect(chunks.every((c) => !c.link)).toBe(true)
})
Loading