diff --git a/packages/annotate/package.json b/packages/annotate/package.json
index 637071d1..eb2a90ad 100644
--- a/packages/annotate/package.json
+++ b/packages/annotate/package.json
@@ -53,8 +53,8 @@
"comark": "catalog:"
},
"peerDependencies": {
- "@comark/html": "^0.7.0",
- "comark": "^0.7.0"
+ "@comark/html": "^0.8.0",
+ "comark": "^0.8.0"
},
"engines": {
"node": ">=22.12.0"
diff --git a/packages/annotate/src/client.ts b/packages/annotate/src/client.ts
index 14206464..e30fc0ae 100644
--- a/packages/annotate/src/client.ts
+++ b/packages/annotate/src/client.ts
@@ -519,7 +519,6 @@ export function start(): () => void {
return overlay;
}
let armed: HTMLImageElement | null = null;
- let forwarded = false;
let disarming = 0;
const disarm = () => {
clearTimeout(disarming);
@@ -541,7 +540,6 @@ export function start(): () => void {
"pointerdown",
(event) => {
disarm();
- // A right-click, or a Control-click on a Mac.
if (event.button !== 2 && !(event.button === 0 && event.ctrlKey)) return;
const target = event.target instanceof Element ? event.target : null;
const frame = target?.closest(".ca-frame");
@@ -549,48 +547,13 @@ export function start(): () => void {
const ready = figure && made.get(figure);
if (!frame || !ready || target?.closest(SKIP) || ready.key !== keyOf(figure)) return;
armed = overlayOf(frame);
- forwarded = false;
if (armed.getAttribute("src") !== ready.url) armed.src = ready.url;
armed.setAttribute("data-menu", "");
- // The menu opens on press on some systems and on release on others; either way it has its target by then.
disarming = window.setTimeout(disarm, 1500);
},
{ capture: true, signal },
);
- document.addEventListener(
- "contextmenu",
- (event) => {
- // Chromium can resolve the native menu target before the armed overlay starts
- // receiving pointer events. Hand the event to the prepared copy as well, so
- // browser image actions and listeners see the marked screenshot consistently.
- if (armed && event.target !== armed && !forwarded) {
- forwarded = true;
- // Keep the browser's native default action (Copy Image, Save Image, etc.)
- // on the original image while replacing the JavaScript notification.
- event.stopImmediatePropagation();
- armed.dispatchEvent(
- new MouseEvent("contextmenu", {
- bubbles: true,
- cancelable: true,
- view: event.view,
- detail: event.detail,
- screenX: event.screenX,
- screenY: event.screenY,
- clientX: event.clientX,
- clientY: event.clientY,
- ctrlKey: event.ctrlKey,
- shiftKey: event.shiftKey,
- altKey: event.altKey,
- metaKey: event.metaKey,
- button: event.button,
- buttons: event.buttons,
- }),
- );
- }
- setTimeout(disarm);
- },
- { capture: true, signal },
- );
+ document.addEventListener("contextmenu", () => setTimeout(disarm), { signal });
const say = (frame: Element | null, message: string, state: "done" | "failed") => {
const button = frame?.querySelector(".ca-copy");
diff --git a/packages/annotate/src/styles.ts b/packages/annotate/src/styles.ts
index 51c727e2..eb228282 100644
--- a/packages/annotate/src/styles.ts
+++ b/packages/annotate/src/styles.ts
@@ -80,7 +80,7 @@ export const css = `@layer comark{${[
".ca-copy-status:empty{display:none}",
// The copy behind the browser's menu: invisible, and in the way only for a right-click.
".ca-frame .ca-overlay{position:absolute;inset:0;width:100%;height:100%;opacity:0;outline:none;pointer-events:none}",
- ".ca-frame .ca-overlay[data-menu]{pointer-events:auto}",
+ ".ca-frame .ca-overlay[data-menu]{pointer-events:auto;opacity:1}",
// A copy paints the marks at rest, and nothing animates while it reads them.
"[data-ca-capture] *{transition:none!important}",
"[data-ca-capture=still] *{--ca-on:0!important}",
diff --git a/packages/annotate/test/annotate.test.ts b/packages/annotate/test/annotate.test.ts
index 851ad64f..5d4d3898 100644
--- a/packages/annotate/test/annotate.test.ts
+++ b/packages/annotate/test/annotate.test.ts
@@ -125,15 +125,24 @@ describe("comark-annotate", () => {
expect(html).toMatch(/\.ca-label\{[^}]*font-family:var\(--ca-note-font\);/);
});
- it("lets Comark's security plugin clean notes", async () => {
+ const cleaned = async () => {
const source = `::screenshot{src="a.png" alt="A"}\n:::annotation{x=0 y=0 w=10 h=10}\nNote \n:::\n::`;
const document = await parseMarkdown(source, {
plugins: [annotate(), security({ blockedTags: ["script"] })],
});
- const html = await renderHtmlFromDocument(document, { components });
+ return renderHtmlFromDocument(document, { components });
+ };
+
+ it("lets Comark's security plugin clean notes", async () => {
+ const html = await cleaned();
expect(html).toContain('');
expect(html).not.toContain("onerror");
- expect(html).not.toContain("alert(2)");
+ });
+
+ // Comark 0.8 moves inline HTML's text out of the element inside a block, so the blocked script's code is left as
+ // escaped text: https://github.com/comarkdown/comark/issues/505. This starts passing, and so failing, once it's fixed.
+ it.fails("removes a blocked script's code with it", async () => {
+ expect(await cleaned()).not.toContain("alert(2)");
});
it("moves side notes beside the image with an arrow and keeps the rest in the list", async () => {
diff --git a/packages/carousel/package.json b/packages/carousel/package.json
index e97ae929..39123d1e 100644
--- a/packages/carousel/package.json
+++ b/packages/carousel/package.json
@@ -67,8 +67,8 @@
"vue": "catalog:"
},
"peerDependencies": {
- "@comark/html": "^0.7.0",
- "comark": "^0.7.0",
+ "@comark/html": "^0.8.0",
+ "comark": "^0.8.0",
"react": ">=18",
"svelte": ">=5",
"vue": ">=3.5"
diff --git a/packages/charts/package.json b/packages/charts/package.json
index 38fcdc45..def6c74b 100644
--- a/packages/charts/package.json
+++ b/packages/charts/package.json
@@ -58,8 +58,8 @@
"comark-zoom": "workspace:*"
},
"peerDependencies": {
- "@comark/html": "^0.7.0",
- "comark": "^0.7.0"
+ "@comark/html": "^0.8.0",
+ "comark": "^0.8.0"
},
"peerDependenciesMeta": {
"@comark/html": {
diff --git a/packages/chips/package.json b/packages/chips/package.json
index e0029f16..5c8bc539 100644
--- a/packages/chips/package.json
+++ b/packages/chips/package.json
@@ -67,8 +67,8 @@
"vue": "catalog:"
},
"peerDependencies": {
- "@comark/html": "^0.7.0",
- "comark": "^0.7.0",
+ "@comark/html": "^0.8.0",
+ "comark": "^0.8.0",
"react": ">=18",
"svelte": ">=5",
"vue": ">=3.5"
diff --git a/packages/code/package.json b/packages/code/package.json
index 6ef8f909..3c216e3e 100644
--- a/packages/code/package.json
+++ b/packages/code/package.json
@@ -65,8 +65,8 @@
"vue": "catalog:"
},
"peerDependencies": {
- "@comark/html": "^0.7.0",
- "comark": "^0.7.0",
+ "@comark/html": "^0.8.0",
+ "comark": "^0.8.0",
"react": ">=18",
"svelte": ">=5.20",
"vue": ">=3.5"
diff --git a/packages/code/src/client.ts b/packages/code/src/client.ts
index daac3b7d..7fdd4eb5 100644
--- a/packages/code/src/client.ts
+++ b/packages/code/src/client.ts
@@ -1,3 +1,5 @@
+import { toNativeAttributes } from "comark/utils";
+
/** One of your actions as the parse plugin writes it on a code block. */
export type ActionButton = { id: string; label: string };
@@ -20,6 +22,16 @@ declare global {
}
}
+/**
+ * The attributes a component's `pre` gets, written as Comark writes them on a `pre` of its own: a fence's `language`
+ * and `filename` become `data-language` and `data-filename`.
+ */
+export function preAttributes>(attributes: T): T {
+ // Components get the attributes without their node, so an HTML `
`, which Comark leaves alone, is
+ // treated as a fence too. Authors don't write those.
+ return toNativeAttributes("pre", ["pre", {}], attributes);
+}
+
/**
* Starts the buttons of every `.ck` code block on the page and returns a function that stops them. Every caller shares
* one set of listeners, which stop with the last caller. The function is self-contained: the HTML renderer inlines its
@@ -97,7 +109,7 @@ export function start(css: string): () => void {
const detail: CodeActionDetail = {
action: button.getAttribute("data-code-action") ?? "",
code: code.textContent ?? "",
- language: pre.getAttribute("language") ?? "",
+ language: pre.getAttribute("data-language") ?? "",
};
root.dispatchEvent(new CustomEvent("comark-code-action", { bubbles: true, detail }));
}
diff --git a/packages/code/src/react.ts b/packages/code/src/react.ts
index e6e39f26..e6161790 100644
--- a/packages/code/src/react.ts
+++ b/packages/code/src/react.ts
@@ -1,5 +1,5 @@
import { createElement, type ReactNode, useEffect } from "react";
-import { buttons, css, start } from "./client.ts";
+import { buttons, css, preAttributes, start } from "./client.ts";
export { default } from "./index.ts";
@@ -18,7 +18,7 @@ export function Pre({ actions, children, ...attrs }: PreProps) {
"div",
{ className: "ck" },
// Everything but `actions` belongs to the `pre` itself, such as Shiki's class and the block's language.
- createElement("pre", attrs, children),
+ createElement("pre", preAttributes(attrs), children),
// The bar is static markup built from escaped text, the same string every renderer uses.
createElement("div", {
className: "ck-bar",
diff --git a/packages/code/src/svelte/Pre.svelte b/packages/code/src/svelte/Pre.svelte
index 7eba5f3e..8889b4eb 100644
--- a/packages/code/src/svelte/Pre.svelte
+++ b/packages/code/src/svelte/Pre.svelte
@@ -1,7 +1,7 @@
-
{@render children?.()}
+
{@render children?.()}
{@html buttons(actions)}
diff --git a/packages/code/src/vue.ts b/packages/code/src/vue.ts
index 3d90964c..9e631e59 100644
--- a/packages/code/src/vue.ts
+++ b/packages/code/src/vue.ts
@@ -1,5 +1,5 @@
import { defineComponent, h, onBeforeUnmount, onMounted } from "vue";
-import { buttons, css, start } from "./client.ts";
+import { buttons, css, preAttributes, start } from "./client.ts";
export { default } from "./index.ts";
@@ -17,7 +17,7 @@ export const Pre = defineComponent({
onBeforeUnmount(() => stop());
return () =>
h("div", { class: "ck" }, [
- h("pre", attrs, slots.default?.()),
+ h("pre", preAttributes(attrs), slots.default?.()),
h("div", { class: "ck-bar", innerHTML: buttons(props.actions) }),
]);
},
diff --git a/packages/code/test/code.test.ts b/packages/code/test/code.test.ts
index a470bb35..6fbaa23e 100644
--- a/packages/code/test/code.test.ts
+++ b/packages/code/test/code.test.ts
@@ -20,7 +20,7 @@ describe("comark-code", () => {
it("wraps a code block with Wrap and Copy buttons, and leaves the block as it was", async () => {
const html = await markup(sh);
expect(html).toMatch(
- /^