feat: code blocks with shiki highlighting
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
DEFAULT_CODE_BLOCK_SHOW_BACKGROUND,
|
||||
EMPTY_CODE_BLOCK_SELECTION_STATE,
|
||||
createCodeBlockCustomData,
|
||||
getCodeBlockSelectionState,
|
||||
getCodeBlockCustomData,
|
||||
isCodeBlockEmbeddable,
|
||||
sanitizeCodeBlockLanguage,
|
||||
updateCodeBlockElements,
|
||||
} from "./codeblock";
|
||||
|
||||
const baseEmbeddable = {
|
||||
id: "codeblock-1",
|
||||
type: "embeddable" as const,
|
||||
x: 10,
|
||||
y: 20,
|
||||
strokeColor: "transparent",
|
||||
backgroundColor: "transparent",
|
||||
fillStyle: "solid" as const,
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid" as const,
|
||||
roundness: null,
|
||||
roughness: 0,
|
||||
opacity: 100,
|
||||
width: 420,
|
||||
height: 260,
|
||||
angle: 0,
|
||||
seed: 1,
|
||||
version: 1,
|
||||
versionNonce: 2,
|
||||
index: null,
|
||||
isDeleted: false,
|
||||
groupIds: [],
|
||||
frameId: null,
|
||||
boundElements: null,
|
||||
updated: 1,
|
||||
link: null,
|
||||
locked: false,
|
||||
};
|
||||
|
||||
describe("codeblock helpers", () => {
|
||||
test("type guard accepts only repo-owned codeblock embeddables", () => {
|
||||
expect(
|
||||
isCodeBlockEmbeddable({
|
||||
...baseEmbeddable,
|
||||
customData: createCodeBlockCustomData(),
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isCodeBlockEmbeddable({
|
||||
...baseEmbeddable,
|
||||
customData: {
|
||||
excaliType: "other",
|
||||
version: 2,
|
||||
code: "",
|
||||
language: "tsx",
|
||||
showBackground: true,
|
||||
},
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
isCodeBlockEmbeddable({
|
||||
...baseEmbeddable,
|
||||
link: "https://example.com",
|
||||
customData: createCodeBlockCustomData(),
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
isCodeBlockEmbeddable({
|
||||
...baseEmbeddable,
|
||||
type: "iframe",
|
||||
customData: createCodeBlockCustomData(),
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
isCodeBlockEmbeddable({
|
||||
...baseEmbeddable,
|
||||
customData: {
|
||||
excaliType: "codeblock",
|
||||
version: 1,
|
||||
code: "",
|
||||
language: "tsx",
|
||||
showBackground: true,
|
||||
},
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("create helper defaults background to enabled", () => {
|
||||
expect(createCodeBlockCustomData().showBackground).toBe(
|
||||
DEFAULT_CODE_BLOCK_SHOW_BACKGROUND,
|
||||
);
|
||||
});
|
||||
|
||||
test("update helper preserves unrelated fields and non-codeblock embeddables", () => {
|
||||
const codeBlock = {
|
||||
...baseEmbeddable,
|
||||
customData: createCodeBlockCustomData({ code: "const a = 1;" }),
|
||||
};
|
||||
const foreignEmbeddable = {
|
||||
...baseEmbeddable,
|
||||
id: "embed-2",
|
||||
customData: { source: "foreign" },
|
||||
};
|
||||
const elements = [codeBlock, foreignEmbeddable];
|
||||
|
||||
const nextElements = updateCodeBlockElements(elements, "codeblock-1", {
|
||||
code: "const a = 2;",
|
||||
language: "ts",
|
||||
showBackground: false,
|
||||
});
|
||||
|
||||
expect(nextElements).not.toBe(elements);
|
||||
expect(nextElements[0]).not.toBe(codeBlock);
|
||||
expect(nextElements[0]).toMatchObject({
|
||||
id: "codeblock-1",
|
||||
x: 10,
|
||||
y: 20,
|
||||
width: 420,
|
||||
customData: {
|
||||
excaliType: "codeblock",
|
||||
version: 2,
|
||||
code: "const a = 2;",
|
||||
language: "typescript",
|
||||
showBackground: false,
|
||||
},
|
||||
});
|
||||
expect(nextElements[1]).toBe(foreignEmbeddable);
|
||||
expect(
|
||||
updateCodeBlockElements(elements, "embed-2", { code: "ignored" }),
|
||||
).toBe(elements);
|
||||
});
|
||||
|
||||
test("selection helper opens only for a single selected codeblock", () => {
|
||||
const codeBlock = {
|
||||
...baseEmbeddable,
|
||||
customData: createCodeBlockCustomData(),
|
||||
};
|
||||
const other = {
|
||||
...baseEmbeddable,
|
||||
id: "shape-2",
|
||||
type: "rectangle" as const,
|
||||
};
|
||||
|
||||
expect(
|
||||
getCodeBlockSelectionState([codeBlock, other], {
|
||||
selectedElementIds: { "codeblock-1": true },
|
||||
}),
|
||||
).toMatchObject({
|
||||
isPanelOpen: true,
|
||||
selectedCodeBlockId: "codeblock-1",
|
||||
selectedCodeBlock: {
|
||||
customData: {
|
||||
showBackground: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
getCodeBlockSelectionState([codeBlock, other], {
|
||||
selectedElementIds: { "codeblock-1": true, "shape-2": true },
|
||||
}),
|
||||
).toEqual(EMPTY_CODE_BLOCK_SELECTION_STATE);
|
||||
|
||||
expect(
|
||||
getCodeBlockSelectionState([codeBlock, other], {
|
||||
selectedElementIds: { "shape-2": true },
|
||||
}),
|
||||
).toEqual(EMPTY_CODE_BLOCK_SELECTION_STATE);
|
||||
});
|
||||
|
||||
test("language sanitization accepts shiki aliases and normalizes them", () => {
|
||||
expect(sanitizeCodeBlockLanguage("ts")).toBe("typescript");
|
||||
expect(sanitizeCodeBlockLanguage("js")).toBe("javascript");
|
||||
expect(sanitizeCodeBlockLanguage("md")).toBe("markdown");
|
||||
expect(sanitizeCodeBlockLanguage("bash")).toBe("shellscript");
|
||||
expect(sanitizeCodeBlockLanguage("plaintext")).toBe("plaintext");
|
||||
});
|
||||
|
||||
test("custom data reads normalize persisted shiki aliases", () => {
|
||||
const codeBlock = {
|
||||
...baseEmbeddable,
|
||||
customData: {
|
||||
excaliType: "codeblock" as const,
|
||||
version: 2 as const,
|
||||
code: "echo hi",
|
||||
language: "bash" as const,
|
||||
showBackground: true,
|
||||
},
|
||||
};
|
||||
|
||||
expect(getCodeBlockCustomData(codeBlock)).toMatchObject({
|
||||
code: "echo hi",
|
||||
language: "shellscript",
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user