Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# comark-plugins

Comark plugins for review documents: annotated screenshots, before/after comparisons, and suggested code changes. Agents write most of the documents they render, often as PR evidence. Because of that, the issue messages are as important as the HTML: every one names its block and says how to fix it.
Comark plugins for review documents: annotated screenshots, before/after comparisons, suggested code changes, and charts. Agents write most of the documents they render, often as PR evidence. Because of that, the issue messages are as important as the HTML: every one names its block and says how to fix it.

## Shape

Expand All @@ -12,7 +12,15 @@ A pnpm workspace. Every package in `packages/` is published to npm under its own
- `src/html.ts` is the `@comark/html` renderer. The output reads without a script; the zoom and carousel scripts and all styles are inlined once per document. A screenshot zooms with `comark-zoom`, and `::compare` is a two-column `comark-carousel`. Marks are not links, so a click anywhere on a screenshot zooms it. `::diff` is a table of before and after lines with no script, and doesn't zoom: its code scrolls in place.
- Only the HTML renderer exists: notes are Markdown, and Vue, React, and Svelte versions would each copy the whole figure template.
- `src/marks.ts` holds the hand-drawn marks, taken from AnnotationKit.
- `readMark` (from the root entry) and `annotateHtml` (from `/html`) are public so comark-charts can mark charts: one checks an annotation's mark, color, side, and note without its box, and the other draws annotations over any HTML.
- `packages/carousel` is `comark-carousel`. Its parse plugin turns `::carousel` content into explicit `slide` nodes with their position, so no renderer splits children. Its script is built like zoom's, and a carousel is a zoom gallery.
- `packages/charts` is `comark-charts`.
- `src/read.ts` reads a `::chart` node and its Markdown table and reports their issues. The plugin (`src/index.ts`) and the renderer both go through it; the parse tree keeps the table as the data, so agents reading the Markdown or the page get numbers, not a spec.
- `src/draw.ts` turns the table into a Flint spec with a house theme and draws it to SVG with Vega, when rendering, so the parse plugin stays light. Field names are fixed (`x`, `y`, `series`) because Vega-Lite reads dots in a name as a path.
- `src/styles.ts` swaps the SVG's inks for the `--comark-*` tokens and `--chart-N` with attribute selectors, since CSS beats presentation attributes. Keep `INK` and `SERIES` in `draw.ts` in step with it.
- Marks are comark-annotate annotations with `at` (a row's x) and `series` instead of a box. `read.ts` checks them with annotate's `readMark`; `draw.ts` finds each one's box in Vega's scenegraph (lines and areas get an invisible point layer) and `html.ts` draws them with annotate's `annotateHtml`, so they look and behave like marks on a screenshot. annotate skips `::chart` blocks when it checks annotations.
- Only the HTML renderer exists: Vega draws on the server, and a framework component would ship it to the browser.
- `skills/comark-charts/SKILL.md` is its agent skill. `docs/build.ts` serves every package's skill for `npx skills add`; add a new skill's folder there.
- `packages/embed` is `comark-embed`. Its parse plugin reads the player's `src` from the `::embed` link alone, with no fetch, and replaces the node's attributes with `provider`, `src`, `label`, `size`, `href`, `title`, and `host`, so no renderer parses URLs. `src/providers.ts` holds one entry per provider. A link with no player becomes a card, and one that isn't https renders nothing. No script: the components add the styles on mount.
- `packages/github-callouts` is `comark-github-callouts`. Its parse plugin is named `alert`, so Comark's dedupe swaps it in for the built-in alert plugin and it always sees the raw `[!TYPE]` marker. It turns those blockquotes into `github-callout` nodes with `type` and `title`; the renderers only draw them. No script: the components add the styles on mount.
- `packages/lint` is `comark-lint`, a library, CLI, and Vite Doctor extension that lints Comark documents.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
| --- | --- |
| [`comark-annotate`](./packages/annotate) | Hand-drawn marks, numbered notes, and arrows on screenshots, and before/after comparisons. |
| [`comark-carousel`](./packages/carousel) | A carousel of any Markdown content, with arrows, a counter, keys, and zoom. |
| [`comark-charts`](./packages/charts) | Bars, lines, areas, scatter plots, dumbbells, waterfalls, and pies from a Markdown table, drawn to SVG with the table kept under them. |
| [`comark-embed`](./packages/embed) | YouTube, Vimeo, Loom, Spotify, CodePen, StackBlitz, and Figma players from a link, with no network fetch. |
| [`comark-github-callouts`](./packages/github-callouts) | GitHub's `> [!NOTE]` alerts as callouts with an icon and a title. |
| [`comark-lint`](./packages/lint) | Finds unclosed blocks, unknown components, broken anchors, and plugin issues in Comark documents, from a script, the command line, or Vite Doctor. |
Expand Down
48 changes: 28 additions & 20 deletions docs/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import dark from "shiki/themes/vitesse-dark.mjs";
import light from "shiki/themes/vitesse-light.mjs";
import annotate, { components as annotateComponents } from "../packages/annotate/src/html.ts";
import carousel, { components as carouselComponents } from "../packages/carousel/src/html.ts";
import charts, { components as chartsComponents } from "../packages/charts/src/html.ts";
import embed, { components as embedComponents } from "../packages/embed/src/html.ts";
import githubCallouts, {
components as githubCalloutsComponents,
Expand All @@ -28,6 +29,7 @@ const contentPlugins = () => [
include({ root: fileURLToPath(new URL("pages/", root)) }),
annotate(),
carousel(),
charts(),
embed(),
githubCallouts(),
tabs(),
Expand All @@ -37,6 +39,7 @@ const plugins = [...contentPlugins(), shiki({ themes: { light, dark } })];
const components = {
...annotateComponents,
...carouselComponents,
...chartsComponents,
...embedComponents,
...githubCalloutsComponents,
...tabsComponents,
Expand All @@ -54,6 +57,7 @@ const pages = [
{ slug: "lint", file: "lint.md", npm: "comark-lint" },
{ slug: "include", file: "include.md", npm: "comark-include" },
{ slug: "embed", file: "embed.md", npm: "comark-embed" },
{ slug: "charts", file: "charts.md", npm: "comark-charts" },
];

const isElement = (node: Node): node is ElementNode => Array.isArray(node) && node[0] !== null;
Expand Down Expand Up @@ -209,36 +213,40 @@ await cp(
new URL("fonts/Caveat-Variable.woff2", dist),
);

// The package's agent skill, served for Agent Skills Discovery: v0.2 with a digest, and v0.1, which `npx skills add` reads.
const skill = await readFile(
new URL("../packages/annotate/skills/comark-annotate/SKILL.md", root),
"utf8",
// The packages' agent skills, served for Agent Skills Discovery: v0.2 with a digest, and v0.1, which `npx skills add` reads.
const skills = await Promise.all(
["annotate/skills/comark-annotate", "charts/skills/comark-charts"].map(async (folder) => {
const text = await readFile(new URL(`../packages/${folder}/SKILL.md`, root), "utf8");
const meta = (await parseMarkdown(text)).frontmatter;
return { text, name: str(meta.name), description: str(meta.description) };
}),
);
const skillMeta = (await parseMarkdown(skill)).frontmatter;
const [skillName, skillDescription] = [str(skillMeta.name), str(skillMeta.description)];
for (const base of ["agent-skills", "skills"]) {
await mkdir(new URL(`.well-known/${base}/${skillName}/`, dist), { recursive: true });
await writeFile(new URL(`.well-known/${base}/${skillName}/SKILL.md`, dist), skill);
}
for (const skill of skills)
for (const base of ["agent-skills", "skills"]) {
await mkdir(new URL(`.well-known/${base}/${skill.name}/`, dist), { recursive: true });
await writeFile(new URL(`.well-known/${base}/${skill.name}/SKILL.md`, dist), skill.text);
}
await writeFile(
new URL(".well-known/agent-skills/index.json", dist),
JSON.stringify({
$schema: "https://schemas.agentskills.io/discovery/0.2.0/schema.json",
skills: [
{
name: skillName,
type: "skill-md",
description: skillDescription,
url: `/.well-known/agent-skills/${skillName}/SKILL.md`,
digest: `sha256:${createHash("sha256").update(skill).digest("hex")}`,
},
],
skills: skills.map((skill) => ({
name: skill.name,
type: "skill-md",
description: skill.description,
url: `/.well-known/agent-skills/${skill.name}/SKILL.md`,
digest: `sha256:${createHash("sha256").update(skill.text).digest("hex")}`,
})),
}),
);
await writeFile(
new URL(".well-known/skills/index.json", dist),
JSON.stringify({
skills: [{ name: skillName, description: skillDescription, files: ["SKILL.md"] }],
skills: skills.map((skill) => ({
name: skill.name,
description: skill.description,
files: ["SKILL.md"],
})),
}),
);

Expand Down
Loading
Loading