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( - /^
pnpm add comark-code<\/code><\/pre>
/, + /^
pnpm add comark-code<\/code><\/pre>
/, ); expect(html).toContain( '', + '
pnpm add comark-code
', ); expect(expected.match(/class="ck-copy"/g)).toHaveLength(2); for (const name of ["vue", "react"] as const) { diff --git a/packages/embed/package.json b/packages/embed/package.json index 9cb2126a..59a8906f 100644 --- a/packages/embed/package.json +++ b/packages/embed/package.json @@ -66,8 +66,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/icon/package.json b/packages/icon/package.json index 89751f76..5920d929 100644 --- a/packages/icon/package.json +++ b/packages/icon/package.json @@ -68,8 +68,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/include/package.json b/packages/include/package.json index a2cb262d..25c43380 100644 --- a/packages/include/package.json +++ b/packages/include/package.json @@ -44,7 +44,7 @@ "comark": "catalog:" }, "peerDependencies": { - "comark": "^0.7.0" + "comark": "^0.8.0" }, "engines": { "node": ">=22.12.0" diff --git a/packages/include/test/include.test.ts b/packages/include/test/include.test.ts index 64c79495..2466c30d 100644 --- a/packages/include/test/include.test.ts +++ b/packages/include/test/include.test.ts @@ -81,10 +81,10 @@ describe("comark-include", () => { ], ]); expect(await render(':include{src="src/index.ts" region="setup"}', { read })).toBe( - '
const a = 1\nconst b = 2
', + '
const a = 1\nconst b = 2
', ); expect(await render(':include{src="src/index.ts" lines="1:1" lang="js"}', { read })).toBe( - `
import { x } from 'x'
`, + `
import { x } from 'x'
`, ); }); @@ -124,7 +124,7 @@ describe("comark-include", () => { it("indents pasted lines like the include, so a partial stays in its list item", async () => { const read = files({ "step.md": "Run it:\n\n```sh\npnpm i\n```" }); expect(await render('- One\n\n :include{src="step.md"}\n- Two', { read })).toBe( - '
    \n
  • \n

    One

    \n

    Run it:

    \n
    pnpm i
    \n
  • \n
  • Two
  • \n
', + '
    \n
  • \n

    One

    \n

    Run it:

    \n
    pnpm i
    \n
  • \n
  • Two
  • \n
', ); }); @@ -166,7 +166,9 @@ describe("comark-include", () => { const source = ["# #region setup", 'end = "#endregion setup"', "# #endregion setup"].join("\n"); expect( await render(':include{src="a.py" region="setup"}', { read: files({ "a.py": source }) }), - ).toBe('
end = "#endregion setup"
'); + ).toBe( + '
end = "#endregion setup"
', + ); }); it("drops a failing include and names it with the fix", async () => { diff --git a/packages/lint/package.json b/packages/lint/package.json index 8ce40732..fe71d7fd 100644 --- a/packages/lint/package.json +++ b/packages/lint/package.json @@ -55,7 +55,7 @@ "vite-doctor": "catalog:" }, "peerDependencies": { - "comark": "^0.7.0", + "comark": "^0.8.0", "comark-annotate": "workspace:^", "comark-carousel": "workspace:^", "comark-charts": "workspace:^", diff --git a/packages/lint/src/index.ts b/packages/lint/src/index.ts index 15a0bfba..db7d7c4c 100644 --- a/packages/lint/src/index.ts +++ b/packages/lint/src/index.ts @@ -51,8 +51,8 @@ export type LintOptions = { type Token = { type: string; tag: string; - nesting: number; map: [number, number] | null; + content: string; children: Token[] | null; }; @@ -154,11 +154,17 @@ const crashed = (rule: Rule, error: unknown): Diagnostic => ({ fix: "The other rules still ran. Report this with the document at https://github.com/onmax/comark-plugins/issues.", }); -// Comark reads HTML blocks and code as text, so a `::` or `{` there opens nothing. +// Comark reads code, comments, and `
`, `