comark-code-notes 0.0.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,69 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 onmax
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ The icons are Lucide's (https://lucide.dev), under its license:
26
+
27
+ ISC License
28
+
29
+ Copyright (c) 2026 Lucide Icons and Contributors
30
+
31
+ Permission to use, copy, modify, and/or distribute this software for any
32
+ purpose with or without fee is hereby granted, provided that the above
33
+ copyright notice and this permission notice appear in all copies.
34
+
35
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
36
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
37
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
38
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
39
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
40
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
41
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
42
+
43
+ ---
44
+
45
+ The following Lucide icons are derived from the Feather project:
46
+
47
+ airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out
48
+
49
+ The MIT License (MIT) (for the icons listed above)
50
+
51
+ Copyright (c) 2013-present Cole Bemis
52
+
53
+ Permission is hereby granted, free of charge, to any person obtaining a copy
54
+ of this software and associated documentation files (the "Software"), to deal
55
+ in the Software without restriction, including without limitation the rights
56
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
57
+ copies of the Software, and to permit persons to whom the Software is
58
+ furnished to do so, subject to the following conditions:
59
+
60
+ The above copyright notice and this permission notice shall be included in all
61
+ copies or substantial portions of the Software.
62
+
63
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
64
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
65
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
66
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
67
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
68
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
69
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,45 @@
1
1
  # comark-code-notes
2
2
 
3
- Placeholder. The first release is published from https://github.com/onmax/comark-plugins.
3
+ [![npm version](https://comark-ui.onmax.me/badge.svg?source=npm-version&package=comark-code-notes&icon=npm)](https://npmx.dev/package/comark-code-notes)
4
+ [![npm downloads](https://comark-ui.onmax.me/badge.svg?source=npm-downloads&package=comark-code-notes&period=month&icon=npm)](https://npmx.dev/package/comark-code-notes)
5
+ [![license](https://comark-ui.onmax.me/badge.svg?label=license&value=MIT)](https://npmx.dev/package/comark-code-notes)
6
+ [![docs](https://comark-ui.onmax.me/badge.svg?label=docs&value=comark-ui.onmax.me)](https://comark-ui.onmax.me/code-notes/)
7
+
8
+ Numbered notes on lines of code, as in Quarto and AsciiDoc: a `// <1>` comment at the end of a line becomes a marker, and the numbered list after the fence becomes its notes, each linked to its marker and back. A [Comark](https://comark.dev) plugin for HTML, Vue, React, and Svelte.
9
+
10
+ ```sh
11
+ pnpm add comark comark-code-notes
12
+ ```
13
+
14
+ ```ts
15
+ import { createHtmlRenderer } from "@comark/html";
16
+ import shiki from "@comark/html/plugins/shiki";
17
+ import codeNotes, { components } from "comark-code-notes/html";
18
+
19
+ const render = createHtmlRenderer({ plugins: [shiki(), codeNotes()], components });
20
+ ```
21
+
22
+ ````md
23
+ ```ts
24
+ const res = await fetch(url) // <1>
25
+ if (!res.ok) return 0 // <2>
26
+ ```
27
+
28
+ 1. One request per plaza, so the counts load in parallel.
29
+ 2. A plaza with no census yet counts as empty.
30
+ ````
31
+
32
+ Markers sit in a line-end comment: `// <1>`, `# <1>`, `-- <1>`, `; <1>`, `/* <1> */`, or `<!-- <1> -->`. They are taken out before Shiki highlights the code and drawn into its lines after, so put `codeNotes()` after `shiki()`. The marker's number is CSS, so copying the code, by hand or with comark-code's button, leaves it out. A fence with no numbered list after it, and any ` ```md ` fence, stays as written.
33
+
34
+ The plugin adds every problem to `meta.issues`, so a host can reject a document before publishing it: markers with no list, a marker with no note, a note with no marker, and `shiki` after the plugin. Each issue names its fence and says how to fix it.
35
+
36
+ ## Renderers
37
+
38
+ | Renderer | Import |
39
+ | --- | --- |
40
+ | `@comark/html` | `comark-code-notes/html`: the styles are inlined once per document |
41
+ | `@comark/vue` | `comark-code-notes/vue` |
42
+ | `@comark/react` | `comark-code-notes/react` |
43
+ | `@comark/svelte` | `comark-code-notes/svelte` |
44
+
45
+ Each entry exports the parse plugin as `default` and the renderer's `components`. The Vue, React, and Svelte components add the styles when they mount; import `comark-code-notes/style.css` with your styles so a server-rendered page doesn't shift. The styles read the shared `--comark-*` tokens and sit in a `comark` CSS layer, so any plain rule of your own wins.
@@ -0,0 +1,13 @@
1
+ //#region src/styles.d.ts
2
+ /**
3
+ * Round numbered markers at the ends of lines and before each note, as a soft tint of shadcn/ui's `--primary` when the
4
+ * page has it, or of the foreground otherwise, with the number in that color, on the shared `--comark-*` tokens.
5
+ * Override `--cn-marker` and `--cn-marker-text`. The number is drawn by CSS, so copying the code never takes it along.
6
+ * The rules sit in the `comark` layer, so any unlayered rule of the host's wins.
7
+ */
8
+ export declare const css: string;
9
+ //#endregion
10
+ //#region src/client.d.ts
11
+ /** Adds the code notes styles to the page once. The Vue, React, and Svelte components call it when they mount. */
12
+ export declare function addStyles(): void;
13
+ //#endregion
@@ -0,0 +1,12 @@
1
+ import { t as css } from "./styles-zxOi3Y7k.mjs";
2
+ //#region src/client.ts
3
+ /** Adds the code notes styles to the page once. The Vue, React, and Svelte components call it when they mount. */
4
+ function addStyles() {
5
+ if (document.querySelector("style[data-comark-code-notes]")) return;
6
+ const style = document.createElement("style");
7
+ style.dataset.comarkCodeNotes = "";
8
+ style.textContent = css;
9
+ document.head.append(style);
10
+ }
11
+ //#endregion
12
+ export { addStyles, css };
@@ -0,0 +1,7 @@
1
+ import _default, { CodeNotesAttributes, CodeNotesMeta, NOTES_TAG } from "./index.mjs";
2
+ import { NodeHandler } from "comark";
3
+ //#region src/html.d.ts
4
+ /** The HTML renderer for code notes. Pass it as `components`; it inlines the styles once per document. */
5
+ export declare const components: Record<"code-notes", NodeHandler>;
6
+ //#endregion
7
+ export { type CodeNotesAttributes, type CodeNotesMeta, NOTES_TAG, _default as default };
package/dist/html.mjs ADDED
@@ -0,0 +1,18 @@
1
+ import src_default, { NOTES_TAG } from "./index.mjs";
2
+ import { t as css } from "./styles-zxOi3Y7k.mjs";
3
+ //#region src/html.ts
4
+ const styled = /* @__PURE__ */ new WeakSet();
5
+ const notes = async ([, attrs, ...children], state) => {
6
+ const styles = styled.has(state.context) ? "" : `<style data-comark-code-notes>${css}</style>`;
7
+ styled.add(state.context);
8
+ const start = Number(attrs.start);
9
+ return `${styles}<ol class="cn-notes"${Number.isInteger(start) && start !== 1 ? ` start="${start}"` : ""}>${await state.flow([
10
+ "div",
11
+ {},
12
+ ...children
13
+ ], state)}</ol>`;
14
+ };
15
+ /** The HTML renderer for code notes. Pass it as `components`; it inlines the styles once per document. */
16
+ const components = { "code-notes": notes };
17
+ //#endregion
18
+ export { NOTES_TAG, components, src_default as default };
@@ -0,0 +1,22 @@
1
+ //#region src/index.d.ts
2
+ export type CodeNotesMeta = {
3
+ /** One readable sentence per problem with a fence's markers or notes. Other plugins add theirs too. */
4
+ issues: string[];
5
+ /** The comark-linter rule of each issue, by its sentence, so the linter reports it under its own code. */
6
+ issueRules: Record<string, string>;
7
+ };
8
+ /** The node the numbered list after an annotated fence becomes. Each item starts with its marker, linking back. */
9
+ export declare const NOTES_TAG = "code-notes";
10
+ /** What a `code-notes` node carries besides its items: the number of its first note. */
11
+ export type CodeNotesAttributes = {
12
+ start?: number;
13
+ };
14
+ /**
15
+ * Turns `// <1>` at the end of a line in a fence, in the language's comment, into a numbered marker, and the numbered
16
+ * list right after the fence into its notes, each linked to its marker and back. Markers are stripped before Shiki
17
+ * highlights the code and drawn into its lines after, so put this plugin after Comark's `shiki` in `plugins`.
18
+ * Problems are added to `meta.issues`.
19
+ */
20
+ declare const _default: import("comark").ComarkPluginFactory<void, CodeNotesMeta, {}>;
21
+ //#endregion
22
+ export { _default as default };
package/dist/index.mjs ADDED
@@ -0,0 +1,213 @@
1
+ import { defineComarkPlugin } from "comark";
2
+ //#region src/index.ts
3
+ /** The node the numbered list after an annotated fence becomes. Each item starts with its marker, linking back. */
4
+ const NOTES_TAG = "code-notes";
5
+ const MARKERS = String.raw`<\d+>(?:\s*<\d+>)*`;
6
+ const ALONE = new RegExp(String.raw`\s*(?:(?://|#|--|;|%)\s*(${MARKERS})|(?:/\*|<!--)\s*(${MARKERS})\s*(?:\*/|-->))\s*$`);
7
+ const AFTER_WORDS = new RegExp(String.raw`((?://|#|--|;|%).*?\S)\s+(${MARKERS})\s*$`);
8
+ const BLOCK_WORDS = new RegExp(String.raw`^(.*?)(/\*|<!--)(.*?)\s+(${MARKERS})\s*(\*/|-->)\s*$`);
9
+ const MARKDOWN = /* @__PURE__ */ new Set([
10
+ "md",
11
+ "mdc",
12
+ "markdown",
13
+ "comark"
14
+ ]);
15
+ const isElement = (node) => Array.isArray(node) && node[0] !== null;
16
+ const childrenOf = ([, , ...rest]) => rest;
17
+ const textOf = (node) => !Array.isArray(node) ? node : isElement(node) ? childrenOf(node).map(textOf).join("") : "";
18
+ const numbers = (markers) => [...markers.matchAll(/\d+/g)].map(([digits]) => Number(digits));
19
+ function hash(text) {
20
+ let value = 2166136261;
21
+ for (let at = 0; at < text.length; at++) value = Math.imul(value ^ text.charCodeAt(at), 16777619);
22
+ return (value >>> 0).toString(36);
23
+ }
24
+ /** Strips the markers off each line, and says where they were. */
25
+ function strip(content) {
26
+ const markers = /* @__PURE__ */ new Map();
27
+ return {
28
+ code: content.split("\n").map((line, index) => {
29
+ const alone = ALONE.exec(line);
30
+ if (alone) {
31
+ markers.set(index, numbers(alone[1] ?? alone[2] ?? ""));
32
+ return line.slice(0, alone.index);
33
+ }
34
+ const block = BLOCK_WORDS.exec(line);
35
+ if (block) {
36
+ markers.set(index, numbers(block[4] ?? ""));
37
+ return `${block[1] ?? ""}${block[2] ?? ""}${(block[3] ?? "").trimEnd()} ${block[5] ?? ""}`;
38
+ }
39
+ const words = AFTER_WORDS.exec(line);
40
+ if (!words) return line;
41
+ markers.set(index, numbers(words[2] ?? ""));
42
+ return line.slice(0, words.index) + (words[1] ?? "");
43
+ }).join("\n"),
44
+ markers
45
+ };
46
+ }
47
+ /**
48
+ * Takes the markers out of every fence a numbered list follows, before any plugin's `post`, so Shiki highlights the
49
+ * code without them in any plugin order.
50
+ */
51
+ function read(tokens) {
52
+ const fences = [];
53
+ let count = 0;
54
+ for (const [index, token] of tokens.entries()) {
55
+ if (token.type !== "fence") continue;
56
+ count++;
57
+ const language = token.info.trim().split(/[\s[{]/, 1)[0] ?? "";
58
+ if (MARKDOWN.has(language.toLowerCase())) continue;
59
+ const { code, markers } = strip(token.content);
60
+ if (!markers.size) continue;
61
+ const name = `The \`\`\`${language} fence ${count}`;
62
+ const notes = tokens[index + 1]?.type === "ordered_list_open";
63
+ if (notes) token.content = code;
64
+ fences.push({
65
+ name,
66
+ code: (notes ? code : token.content).replace(/\n+$/, ""),
67
+ markers,
68
+ notes,
69
+ order: count
70
+ });
71
+ }
72
+ return fences;
73
+ }
74
+ const link = (note, uid, first) => ["a", {
75
+ class: "cn-marker",
76
+ href: `#${uid}-${note}`,
77
+ ...first && { id: `${uid}-${note}-ref` },
78
+ "data-note": String(note),
79
+ "aria-label": `Note ${note}`
80
+ }];
81
+ const marker = (note, uid, first, known) => known.has(note) ? link(note, uid, first) : ["span", {
82
+ class: "cn-marker",
83
+ "data-note": String(note)
84
+ }];
85
+ /** Adds each line's markers to its end, in Shiki's line spans or in the plain code. */
86
+ function draw(code, fence, uid, known) {
87
+ const seen = /* @__PURE__ */ new Set();
88
+ const markersOf = (line) => (fence.markers.get(line) ?? []).map((note) => {
89
+ const first = !seen.has(note);
90
+ seen.add(note);
91
+ return marker(note, uid, first, known);
92
+ });
93
+ const lines = childrenOf(code).filter((child) => isElement(child) && child[0] === "span");
94
+ if (lines.length) {
95
+ for (const [index, line] of lines.entries()) line.push(...markersOf(index));
96
+ return;
97
+ }
98
+ const drawn = textOf(code).split("\n").flatMap((line, index) => [...index ? ["\n"] : [], [
99
+ "span",
100
+ { class: "line" },
101
+ line,
102
+ ...markersOf(index)
103
+ ]]);
104
+ code.splice(2, code.length - 2, ...drawn);
105
+ }
106
+ /** A list's items, with the note number each one holds. */
107
+ function itemsOf(list) {
108
+ const start = Number(list[1].start ?? 1) || 1;
109
+ return childrenOf(list).filter((child) => isElement(child) && child[0] === "li").map((item, index) => ({
110
+ item,
111
+ note: start + index
112
+ }));
113
+ }
114
+ function notesOf(list, fence, uid, issues) {
115
+ const start = Number(list[1].start ?? 1) || 1;
116
+ const items = itemsOf(list);
117
+ const used = new Set([...fence.markers.values()].flat());
118
+ const known = new Set(items.map(({ note }) => note));
119
+ for (const note of [...used].sort((a, b) => a - b)) if (!known.has(note)) issues.push({
120
+ rule: "comark/code-notes/marker-without-note",
121
+ message: `${fence.name}: marker <${note}> has no note, since the list after it ${items.length === 1 ? "has 1 item" : `has ${items.length} items`}. Add its note, or remove the marker.`
122
+ });
123
+ for (const note of known) if (!used.has(note)) issues.push({
124
+ rule: "comark/code-notes/note-without-marker",
125
+ message: `${fence.name}: note ${note} has no <${note}> marker in the code. Add \`// <${note}>\` at the end of its line, in the language's comment.`
126
+ });
127
+ return [
128
+ NOTES_TAG,
129
+ start === 1 ? {} : { start },
130
+ ...items.map(({ item, note }) => {
131
+ return [
132
+ "li",
133
+ { id: `${uid}-${note}` },
134
+ used.has(note) ? ["a", {
135
+ class: "cn-marker",
136
+ href: `#${uid}-${note}-ref`,
137
+ "data-note": String(note),
138
+ "aria-label": `Back to marker ${note}`
139
+ }] : ["span", {
140
+ class: "cn-marker",
141
+ "data-note": String(note)
142
+ }],
143
+ [
144
+ "div",
145
+ {},
146
+ ...childrenOf(item)
147
+ ]
148
+ ];
149
+ })
150
+ ];
151
+ }
152
+ /**
153
+ * Turns `// <1>` at the end of a line in a fence, in the language's comment, into a numbered marker, and the numbered
154
+ * list right after the fence into its notes, each linked to its marker and back. Markers are stripped before Shiki
155
+ * highlights the code and drawn into its lines after, so put this plugin after Comark's `shiki` in `plugins`.
156
+ * Problems are added to `meta.issues`.
157
+ */
158
+ var src_default = defineComarkPlugin(() => {
159
+ const parses = /* @__PURE__ */ new WeakMap();
160
+ return {
161
+ name: "comark-code-notes",
162
+ markdownItPost(state) {
163
+ parses.set(state, read(state.tokens));
164
+ },
165
+ post(state) {
166
+ const fences = parses.get(state) ?? [];
167
+ const { tree } = state;
168
+ const issues = [];
169
+ const names = (state.options.plugins ?? []).map((plugin) => plugin.name);
170
+ const shikiLater = names.indexOf("shiki") > names.indexOf("comark-code-notes");
171
+ let preOrder = 0;
172
+ const walk = (nodes, start) => {
173
+ for (let index = start; index < nodes.length; index++) {
174
+ const node = nodes[index];
175
+ if (!node || !isElement(node)) continue;
176
+ if (node[0] !== "pre") {
177
+ walk(node, 2);
178
+ continue;
179
+ }
180
+ preOrder++;
181
+ const fence = fences.find((candidate) => candidate.order === preOrder);
182
+ if (!fence || !fence.markers.size) continue;
183
+ if (!fence.notes) {
184
+ issues.push({
185
+ rule: "comark/code-notes/missing-notes",
186
+ message: `${fence.name} has markers like \`<1>\` but no numbered list right after it, so they stay in the code. Write the notes as a numbered list right after the fence.`
187
+ });
188
+ continue;
189
+ }
190
+ const next = nodes.findIndex((child, after) => after > index && isElement(child));
191
+ const list = nodes[next];
192
+ const code = childrenOf(node).find((child) => isElement(child) && child[0] === "code");
193
+ if (!list || !isElement(list) || list[0] !== "ol" || !code) continue;
194
+ const uid = `cn-${hash(`${fence.name}\n${fence.code}`)}`;
195
+ draw(code, fence, uid, new Set(itemsOf(list).map(({ note }) => note)));
196
+ nodes[next] = notesOf(list, fence, uid, issues);
197
+ }
198
+ };
199
+ walk(tree.nodes, 0);
200
+ if (shikiLater && fences.some((fence) => fence.notes)) issues.push({
201
+ rule: "comark/code-notes/shiki-order",
202
+ message: "Code notes: `shiki` comes after `comark-code-notes` in `plugins`, so the annotated code isn't highlighted. Put the code notes plugin after `shiki`."
203
+ });
204
+ tree.meta.issues = [...tree.meta.issues ?? [], ...issues.map((issue) => issue.message)];
205
+ tree.meta.issueRules = {
206
+ ...tree.meta.issueRules,
207
+ ...Object.fromEntries(issues.map((issue) => [issue.message, issue.rule]))
208
+ };
209
+ }
210
+ };
211
+ });
212
+ //#endregion
213
+ export { NOTES_TAG, src_default as default };
@@ -0,0 +1,16 @@
1
+ import _default, { CodeNotesAttributes, CodeNotesMeta, NOTES_TAG } from "./index.mjs";
2
+ import { ReactNode } from "react";
3
+ //#region src/react.d.ts
4
+ /** The notes under an annotated fence, for `@comark/react`. */
5
+ export declare function CodeNotes({ start, children }: CodeNotesAttributes & {
6
+ children?: ReactNode;
7
+ }): import("react").DetailedReactHTMLElement<{
8
+ className: string;
9
+ start: number | undefined;
10
+ }, HTMLElement>;
11
+ /** Pass these as `components` to `@comark/react`. */
12
+ export declare const components: {
13
+ CodeNotes: typeof CodeNotes;
14
+ };
15
+ //#endregion
16
+ export { type CodeNotesAttributes, type CodeNotesMeta, NOTES_TAG, _default as default };
package/dist/react.mjs ADDED
@@ -0,0 +1,16 @@
1
+ import src_default, { NOTES_TAG } from "./index.mjs";
2
+ import { addStyles } from "./client.mjs";
3
+ import { createElement, useEffect } from "react";
4
+ //#region src/react.ts
5
+ /** The notes under an annotated fence, for `@comark/react`. */
6
+ function CodeNotes({ start, children }) {
7
+ useEffect(addStyles, []);
8
+ return createElement("ol", {
9
+ className: "cn-notes",
10
+ start: start && start !== 1 ? start : void 0
11
+ }, children);
12
+ }
13
+ /** Pass these as `components` to `@comark/react`. */
14
+ const components = { CodeNotes };
15
+ //#endregion
16
+ export { CodeNotes, NOTES_TAG, components, src_default as default };
package/dist/style.css ADDED
@@ -0,0 +1 @@
1
+ @layer comark{:where(.cn-marker,.cn-notes){--_comark-background:var(--comark-background,var(--ui-bg,var(--background,#fff)));--_comark-foreground:var(--comark-foreground,var(--ui-text-highlighted,var(--foreground,#0a0a0a)));--_comark-muted:var(--comark-muted,var(--ui-text-muted,var(--muted-foreground,#737373)));--_comark-border:var(--comark-border,var(--ui-border,var(--border,#e5e5e5)));--_comark-accent:var(--comark-accent,var(--ui-bg-elevated,var(--accent,#f5f5f5)));--_comark-ring:var(--comark-ring,var(--ui-primary,var(--ring,#a1a1a1)));--_comark-radius:var(--comark-radius,calc(var(--ui-radius,calc(var(--radius,.625rem)/2))*2));--_comark-font:var(--comark-font,var(--font-sans,ui-sans-serif,system-ui,sans-serif))}@media (prefers-color-scheme:dark){:where(.cn-marker,.cn-notes){--_comark-background:var(--comark-background,var(--ui-bg,var(--background,#0a0a0a)));--_comark-foreground:var(--comark-foreground,var(--ui-text-highlighted,var(--foreground,#fafafa)));--_comark-muted:var(--comark-muted,var(--ui-text-muted,var(--muted-foreground,#a1a1a1)));--_comark-border:var(--comark-border,var(--ui-border,var(--border,rgb(255 255 255/.1))));--_comark-accent:var(--comark-accent,var(--ui-bg-elevated,var(--accent,#262626)));--_comark-ring:var(--comark-ring,var(--ui-primary,var(--ring,#737373)))}}:where(.cn-marker){--cn-marker:var(--primary,var(--_comark-foreground));--cn-marker-text:var(--cn-marker)}.cn-marker{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;min-width:1.25rem;height:1.25rem;margin-inline-start:.5rem;padding:0 .3rem;border-radius:999px;background:color-mix(in srgb,var(--cn-marker) 10%,var(--_comark-background));box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--cn-marker) 22%,transparent);font:600 .6875rem/1 var(--_comark-font);font-variant-numeric:tabular-nums;vertical-align:.0625rem;user-select:none;transition:background-color .15s}.cn-marker{color:var(--cn-marker-text)!important;text-decoration:none!important;border:0!important}.cn-marker::after{content:attr(data-note)}a.cn-marker:hover{background:color-mix(in srgb,var(--cn-marker) 18%,var(--_comark-background))}a.cn-marker:focus-visible{outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--_comark-ring) 50%,transparent)}.cn-marker:target{box-shadow:0 0 0 3px color-mix(in srgb,var(--_comark-ring) 60%,transparent)}.line:has(>.cn-marker:is(:target,:focus)){border-image:conic-gradient(color-mix(in srgb,var(--cn-marker,var(--primary,currentColor)) 14%,transparent) 0 0) fill 0//.25em 100vw}.cn-notes{margin:.75rem 0 1.5rem}.cn-notes{padding:0!important;list-style:none!important}.cn-notes>li{display:grid;grid-template-columns:auto minmax(0,1fr);align-items:baseline;gap:.625rem;margin:.375rem 0;padding:.125rem .375rem;border-radius:calc(var(--_comark-radius) - 4px)}.cn-notes>li>.cn-marker{margin:0}.cn-notes>li>div>:first-child{margin-top:0!important}.cn-notes>li>div>:last-child{margin-bottom:0!important}.cn-notes>li:target{background:var(--_comark-accent)}}
@@ -0,0 +1,83 @@
1
+ //#region ../zoom/dist/theme.mjs
2
+ const tokens = [
3
+ [
4
+ "background",
5
+ "ui-bg",
6
+ "background",
7
+ "#fff",
8
+ "#0a0a0a"
9
+ ],
10
+ [
11
+ "foreground",
12
+ "ui-text-highlighted",
13
+ "foreground",
14
+ "#0a0a0a",
15
+ "#fafafa"
16
+ ],
17
+ [
18
+ "muted",
19
+ "ui-text-muted",
20
+ "muted-foreground",
21
+ "#737373",
22
+ "#a1a1a1"
23
+ ],
24
+ [
25
+ "border",
26
+ "ui-border",
27
+ "border",
28
+ "#e5e5e5",
29
+ "rgb(255 255 255/.1)"
30
+ ],
31
+ [
32
+ "accent",
33
+ "ui-bg-elevated",
34
+ "accent",
35
+ "#f5f5f5",
36
+ "#262626"
37
+ ],
38
+ [
39
+ "ring",
40
+ "ui-primary",
41
+ "ring",
42
+ "#a1a1a1",
43
+ "#737373"
44
+ ]
45
+ ];
46
+ const resolve = (scheme) => tokens.map((token) => `--_comark-${token[0]}:var(--comark-${token[0]},var(--${token[1]},var(--${token[2]},${token[scheme]})))`).join(";");
47
+ /**
48
+ * Resolves the `--comark-*` tokens on `selector`, falling back to Nuxt UI's variables, then shadcn/ui's, then shadcn's
49
+ * neutral theme. Nuxt UI's corners are twice its `--ui-radius`, so the radius halves shadcn's to double it back.
50
+ * They resolve on the component rather than `:root`, so a `.dark` class anywhere above it switches the theme, and a
51
+ * `--comark-*` set on any ancestor still inherits down. `:where()` keeps them at zero specificity.
52
+ */
53
+ function theme(selector) {
54
+ const where = `:where(${selector})`;
55
+ return `${where}{${resolve(3)};--_comark-radius:var(--comark-radius,calc(var(--ui-radius,calc(var(--radius,.625rem)/2))*2));--_comark-font:var(--comark-font,var(--font-sans,ui-sans-serif,system-ui,sans-serif))}@media (prefers-color-scheme:dark){${where}{${resolve(4)}}}`;
56
+ }
57
+ //#endregion
58
+ //#region src/styles.ts
59
+ /**
60
+ * Round numbered markers at the ends of lines and before each note, as a soft tint of shadcn/ui's `--primary` when the
61
+ * page has it, or of the foreground otherwise, with the number in that color, on the shared `--comark-*` tokens.
62
+ * Override `--cn-marker` and `--cn-marker-text`. The number is drawn by CSS, so copying the code never takes it along.
63
+ * The rules sit in the `comark` layer, so any unlayered rule of the host's wins.
64
+ */
65
+ const css = `@layer comark{${[
66
+ theme(".cn-marker,.cn-notes"),
67
+ ":where(.cn-marker){--cn-marker:var(--primary,var(--_comark-foreground));--cn-marker-text:var(--cn-marker)}",
68
+ ".cn-marker{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;min-width:1.25rem;height:1.25rem;margin-inline-start:.5rem;padding:0 .3rem;border-radius:999px;background:color-mix(in srgb,var(--cn-marker) 10%,var(--_comark-background));box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--cn-marker) 22%,transparent);font:600 .6875rem/1 var(--_comark-font);font-variant-numeric:tabular-nums;vertical-align:.0625rem;user-select:none;transition:background-color .15s}",
69
+ ".cn-marker{color:var(--cn-marker-text)!important;text-decoration:none!important;border:0!important}",
70
+ ".cn-marker::after{content:attr(data-note)}",
71
+ "a.cn-marker:hover{background:color-mix(in srgb,var(--cn-marker) 18%,var(--_comark-background))}",
72
+ "a.cn-marker:focus-visible{outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--_comark-ring) 50%,transparent)}",
73
+ ".cn-marker:target{box-shadow:0 0 0 3px color-mix(in srgb,var(--_comark-ring) 60%,transparent)}",
74
+ ".line:has(>.cn-marker:is(:target,:focus)){border-image:conic-gradient(color-mix(in srgb,var(--cn-marker,var(--primary,currentColor)) 14%,transparent) 0 0) fill 0//.25em 100vw}",
75
+ ".cn-notes{margin:.75rem 0 1.5rem}",
76
+ ".cn-notes{padding:0!important;list-style:none!important}",
77
+ ".cn-notes>li{display:grid;grid-template-columns:auto minmax(0,1fr);align-items:baseline;gap:.625rem;margin:.375rem 0;padding:.125rem .375rem;border-radius:calc(var(--_comark-radius) - 4px)}",
78
+ ".cn-notes>li>.cn-marker{margin:0}",
79
+ ".cn-notes>li>div>:first-child{margin-top:0!important}.cn-notes>li>div>:last-child{margin-bottom:0!important}",
80
+ ".cn-notes>li:target{background:var(--_comark-accent)}"
81
+ ].join("")}}`;
82
+ //#endregion
83
+ export { css as t };
@@ -0,0 +1,10 @@
1
+ <!-- The notes under an annotated fence, for `@comark/svelte`. -->
2
+ <script>
3
+ import { onMount } from "svelte";
4
+ import { addStyles } from "comark-code-notes/client";
5
+
6
+ let { start = undefined, children } = $props();
7
+ onMount(addStyles);
8
+ </script>
9
+
10
+ <ol class="cn-notes" start={start && start !== 1 ? start : undefined}>{@render children?.()}</ol>
@@ -0,0 +1,8 @@
1
+ import type { Component, Snippet } from "svelte";
2
+ import type { CodeNotesAttributes } from "comark-code-notes";
3
+
4
+ export { default, NOTES_TAG } from "comark-code-notes";
5
+ export type { CodeNotesAttributes, CodeNotesMeta } from "comark-code-notes";
6
+
7
+ export declare const CodeNotes: Component<CodeNotesAttributes & { children?: Snippet }>;
8
+ export declare const components: { CodeNotes: typeof CodeNotes };
@@ -0,0 +1,6 @@
1
+ // The notes under an annotated fence, for `@comark/svelte`. Pass `components` to it; the parse plugin comes from `comark-code-notes`.
2
+ import CodeNotes from "./CodeNotes.svelte";
3
+
4
+ export { default, NOTES_TAG } from "comark-code-notes";
5
+ export { CodeNotes };
6
+ export const components = { CodeNotes };
package/dist/vue.d.mts ADDED
@@ -0,0 +1,22 @@
1
+ import _default, { CodeNotesAttributes, CodeNotesMeta, NOTES_TAG } from "./index.mjs";
2
+ //#region src/vue.d.ts
3
+ /** The notes under an annotated fence, for `@comark/vue`. */
4
+ export declare const CodeNotes: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
5
+ start: NumberConstructor;
6
+ }>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
7
+ [key: string]: any;
8
+ }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
9
+ start: NumberConstructor;
10
+ }>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
11
+ /** Pass these as `components` to `@comark/vue`. */
12
+ export declare const components: {
13
+ CodeNotes: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
14
+ start: NumberConstructor;
15
+ }>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
16
+ [key: string]: any;
17
+ }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
18
+ start: NumberConstructor;
19
+ }>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
20
+ };
21
+ //#endregion
22
+ export { type CodeNotesAttributes, type CodeNotesMeta, NOTES_TAG, _default as default };
package/dist/vue.mjs ADDED
@@ -0,0 +1,21 @@
1
+ import src_default, { NOTES_TAG } from "./index.mjs";
2
+ import { addStyles } from "./client.mjs";
3
+ import { defineComponent, h, onMounted } from "vue";
4
+ //#region src/vue.ts
5
+ /** The notes under an annotated fence, for `@comark/vue`. */
6
+ const CodeNotes = defineComponent({
7
+ name: "CodeNotes",
8
+ inheritAttrs: false,
9
+ props: { start: Number },
10
+ setup(props, { slots }) {
11
+ onMounted(addStyles);
12
+ return () => h("ol", {
13
+ class: "cn-notes",
14
+ start: props.start && props.start !== 1 ? props.start : void 0
15
+ }, slots.default?.());
16
+ }
17
+ });
18
+ /** Pass these as `components` to `@comark/vue`. */
19
+ const components = { CodeNotes };
20
+ //#endregion
21
+ export { CodeNotes, NOTES_TAG, components, src_default as default };
package/package.json CHANGED
@@ -1,12 +1,91 @@
1
1
  {
2
2
  "name": "comark-code-notes",
3
- "version": "0.0.0",
4
- "description": "Placeholder. The first release is published from https://github.com/onmax/comark-plugins.",
3
+ "version": "0.7.1",
4
+ "description": "Numbered notes on lines of code: a // <1> marker in a fence links to the numbered list after it. A Comark plugin.",
5
+ "keywords": [
6
+ "annotations",
7
+ "callouts",
8
+ "code",
9
+ "comark",
10
+ "markdown",
11
+ "mdc",
12
+ "plugin",
13
+ "shiki"
14
+ ],
15
+ "homepage": "https://comark-ui.onmax.me/code-notes",
16
+ "bugs": "https://github.com/onmax/comark-ui/issues",
5
17
  "license": "MIT",
6
18
  "author": "onmax",
7
19
  "repository": {
8
20
  "type": "git",
9
- "url": "git+https://github.com/onmax/comark-plugins.git",
21
+ "url": "git+https://github.com/onmax/comark-ui.git",
10
22
  "directory": "packages/code-notes"
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "type": "module",
28
+ "sideEffects": [
29
+ "*.css"
30
+ ],
31
+ "exports": {
32
+ ".": "./dist/index.mjs",
33
+ "./client": "./dist/client.mjs",
34
+ "./html": "./dist/html.mjs",
35
+ "./react": "./dist/react.mjs",
36
+ "./style.css": "./dist/style.css",
37
+ "./svelte": {
38
+ "types": "./dist/svelte/index.d.ts",
39
+ "svelte": "./dist/svelte/index.js",
40
+ "default": "./dist/svelte/index.js"
41
+ },
42
+ "./vue": "./dist/vue.mjs",
43
+ "./package.json": "./package.json"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public",
47
+ "provenance": true
48
+ },
49
+ "devDependencies": {
50
+ "@comark/html": "^0.8.1",
51
+ "@comark/react": "^0.8.1",
52
+ "@comark/vue": "^0.8.1",
53
+ "@types/react": "^19.3.0",
54
+ "@types/react-dom": "^19.3.0",
55
+ "comark": "^0.8.1",
56
+ "react": "^19.3.1",
57
+ "react-dom": "^19.3.1",
58
+ "svelte": "^5.57.2",
59
+ "vue": "^3.5.44",
60
+ "comark-zoom": "0.7.1"
61
+ },
62
+ "peerDependencies": {
63
+ "@comark/html": "^0.8.0",
64
+ "comark": "^0.8.0",
65
+ "react": ">=18",
66
+ "svelte": ">=5",
67
+ "vue": ">=3.5"
68
+ },
69
+ "peerDependenciesMeta": {
70
+ "@comark/html": {
71
+ "optional": true
72
+ },
73
+ "react": {
74
+ "optional": true
75
+ },
76
+ "svelte": {
77
+ "optional": true
78
+ },
79
+ "vue": {
80
+ "optional": true
81
+ }
82
+ },
83
+ "engines": {
84
+ "node": ">=22.12.0"
85
+ },
86
+ "scripts": {
87
+ "build": "tsdown",
88
+ "dev": "tsdown --watch",
89
+ "doctor": "vite-doctor . --no-cache --max-warnings 0 --format text"
11
90
  }
12
- }
91
+ }