Skip to content
Open
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
3 changes: 3 additions & 0 deletions quartz/processors/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { QuartzLogger } from "../util/log"
import { trace } from "../util/trace"
import { BuildCtx, WorkerSerializableBuildCtx } from "../util/ctx"
import { styleText } from "util"
import { stripCalloutTitleContinuationsInMarkdown } from "../util/calloutTitle"

export type QuartzMdProcessor = Processor<MDRoot, MDRoot, MDRoot>
export type QuartzHtmlProcessor = Processor<undefined, MDRoot, HTMLRoot>
Expand Down Expand Up @@ -99,6 +100,8 @@ export function createFileParser(ctx: BuildCtx, fps: FilePath[]) {
file.value = plugin.textTransform!(ctx, file.value.toString())
}

file.value = stripCalloutTitleContinuationsInMarkdown(file.value.toString())

// base data properties that plugins may use
file.data.filePath = file.path as FilePath
file.data.relativePath = path.posix.relative(argv.directory, file.path) as FilePath
Expand Down
53 changes: 53 additions & 0 deletions quartz/util/calloutTitle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import assert from "node:assert"
import test, { describe } from "node:test"
import {
stripCalloutTitleContinuation,
stripCalloutTitleContinuationsInMarkdown,
} from "./calloutTitle"

describe("stripCalloutTitleContinuation", () => {
test("drops a trailing continuation backslash", () => {
assert.equal(stripCalloutTitleContinuation(" 提示\\"), " 提示")
assert.equal(stripCalloutTitleContinuation("提示\\"), "提示")
assert.equal(stripCalloutTitleContinuation("\\"), "")
})

test("leaves titles without a trailing backslash alone", () => {
assert.equal(stripCalloutTitleContinuation(" 提示"), " 提示")
assert.equal(stripCalloutTitleContinuation("C:\\Windows"), "C:\\Windows")
})
})

describe("stripCalloutTitleContinuationsInMarkdown", () => {
test("strips INFO title continuation and keeps the body line", () => {
const src = "> [!INFO] 提示\\\n> 这里是正文\n"
const out = stripCalloutTitleContinuationsInMarkdown(src)
assert.equal(out, "> [!INFO] 提示\n> 这里是正文\n")
})

test("strips foldable and nested openers", () => {
assert.equal(
stripCalloutTitleContinuationsInMarkdown("> [!TIP]- 标题\\"),
"> [!TIP]- 标题",
)
assert.equal(
stripCalloutTitleContinuationsInMarkdown("> > [!NOTE] x\\"),
"> > [!NOTE] x",
)
})

test("does not touch ordinary quotes or mid-title backslashes", () => {
assert.equal(
stripCalloutTitleContinuationsInMarkdown("> quote\\"),
"> quote\\",
)
assert.equal(
stripCalloutTitleContinuationsInMarkdown("> [!NOTE] C:\\Windows"),
"> [!NOTE] C:\\Windows",
)
})

test("empty title leftover slash becomes default-title case", () => {
assert.equal(stripCalloutTitleContinuationsInMarkdown("> [!INFO]\\"), "> [!INFO]")
})
})
23 changes: 23 additions & 0 deletions quartz/util/calloutTitle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export function stripCalloutTitleContinuation(title: string): string {
if (!/(?:\\[ \t]*)+$/.test(title)) {
return title
}
return title.replace(/(?:\\[ \t]*)+$/u, "").replace(/[ \t]+$/u, "")
}

const CALLOUT_OPEN = /^((?:>[ \t]*)+\[![\w-]+(?:\|[^\]]+)?\][+-]?)(.*)$/

export function stripCalloutTitleContinuationsInMarkdown(src: string): string {
const newline = src.includes("\r\n") ? "\r\n" : src.includes("\r") ? "\r" : "\n"
return src
.split(/\r?\n/)
.map((line) => {
const match = line.match(CALLOUT_OPEN)
if (!match) {
return line
}
const [, directive, rest] = match
return `${directive}${stripCalloutTitleContinuation(rest ?? "")}`
})
.join(newline)
}