mirror of
https://github.com/vicky469/aside.git
synced 2026-07-22 07:01:57 +00:00
404 lines
15 KiB
TypeScript
404 lines
15 KiB
TypeScript
import * as assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import {
|
|
ALL_COMMENTS_NOTE_PATH,
|
|
LEGACY_ALL_COMMENTS_NOTE_PATH,
|
|
ALL_COMMENTS_NOTE_IMAGE_ALT,
|
|
ALL_COMMENTS_NOTE_IMAGE_CAPTION,
|
|
ALL_COMMENTS_NOTE_IMAGE_URL,
|
|
buildAllCommentsNoteContent,
|
|
buildCommentLocationLineNumberMap,
|
|
buildIndexCommentBlockId,
|
|
buildIndexNoteNavigationMap,
|
|
buildCommentLocationUrl,
|
|
findCommentLocationLineNumber,
|
|
findCommentLocationTargetInMarkdownLine,
|
|
findFileHeadingPathInMarkdownLine,
|
|
findIndexMarkdownLineTarget,
|
|
isAllCommentsNotePath,
|
|
normalizeAllCommentsNoteImageCaption,
|
|
normalizeAllCommentsNoteImageUrl,
|
|
normalizeAllCommentsNotePath,
|
|
parseCommentLocationUrl,
|
|
parseIndexFileOpenUrl,
|
|
} from "../src/core/derived/allCommentsNote";
|
|
import type { Comment, CommentThread } from "../src/commentManager";
|
|
|
|
function createComment(overrides: Partial<Comment> = {}): Comment {
|
|
return {
|
|
id: "comment-1",
|
|
filePath: "Folder/Note.md",
|
|
startLine: 4,
|
|
startChar: 2,
|
|
endLine: 4,
|
|
endChar: 7,
|
|
selectedText: "hello",
|
|
selectedTextHash: "hash-1",
|
|
comment: "This is a side note.",
|
|
timestamp: 1710000000000,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createThread(overrides: Partial<CommentThread> = {}): CommentThread {
|
|
return {
|
|
id: "thread-1",
|
|
filePath: "Folder/Note.md",
|
|
startLine: 4,
|
|
startChar: 2,
|
|
endLine: 4,
|
|
endChar: 7,
|
|
selectedText: "hello",
|
|
selectedTextHash: "hash-1",
|
|
entries: [{
|
|
id: "entry-1",
|
|
body: "This is a side note.",
|
|
timestamp: 1710000000000,
|
|
}],
|
|
createdAt: 1710000000000,
|
|
updatedAt: 1710000000000,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function escapeHtmlText(value: string): string {
|
|
return value
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """);
|
|
}
|
|
|
|
function expectedFileRow(filePath: string): string {
|
|
const fileName = filePath.split("/").pop() ?? filePath;
|
|
const escapedPath = escapeHtmlText(filePath);
|
|
return `- <a href="#" class="aside-index-file-filter-link aside-index-heading-label" title="${escapedPath}" data-aside-file-path="${escapedPath}">${escapeHtmlText(fileName)}</a>`;
|
|
}
|
|
|
|
function countOccurrences(value: string, needle: string): number {
|
|
return value.split(needle).length - 1;
|
|
}
|
|
|
|
test("buildCommentLocationUrl encodes vault, file, and comment id", () => {
|
|
const url = buildCommentLocationUrl("dev vault", createComment({
|
|
filePath: "Folder/My Note.md",
|
|
id: "comment 1",
|
|
}));
|
|
|
|
assert.equal(
|
|
url,
|
|
"obsidian://aside-comment?vault=dev%20vault&file=Folder%2FMy%20Note.md&commentId=comment%201"
|
|
);
|
|
});
|
|
|
|
test("parseCommentLocationUrl extracts file path and comment id", () => {
|
|
assert.deepEqual(
|
|
parseCommentLocationUrl("obsidian://aside-comment?vault=dev&file=Folder%2FMy%20Note.md&commentId=comment%201"),
|
|
{
|
|
filePath: "Folder/My Note.md",
|
|
commentId: "comment 1",
|
|
},
|
|
);
|
|
assert.equal(parseCommentLocationUrl("obsidian://open?vault=dev&file=Folder%2FMy%20Note.md"), null);
|
|
});
|
|
|
|
test("findCommentLocationTargetInMarkdownLine finds the side note target in a generated markdown list row", () => {
|
|
assert.deepEqual(
|
|
findCommentLocationTargetInMarkdownLine('<span class="aside-index-kind-dot aside-index-kind-page"></span> [hello](obsidian://aside-comment?vault=dev&file=Folder%2FNote.md&commentId=comment-1&kind=page) #hi'),
|
|
{
|
|
filePath: "Folder/Note.md",
|
|
commentId: "comment-1",
|
|
},
|
|
);
|
|
assert.equal(findCommentLocationTargetInMarkdownLine("**Folder/Note.md**"), null);
|
|
});
|
|
|
|
test("findFileHeadingPathInMarkdownLine extracts the source file path from an index heading row", () => {
|
|
assert.equal(
|
|
findFileHeadingPathInMarkdownLine('##### <span class="aside-index-heading-label" title="Folder/Note.md">Note.md</span>'),
|
|
"Folder/Note.md",
|
|
);
|
|
assert.equal(
|
|
findFileHeadingPathInMarkdownLine('##### <strong class="aside-index-heading-label" title="Folder/Legacy.md">Legacy.md</strong>'),
|
|
"Folder/Legacy.md",
|
|
);
|
|
assert.equal(
|
|
findFileHeadingPathInMarkdownLine('- <a class="aside-index-heading-label" title="Folder/Linked.md" href="obsidian://open?vault=dev&file=Folder%2FLinked.md">Linked.md</a>'),
|
|
"Folder/Linked.md",
|
|
);
|
|
assert.equal(
|
|
findFileHeadingPathInMarkdownLine('- [Linked.md](obsidian://open?vault=dev&file=Folder%2FLinked.md)'),
|
|
"Folder/Linked.md",
|
|
);
|
|
assert.equal(findFileHeadingPathInMarkdownLine("### Folder"), null);
|
|
});
|
|
|
|
test("parseIndexFileOpenUrl extracts file paths from generated file links", () => {
|
|
assert.equal(
|
|
parseIndexFileOpenUrl("obsidian://open?vault=dev&file=Folder%2FLinked.md"),
|
|
"Folder/Linked.md",
|
|
);
|
|
assert.equal(parseIndexFileOpenUrl("obsidian://aside-comment?vault=dev&file=Folder%2FLinked.md"), null);
|
|
});
|
|
|
|
test("findIndexMarkdownLineTarget resolves both comment rows and file heading rows", () => {
|
|
assert.deepEqual(
|
|
findIndexMarkdownLineTarget('<span class="aside-index-kind-dot aside-index-kind-page"></span> [hello](obsidian://aside-comment?vault=dev&file=Folder%2FNote.md&commentId=comment-1&kind=page) #hi'),
|
|
{
|
|
kind: "comment",
|
|
filePath: "Folder/Note.md",
|
|
commentId: "comment-1",
|
|
},
|
|
);
|
|
assert.deepEqual(
|
|
findIndexMarkdownLineTarget('##### <span class="aside-index-heading-label" title="Folder/Note.md">Note.md</span>'),
|
|
{
|
|
kind: "file",
|
|
filePath: "Folder/Note.md",
|
|
},
|
|
);
|
|
});
|
|
|
|
test("findCommentLocationLineNumber returns null for the simplified generated index", () => {
|
|
const content = buildAllCommentsNoteContent("dev", [
|
|
createComment({
|
|
id: "comment-1",
|
|
filePath: "Folder/Note.md",
|
|
}),
|
|
]);
|
|
|
|
assert.equal(findCommentLocationLineNumber(content, "comment-1"), null);
|
|
});
|
|
|
|
test("buildCommentLocationLineNumberMap still indexes legacy comment rows", () => {
|
|
const content = [
|
|
'<span class="aside-index-kind-dot aside-index-kind-anchored"></span> [alpha](obsidian://aside-comment?vault=dev&file=Folder%2FNote.md&commentId=comment-1&kind=anchored)',
|
|
'<span class="aside-index-kind-dot aside-index-kind-anchored"></span> [beta](obsidian://aside-comment?vault=dev&file=Folder%2FOther.md&commentId=comment-2&kind=anchored)',
|
|
].join("\n");
|
|
|
|
const lineNumbersByCommentId = buildCommentLocationLineNumberMap(content);
|
|
|
|
assert.equal(lineNumbersByCommentId.get("comment-1"), 0);
|
|
assert.equal(lineNumbersByCommentId.get("comment-2"), 1);
|
|
assert.equal(lineNumbersByCommentId.get("missing-comment"), undefined);
|
|
});
|
|
|
|
test("buildIndexNoteNavigationMap tracks simplified file link rows", () => {
|
|
const content = buildAllCommentsNoteContent("dev", [
|
|
createComment({
|
|
id: "comment-1",
|
|
filePath: "Projects/Alpha/Note A.md",
|
|
}),
|
|
createComment({
|
|
id: "comment-2",
|
|
filePath: "Projects/Alpha/Note B.md",
|
|
}),
|
|
]);
|
|
|
|
const navigationMap = buildIndexNoteNavigationMap(content);
|
|
|
|
assert.equal(navigationMap.fileLineByFilePath.get("Projects/Alpha/Note A.md") === undefined, false);
|
|
assert.equal(navigationMap.fileLineByFilePath.get("Projects/Alpha/Note B.md") === undefined, false);
|
|
assert.equal(navigationMap.targetsByCommentId.size, 0);
|
|
assert.equal(content.split("\n")[navigationMap.fileLineByFilePath.get("Projects/Alpha/Note A.md") ?? -1] ?? "", expectedFileRow("Projects/Alpha/Note A.md"));
|
|
assert.equal(content.split("\n")[navigationMap.fileLineByFilePath.get("Projects/Alpha/Note B.md") ?? -1] ?? "", expectedFileRow("Projects/Alpha/Note B.md"));
|
|
});
|
|
|
|
test("buildIndexCommentBlockId normalizes comment ids into stable block ids", () => {
|
|
assert.equal(
|
|
buildIndexCommentBlockId("Comment 01/alpha"),
|
|
"aside-index-comment-comment-01-alpha",
|
|
);
|
|
});
|
|
|
|
test("buildAllCommentsNoteContent lists unique files grouped by folder", () => {
|
|
const content = buildAllCommentsNoteContent("dev", [
|
|
createComment({
|
|
id: "comment-1",
|
|
filePath: "Projects/Alpha/Note B.md",
|
|
}),
|
|
createComment({
|
|
id: "comment-2",
|
|
filePath: "Projects/Alpha/Note A.md",
|
|
}),
|
|
createComment({
|
|
id: "comment-3",
|
|
filePath: "Projects/Alpha/Note A.md",
|
|
}),
|
|
]);
|
|
|
|
assert.equal(content.match(/^Projects\/Alpha$/gm)?.length ?? 0, 1);
|
|
assert.equal(countOccurrences(content, expectedFileRow("Projects/Alpha/Note A.md")), 1);
|
|
assert.equal(countOccurrences(content, expectedFileRow("Projects/Alpha/Note B.md")), 1);
|
|
assert.equal(content.includes(`Projects/Alpha\n${expectedFileRow("Projects/Alpha/Note A.md")}\n\n${expectedFileRow("Projects/Alpha/Note B.md")}`), true);
|
|
assert.doesNotMatch(content, /commentId=/);
|
|
assert.doesNotMatch(content, /aside-index-kind-dot/);
|
|
});
|
|
|
|
test("buildAllCommentsNoteContent renders deduped source-file tags", () => {
|
|
const content = buildAllCommentsNoteContent("dev", [
|
|
createComment({
|
|
id: "comment-1",
|
|
filePath: "Projects/Alpha/Note A.md",
|
|
comment: "Needs #Review and #follow/up.",
|
|
}),
|
|
createComment({
|
|
id: "comment-2",
|
|
filePath: "Projects/Alpha/Note A.md",
|
|
comment: "Duplicate #review plus #idea.",
|
|
}),
|
|
createComment({
|
|
id: "comment-3",
|
|
filePath: "Projects/Alpha/Note B.md",
|
|
comment: "No tags here.",
|
|
}),
|
|
]);
|
|
|
|
assert.equal(
|
|
content.includes(`${expectedFileRow("Projects/Alpha/Note A.md")} #follow/up #idea #Review`),
|
|
true,
|
|
);
|
|
assert.equal(content.includes(`${expectedFileRow("Projects/Alpha/Note A.md")}\n #follow/up`), false);
|
|
assert.equal(content.includes("Tags:"), false);
|
|
});
|
|
|
|
test("buildAllCommentsNoteContent includes tags from every visible thread entry", () => {
|
|
const content = buildAllCommentsNoteContent("dev", [
|
|
createThread({
|
|
id: "thread-1",
|
|
filePath: "Projects/Alpha/Note A.md",
|
|
entries: [
|
|
{
|
|
id: "entry-1",
|
|
body: "Root #root",
|
|
timestamp: 1710000000000,
|
|
},
|
|
{
|
|
id: "entry-2",
|
|
body: "Reply #reply",
|
|
timestamp: 1710000001000,
|
|
},
|
|
],
|
|
}),
|
|
]);
|
|
|
|
assert.equal(
|
|
content.includes(`${expectedFileRow("Projects/Alpha/Note A.md")} #reply #root`),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test("buildAllCommentsNoteContent renders only the header when there are no comments", () => {
|
|
const content = buildAllCommentsNoteContent("dev", []);
|
|
|
|
assert.equal(
|
|
content,
|
|
`\n<div class="aside-index-header-caption" style="display: block; color: #8a8a8a; font-size: 12px; line-height: 1.2; text-align: center;">${ALL_COMMENTS_NOTE_IMAGE_CAPTION}</div>\n`,
|
|
);
|
|
});
|
|
|
|
test("buildAllCommentsNoteContent ignores comments attached to the generated aggregate note", () => {
|
|
const content = buildAllCommentsNoteContent("dev", [
|
|
createComment({
|
|
filePath: ALL_COMMENTS_NOTE_PATH,
|
|
selectedText: "self",
|
|
}),
|
|
createComment({
|
|
id: "comment-2",
|
|
filePath: "Z.md",
|
|
selectedText: "real",
|
|
}),
|
|
]);
|
|
|
|
assert.doesNotMatch(content, /Aside index\.md/);
|
|
assert.equal(content.includes(expectedFileRow("Z.md")), true);
|
|
});
|
|
|
|
test("buildAllCommentsNoteContent ignores comments whose source file no longer exists", () => {
|
|
const content = buildAllCommentsNoteContent("dev", [
|
|
createComment({
|
|
id: "missing-note",
|
|
filePath: "Missing.md",
|
|
selectedText: "gone",
|
|
}),
|
|
createComment({
|
|
id: "real-note",
|
|
filePath: "Real.md",
|
|
selectedText: "still here",
|
|
}),
|
|
], {
|
|
hasSourceFile: (filePath) => filePath === "Real.md",
|
|
});
|
|
|
|
assert.doesNotMatch(content, /Missing\.md/);
|
|
assert.equal(content.includes(expectedFileRow("Real.md")), true);
|
|
});
|
|
|
|
test("buildAllCommentsNoteContent escapes folder and file link HTML", () => {
|
|
const content = buildAllCommentsNoteContent("dev", [
|
|
createComment({
|
|
filePath: "A&B/<Note>.md",
|
|
}),
|
|
]);
|
|
|
|
assert.match(content, /^A&B$/m);
|
|
assert.equal(content.includes(expectedFileRow("A&B/<Note>.md")), true);
|
|
});
|
|
|
|
test("buildAllCommentsNoteContent groups files under a shared folder heading", () => {
|
|
const longPath = "Clippings/Vlad Tenev and Tudor Achim on mathematical superintelligence and the end of buggy software.md";
|
|
const content = buildAllCommentsNoteContent("dev", [
|
|
createComment({
|
|
filePath: longPath,
|
|
}),
|
|
]);
|
|
|
|
assert.match(
|
|
content,
|
|
new RegExp(`Clippings\\n${expectedFileRow(longPath).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`),
|
|
);
|
|
});
|
|
|
|
test("buildAllCommentsNoteContent groups multiple files under the same path heading", () => {
|
|
const content = buildAllCommentsNoteContent("dev", [
|
|
createComment({
|
|
id: "comment-1",
|
|
filePath: "Projects/Alpha/Note A.md",
|
|
anchorKind: "page",
|
|
}),
|
|
createComment({
|
|
id: "comment-2",
|
|
filePath: "Projects/Alpha/Note B.md",
|
|
selectedText: "beta",
|
|
}),
|
|
]);
|
|
|
|
assert.equal(content.match(/^Projects\/Alpha$/gm)?.length ?? 0, 1);
|
|
assert.equal(content.includes(`Projects/Alpha\n${expectedFileRow("Projects/Alpha/Note A.md")}`), true);
|
|
assert.equal(content.includes(expectedFileRow("Projects/Alpha/Note B.md")), true);
|
|
});
|
|
|
|
test("isAllCommentsNotePath matches the generated note path", () => {
|
|
assert.equal(isAllCommentsNotePath(ALL_COMMENTS_NOTE_PATH), true);
|
|
assert.equal(isAllCommentsNotePath("notes/custom index.md", "notes/custom index.md"), true);
|
|
assert.equal(isAllCommentsNotePath("Random.md"), false);
|
|
});
|
|
|
|
test("normalizeAllCommentsNotePath keeps the current and legacy names and adds md when needed", () => {
|
|
assert.equal(ALL_COMMENTS_NOTE_PATH, "🐰 Aside Index.md");
|
|
assert.equal(LEGACY_ALL_COMMENTS_NOTE_PATH, "Aside index.md");
|
|
assert.equal(normalizeAllCommentsNotePath(""), ALL_COMMENTS_NOTE_PATH);
|
|
assert.equal(normalizeAllCommentsNotePath("notes/custom index"), "notes/custom index.md");
|
|
assert.equal(normalizeAllCommentsNotePath("notes/custom index.md"), "notes/custom index.md");
|
|
});
|
|
|
|
test("normalizeAllCommentsNoteImageUrl keeps the default when blank", () => {
|
|
assert.equal(normalizeAllCommentsNoteImageUrl(""), ALL_COMMENTS_NOTE_IMAGE_URL);
|
|
assert.equal(normalizeAllCommentsNoteImageUrl(" https://example.com/header.webp "), "https://example.com/header.webp");
|
|
});
|
|
|
|
test("normalizeAllCommentsNoteImageCaption keeps the default when missing and allows blank", () => {
|
|
assert.equal(normalizeAllCommentsNoteImageCaption(null), ALL_COMMENTS_NOTE_IMAGE_CAPTION);
|
|
assert.equal(normalizeAllCommentsNoteImageCaption(" Custom caption "), "Custom caption");
|
|
assert.equal(normalizeAllCommentsNoteImageCaption(" "), "");
|
|
});
|