mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
fix(archive): normalize markdown links with mdast
This commit is contained in:
parent
05f9740e8e
commit
5b6fce1a4d
4 changed files with 294 additions and 90 deletions
190
package-lock.json
generated
190
package-lock.json
generated
|
|
@ -11,9 +11,12 @@
|
|||
"dependencies": {
|
||||
"@tanstack/query-core": "^5.101.2",
|
||||
"defuddle": "^0.19.1",
|
||||
"mdast-util-from-markdown": "^2.0.3",
|
||||
"mdast-util-to-markdown": "^2.1.2",
|
||||
"micromark": "^4.0.2",
|
||||
"obsidian-daily-notes-interface": "^0.9.5",
|
||||
"preact": "^10.29.7"
|
||||
"preact": "^10.29.7",
|
||||
"unist-util-visit": "^5.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.5.3",
|
||||
|
|
@ -21,6 +24,7 @@
|
|||
"@commitlint/config-conventional": "^21.2.0",
|
||||
"@stryker-mutator/core": "^9.6.1",
|
||||
"@stryker-mutator/vitest-runner": "^9.6.1",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"@types/node": "^26.1.1",
|
||||
"@vitest/coverage-v8": "^4.1.10",
|
||||
"concurrently": "^10.0.3",
|
||||
|
|
@ -3894,6 +3898,15 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/mdast": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
|
||||
"integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ms": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
||||
|
|
@ -3919,6 +3932,12 @@
|
|||
"@types/estree": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/unist": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
||||
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz",
|
||||
|
|
@ -8608,6 +8627,16 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/longest-streak": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
|
||||
"integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
|
|
@ -8689,6 +8718,78 @@
|
|||
"@xmldom/xmldom": "^0.9.10"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-from-markdown": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz",
|
||||
"integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"@types/unist": "^3.0.0",
|
||||
"decode-named-character-reference": "^1.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
"micromark": "^4.0.0",
|
||||
"micromark-util-decode-numeric-character-reference": "^2.0.0",
|
||||
"micromark-util-decode-string": "^2.0.0",
|
||||
"micromark-util-normalize-identifier": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0",
|
||||
"unist-util-stringify-position": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-phrasing": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
|
||||
"integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"unist-util-is": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-to-markdown": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
|
||||
"integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"@types/unist": "^3.0.0",
|
||||
"longest-streak": "^3.0.0",
|
||||
"mdast-util-phrasing": "^4.0.0",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
"micromark-util-classify-character": "^2.0.0",
|
||||
"micromark-util-decode-string": "^2.0.0",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"zwitch": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-to-string": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
|
||||
"integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdn-data": {
|
||||
"version": "2.27.1",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
|
||||
|
|
@ -8971,6 +9072,28 @@
|
|||
"micromark-util-symbol": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-util-decode-string": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz",
|
||||
"integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"decode-named-character-reference": "^1.0.0",
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-decode-numeric-character-reference": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-util-encode": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
|
||||
|
|
@ -11071,6 +11194,61 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-is": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
|
||||
"integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-stringify-position": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
|
||||
"integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-visit": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz",
|
||||
"integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0",
|
||||
"unist-util-is": "^6.0.0",
|
||||
"unist-util-visit-parents": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-visit-parents": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
|
||||
"integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0",
|
||||
"unist-util-is": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
||||
|
|
@ -11681,6 +11859,16 @@
|
|||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/zwitch": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
|
||||
"integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@
|
|||
"@commitlint/config-conventional": "^21.2.0",
|
||||
"@stryker-mutator/core": "^9.6.1",
|
||||
"@stryker-mutator/vitest-runner": "^9.6.1",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"@types/node": "^26.1.1",
|
||||
"@vitest/coverage-v8": "^4.1.10",
|
||||
"concurrently": "^10.0.3",
|
||||
|
|
@ -65,8 +66,11 @@
|
|||
"dependencies": {
|
||||
"@tanstack/query-core": "^5.101.2",
|
||||
"defuddle": "^0.19.1",
|
||||
"mdast-util-from-markdown": "^2.0.3",
|
||||
"mdast-util-to-markdown": "^2.1.2",
|
||||
"micromark": "^4.0.2",
|
||||
"obsidian-daily-notes-interface": "^0.9.5",
|
||||
"preact": "^10.29.7"
|
||||
"preact": "^10.29.7",
|
||||
"unist-util-visit": "^5.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,19 @@
|
|||
import type { Link, Nodes } from "mdast";
|
||||
import { fromMarkdown } from "mdast-util-from-markdown";
|
||||
import { toMarkdown } from "mdast-util-to-markdown";
|
||||
import { visit } from "unist-util-visit";
|
||||
|
||||
import { yamlFrontmatterInlineList, yamlFrontmatterString } from "../markdown/frontmatter";
|
||||
import { isExternalFileHref, parseFileHref } from "../vault/file-hrefs";
|
||||
import { parseFileHref } from "../vault/file-hrefs";
|
||||
import { isFilesystemAbsolutePath, isVaultConfigPath, normalizeFilePath, vaultRelativePath } from "../vault/paths";
|
||||
import type { Thread } from "./model";
|
||||
import { threadArchiveTitle } from "./title";
|
||||
import type { ThreadTranscriptEntry } from "./transcript";
|
||||
|
||||
interface ParsedMarkdownLink {
|
||||
raw: string;
|
||||
label: string;
|
||||
href: string;
|
||||
interface MarkdownSourceReplacement {
|
||||
start: number;
|
||||
end: number;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface ArchiveExportSettings {
|
||||
|
|
@ -31,49 +35,28 @@ export function archivedThreadMarkdown(
|
|||
): string {
|
||||
const title = threadArchiveTitle(thread);
|
||||
const tags = normalizedArchiveTags(settings?.archiveExportTags ?? "");
|
||||
const lines = [
|
||||
const frontmatter = [
|
||||
"---",
|
||||
`title: ${yamlFrontmatterString(title)}`,
|
||||
`thread_id: ${yamlFrontmatterString(thread.id)}`,
|
||||
`created: ${yamlFrontmatterString(formatDate(exportedAt))}`,
|
||||
...frontmatterTagsLines(tags),
|
||||
"---",
|
||||
"",
|
||||
`# ${title}`,
|
||||
"",
|
||||
...transcriptMarkdownLines(thread.transcriptEntries),
|
||||
];
|
||||
const markdown = `${trimTrailingBlankLines(lines).join("\n")}\n`;
|
||||
return settings?.vaultPath ? normalizeExportedMarkdownLinks(markdown, settings.vaultPath, settings.vaultConfigDir) : markdown;
|
||||
].join("\n");
|
||||
const body = `${trimTrailingBlankLines([`# ${title}`, "", ...transcriptMarkdownLines(thread.transcriptEntries)]).join("\n")}\n`;
|
||||
const normalizedBody = settings?.vaultPath ? normalizeExportedMarkdownLinks(body, settings.vaultPath, settings.vaultConfigDir) : body;
|
||||
return `${frontmatter}\n\n${normalizedBody}`;
|
||||
}
|
||||
|
||||
function normalizeExportedMarkdownLinks(markdown: string, vaultPath: string, vaultConfigDir: string | null | undefined): string {
|
||||
const lines = markdown.split("\n");
|
||||
let inFence = false;
|
||||
return lines
|
||||
.map((line) => {
|
||||
if (line.trimStart().startsWith("```")) {
|
||||
inFence = !inFence;
|
||||
return line;
|
||||
}
|
||||
return inFence ? line : normalizeExportedMarkdownLinksInLine(line, vaultPath, vaultConfigDir);
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function normalizeExportedMarkdownLinksInLine(line: string, vaultPath: string, vaultConfigDir: string | null | undefined): string {
|
||||
let output = "";
|
||||
let index = 0;
|
||||
while (index < line.length) {
|
||||
const parsed = parseMarkdownLinkAt(line, index);
|
||||
if (!parsed || isInsideInlineCode(line, index)) {
|
||||
output += line[index] ?? "";
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
output += normalizedExportedMarkdownLink(parsed, vaultPath, vaultConfigDir);
|
||||
index = parsed.end;
|
||||
const replacements: MarkdownSourceReplacement[] = [];
|
||||
visit(fromMarkdown(markdown), "link", (link) => {
|
||||
const replacement = normalizedExportedMarkdownLink(link, vaultPath, vaultConfigDir);
|
||||
if (replacement) replacements.push(replacement);
|
||||
});
|
||||
let output = markdown;
|
||||
for (const replacement of replacements.sort((left, right) => right.start - left.start)) {
|
||||
output = `${output.slice(0, replacement.start)}${replacement.value}${output.slice(replacement.end)}`;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
|
@ -163,67 +146,45 @@ function stripMatchingQuotes(value: string): string {
|
|||
return (first === `"` || first === `'`) && first === last ? value.slice(1, -1) : value;
|
||||
}
|
||||
|
||||
function unwrappedMarkdownHref(value: string): string {
|
||||
if (value.startsWith("<") && value.endsWith(">")) return value.slice(1, -1);
|
||||
return value;
|
||||
}
|
||||
function normalizedExportedMarkdownLink(
|
||||
link: Link,
|
||||
vaultPath: string,
|
||||
vaultConfigDir: string | null | undefined,
|
||||
): MarkdownSourceReplacement | null {
|
||||
const start = link.position?.start.offset;
|
||||
const end = link.position?.end.offset;
|
||||
if (start === undefined || end === undefined || !link.url) return null;
|
||||
|
||||
function parseMarkdownLinkAt(line: string, start: number): ParsedMarkdownLink | null {
|
||||
if (line[start] !== "[" || line[start - 1] === "!") return null;
|
||||
const parsed = parseFileHref(link.url);
|
||||
if (!parsed) return null;
|
||||
const vaultRelative = vaultRelativePath(vaultPath, parsed.path);
|
||||
if (vaultRelative && !archiveExportShouldKeepAbsolute(vaultRelative, vaultConfigDir)) {
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
value: markdownNodeSource({ ...link, url: `${vaultRelative}${parsed.subpath}` }),
|
||||
};
|
||||
}
|
||||
|
||||
const labelEnd = line.indexOf("]", start + 1);
|
||||
if (labelEnd === -1 || line[labelEnd + 1] !== "(") return null;
|
||||
|
||||
const hrefStart = labelEnd + 2;
|
||||
const hrefEnd = line[hrefStart] === "<" ? line.indexOf(">)", hrefStart + 1) : line.indexOf(")", hrefStart);
|
||||
if (hrefEnd === -1) return null;
|
||||
|
||||
const end = line[hrefStart] === "<" ? hrefEnd + 2 : hrefEnd + 1;
|
||||
if (!isFilesystemAbsolutePath(normalizeFilePath(parsed.path))) return null;
|
||||
return {
|
||||
raw: line.slice(start, end),
|
||||
label: line.slice(start + 1, labelEnd),
|
||||
href: line.slice(hrefStart, hrefEnd + (line[hrefStart] === "<" ? 1 : 0)),
|
||||
start,
|
||||
end,
|
||||
value: markdownNodeSource({
|
||||
type: "paragraph",
|
||||
children: [...link.children, { type: "text", value: " (" }, { type: "inlineCode", value: link.url }, { type: "text", value: ")" }],
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizedExportedMarkdownLink(link: ParsedMarkdownLink, vaultPath: string, vaultConfigDir: string | null | undefined): string {
|
||||
const href = unwrappedMarkdownHref(link.href.trim());
|
||||
if (!href || isExternalFileHref(href)) return link.raw;
|
||||
|
||||
const parsed = parseFileHref(href);
|
||||
if (!parsed) return link.raw;
|
||||
const vaultRelative = vaultRelativePath(vaultPath, parsed.path);
|
||||
if (vaultRelative && !archiveExportShouldKeepAbsolute(vaultRelative, vaultConfigDir)) {
|
||||
return `[${link.label}](${markdownHref(`${vaultRelative}${parsed.subpath}`)})`;
|
||||
}
|
||||
|
||||
return isFilesystemAbsolutePath(normalizeFilePath(parsed.path)) ? `${link.label} (${markdownCodeSpan(href)})` : link.raw;
|
||||
function markdownNodeSource(node: Nodes): string {
|
||||
return toMarkdown(node, { unsafe: [{ character: "|", inConstruct: ["label", "phrasing"] }] }).trimEnd();
|
||||
}
|
||||
|
||||
function archiveExportShouldKeepAbsolute(vaultRelativePath: string, vaultConfigDir: string | null | undefined): boolean {
|
||||
return typeof vaultConfigDir === "string" && vaultConfigDir.length > 0 && isVaultConfigPath(vaultRelativePath, vaultConfigDir);
|
||||
}
|
||||
|
||||
function markdownHref(value: string): string {
|
||||
return /[\s()]/.test(value) ? `<${value}>` : value;
|
||||
}
|
||||
|
||||
function markdownCodeSpan(value: string): string {
|
||||
let fenceLength = 1;
|
||||
for (const match of value.matchAll(/`+/g)) fenceLength = Math.max(fenceLength, match[0].length + 1);
|
||||
const fence = "`".repeat(fenceLength);
|
||||
return fenceLength === 1 ? `${fence}${value}${fence}` : `${fence} ${value} ${fence}`;
|
||||
}
|
||||
|
||||
function isInsideInlineCode(line: string, offset: number): boolean {
|
||||
let inCode = false;
|
||||
for (let index = 0; index < offset; index += 1) {
|
||||
if (line[index] === "`") inCode = !inCode;
|
||||
}
|
||||
return inCode;
|
||||
}
|
||||
|
||||
function formatDate(date: Date): string {
|
||||
return `${String(date.getFullYear())}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -187,6 +187,8 @@ describe("thread archive export", () => {
|
|||
[
|
||||
"[Vault note](</Users/showhey/Vault/topics/My Note.md>)",
|
||||
"[Vault note with parens](</Users/showhey/Vault/topics/My (Note).md>)",
|
||||
"[Vault note with bare parens](/Users/showhey/Vault/topics/My_(Note).md)",
|
||||
'[**Titled vault note**](/Users/showhey/Vault/topics/Titled.md "Reference")',
|
||||
"[Vault source line](/Users/showhey/Vault/src/main.ts:12:4#L12)",
|
||||
"[Vault config](/Users/showhey/Vault/vault-config/plugins/codex-panel/main.js)",
|
||||
"[External file](/Users/showhey/Repos/project/README.md)",
|
||||
|
|
@ -195,9 +197,13 @@ describe("thread archive export", () => {
|
|||
"[Website](https://example.com/docs)",
|
||||
"",
|
||||
"`[Code link](/Users/showhey/Repos/project/README.md)`",
|
||||
"``[Double-fenced code link](/Users/showhey/Repos/project/README.md)``",
|
||||
"```",
|
||||
"[Code block link](/Users/showhey/Repos/project/README.md)",
|
||||
"```",
|
||||
"~~~",
|
||||
"[Tilde code block link](/Users/showhey/Repos/project/README.md)",
|
||||
"~~~",
|
||||
].join("\n"),
|
||||
1,
|
||||
),
|
||||
|
|
@ -211,17 +217,23 @@ describe("thread archive export", () => {
|
|||
[
|
||||
"[Vault note](<topics/My Note.md>)",
|
||||
"[Vault note with parens](<topics/My (Note).md>)",
|
||||
"[Vault note with bare parens](topics/My_\\(Note\\).md)",
|
||||
'[**Titled vault note**](topics/Titled.md "Reference")',
|
||||
"[Vault source line](src/main.ts#L12)",
|
||||
"Vault config (`/Users/showhey/Vault/vault-config/plugins/codex-panel/main.js`)",
|
||||
"External file (`/Users/showhey/Repos/project/README.md`)",
|
||||
"External file with backticks (``` /Users/showhey/Repos/project/a\\`b``c.md ```)",
|
||||
"External file with backticks (```/Users/showhey/Repos/project/a`b``c.md```)",
|
||||
"[Relative](topics/Other.md)",
|
||||
"[Website](https://example.com/docs)",
|
||||
"",
|
||||
"`[Code link](/Users/showhey/Repos/project/README.md)`",
|
||||
"``[Double-fenced code link](/Users/showhey/Repos/project/README.md)``",
|
||||
"```",
|
||||
"[Code block link](/Users/showhey/Repos/project/README.md)",
|
||||
"```",
|
||||
"~~~",
|
||||
"[Tilde code block link](/Users/showhey/Repos/project/README.md)",
|
||||
"~~~",
|
||||
].join("\n"),
|
||||
);
|
||||
});
|
||||
|
|
@ -244,6 +256,45 @@ describe("thread archive export", () => {
|
|||
expect(output).toContain("[Vault](topics/Alpha.md)");
|
||||
expect(output).toContain("External (`/Users/showhey/Repos/project/README.md`)");
|
||||
});
|
||||
|
||||
it("preserves Obsidian syntax outside normalized links", () => {
|
||||
const obsidianSyntax = [
|
||||
"[[Wiki note]]",
|
||||
"![[image.png]]",
|
||||
"> [!note] Callout",
|
||||
"> body",
|
||||
"#tag/path",
|
||||
"==highlight==",
|
||||
"%% comment %%",
|
||||
"- [x] task",
|
||||
].join("\n");
|
||||
const output = exportedMarkdown(
|
||||
thread({ transcriptEntries: [transcriptEntry("assistant", `${obsidianSyntax}\n[Vault](/Users/showhey/Vault/Note.md)`, 1)] }),
|
||||
new Date(2026, 4, 18),
|
||||
{ vaultPath: "/Users/showhey/Vault" },
|
||||
);
|
||||
|
||||
expect(output).toContain(obsidianSyntax);
|
||||
expect(output).toContain("[Vault](Note.md)");
|
||||
});
|
||||
|
||||
it("preserves escaped pipes in table link labels", () => {
|
||||
const output = exportedMarkdown(
|
||||
thread({
|
||||
transcriptEntries: [
|
||||
transcriptEntry(
|
||||
"assistant",
|
||||
"| Vault | External |\n| --- | --- |\n| [A\\|B](/Users/showhey/Vault/Note.md) | [C\\|D](/Outside/Note.md) |",
|
||||
1,
|
||||
),
|
||||
],
|
||||
}),
|
||||
new Date(2026, 4, 18),
|
||||
{ vaultPath: "/Users/showhey/Vault" },
|
||||
);
|
||||
|
||||
expect(output).toContain("| [A\\|B](Note.md) | C\\|D (`/Outside/Note.md`) |");
|
||||
});
|
||||
});
|
||||
|
||||
function exportedMarkdown(
|
||||
|
|
|
|||
Loading…
Reference in a new issue