@geml/geml
Advanced tools
Sorry, the diff of this file is not supported yet
| --- | ||
| name: geml | ||
| description: >- | ||
| Read, author, edit, or validate GEML — the General Expressive Markup Language | ||
| (.geml files) and its .gemlhistory versioning sidecar. Use whenever creating | ||
| or modifying a .geml/.gemlhistory file, converting Markdown to GEML, or when | ||
| the user mentions GEML, typed blocks, === fences, or geml-chart. Ensures the | ||
| output parses cleanly (zero error diagnostics) against the reference parser. | ||
| --- | ||
| # Writing and reading GEML correctly | ||
| GEML expresses **every** kind of structured content — code, tables, diagrams, | ||
| math, callouts, metadata — through **one** primitive: the **typed block** | ||
| (`=== <type> {#id .class key=val}` … `===`). Always finish by **validating**: a | ||
| GEML file is correct only when `geml check` reports **no error diagnostics** | ||
| (exit 0). | ||
| ## Golden rules (the things that are easy to get wrong) | ||
| 1. **Fences are runs of `=` (≥3).** A block closes at a `=` run of **exactly | ||
| the opening length**, or — when the block has an `#id` — at the labeled | ||
| fence `=== #id` (any `=` run ≥3 followed by the id; no length counting). | ||
| 2. **Nest with longer fences.** A body containing `===` lines needs a | ||
| **longer** outer fence: `====` wraps `===`. Careful: a same-length bare | ||
| `===` in the body closes the block even if you intend a labeled close — | ||
| the labeled close only spares you length-counting, it does NOT protect | ||
| same-length inner fences. | ||
| 3. **Headings are ATX `#` only** (`#`…`######`). No setext underlines, no | ||
| `---` breaks, no YAML frontmatter — metadata is a `=== meta` block, and the | ||
| document TITLE lives there (`title = "…"`), not in an H1. A heading may | ||
| carry a stable explicit id: `## Title {#sec}`. | ||
| 4. **Every `#id` is unique per document**, and **every reference must | ||
| resolve** — `[t](#id)`, `[[#id]]`, `[^id]`, `src=`, `data=`, | ||
| `other.geml#id`. An unresolved reference is a build **error**. | ||
| 5. **No raw HTML.** Notes → `=== note`, comments → `%%` lines, hidden content | ||
| → `{hidden}`, addressable prose → `=== text`, verified data → `=== data` | ||
| (json/jsonl; `code` shows text, `data` IS data). | ||
| ## Validate every time | ||
| ```sh | ||
| geml check file.geml # diagnostics + exit code only; exit 0 = correct | ||
| geml check --json file.geml # machine-readable diagnostics array | ||
| ``` | ||
| If `geml` is not on PATH: `npm i -g @geml/geml` (package `@geml/geml`, command | ||
| `geml`), or run without installing via `npx -y @geml/geml check file.geml`. | ||
| Inside the geml-spec repo prefer the local build: | ||
| `node geml-parser/dist/geml.js <args>`. If no parser is reachable, follow the | ||
| golden rules and validate once it is. | ||
| ## Work blockwise (agent editing) | ||
| ```sh | ||
| geml get file.geml '#id' # read ONE block (a heading id = its whole section) | ||
| geml set file.geml '#id' --in f # replace ONE block (re-parsed; never writes a broken doc) | ||
| geml history save file.geml -m "…" # snapshot to .gemlhistory after each meaningful edit | ||
| geml revert file.geml '#id' # roll ONE block back (--rev -2 | changed | <rev-id>) | ||
| ``` | ||
| ## Full reference — pull ONE section, not the whole file | ||
| `references/authoring.geml` (under this skill's base directory) holds the | ||
| detailed reference. Fetch just the section you need: | ||
| ```sh | ||
| geml get <skill-base>/references/authoring.geml '#tables' | ||
| ``` | ||
| | section | covers | | ||
| |---|---| | ||
| | `#typed-block` | block anatomy, attribute object, examples of every registered type | | ||
| | `#tables` | pipe/CSV bodies, `compute=`, `summary=`, printf display, `span=` merges | | ||
| | `#charts` | `geml-chart` diagrams bound to a table via `data=#id` | | ||
| | `#data` | the `data` block — value tree, `json`/`jsonl` formats, blind append, chart binding | | ||
| | `#inline` | inline markup, links/refs/footnotes, task lists, media embeds | | ||
| | `#hidden` | `%%` comments, `{hidden}`, `{{key}}` interpolation, `=== embed` | | ||
| | `#cli` | every CLI verb — get/set/add/delete/rename, `--to` conversion, check | | ||
| | `#editing` | the blockwise editing loop + `.gemlhistory` versioning | | ||
| | `#project-config` | carrying a project's Claude config docs in GEML, quietly | | ||
| | `#checklist` | full pre-flight authoring checklist | |
@@ -15,2 +15,5 @@ // Stubs for the Node built-ins that geml-parser imports for its CLI/history | ||
| export const statSync = () => ({ isDirectory: () => false }); | ||
| export const mkdirSync = () => {}; | ||
| export const readdirSync = () => []; | ||
| export const copyFileSync = () => {}; | ||
| export const basename = (p) => p; | ||
@@ -25,2 +28,4 @@ export const dirname = (p) => p; | ||
| export const spawnSync = () => ({ status: 1 }); | ||
| // node:os — `geml skill install` resolves the home directory; CLI-only. | ||
| export const homedir = () => "/"; | ||
| export const createHash = () => ({ | ||
@@ -27,0 +32,0 @@ update() { return this; }, |
+10
-3
@@ -42,5 +42,12 @@ #!/usr/bin/env node | ||
| const shq = (s) => `"${String(s).replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1')}"`; | ||
| // A codemap document's `src=` routes are written relative to the indexed | ||
| // SOURCE root (`geml-parser/src/attrs.ts` from a document two levels down), so | ||
| // checking one has to be told that root — the same value `serve` derives, from | ||
| // the recorded recipe, falling back to the parent of the graph directory. | ||
| let srcRoot = resolve(rootDir, ".."); | ||
| try { srcRoot = resolve(rootDir, JSON.parse(readFileSync(join(rootDir, "_index", "refresh.json"), "utf8")).root ?? ".."); } catch { /* no recipe: parent */ } | ||
| const checkArgs = (file) => ["check", "--root", srcRoot, file]; | ||
| const runCheck = (file) => { | ||
| // The built-parser path: run node on geml.js directly (array args, no shell). | ||
| if (cli.endsWith(".js")) return spawnSync(process.execPath, [cli, "check", file], { encoding: "utf8" }); | ||
| if (cli.endsWith(".js")) return spawnSync(process.execPath, [cli, ...checkArgs(file)], { encoding: "utf8" }); | ||
| // A non-.js cli may be a .cmd/.bat launcher (e.g. geml.cmd on PATH), which | ||
@@ -50,5 +57,5 @@ // Node can only spawn through the shell. Hand cmd.exe ONE pre-escaped command | ||
| if (process.platform === "win32") { | ||
| return spawnSync([q(cli), ...["check", file].map(shq)].join(" "), { encoding: "utf8", shell: true }); | ||
| return spawnSync([q(cli), ...checkArgs(file).map(shq)].join(" "), { encoding: "utf8", shell: true }); | ||
| } | ||
| return spawnSync(cli, ["check", file], { encoding: "utf8" }); | ||
| return spawnSync(cli, checkArgs(file), { encoding: "utf8" }); | ||
| }; | ||
@@ -55,0 +62,0 @@ if (!existsSync(localParser)) { |
+1
-0
@@ -6,2 +6,3 @@ import { type DiagnosticCode } from "./diagnostics.js"; | ||
| export type RowScope = "data" | "all" | "summary"; | ||
| export declare const USES: Record<ChartType, Set<string>>; | ||
| export interface ChartDataset { | ||
@@ -8,0 +9,0 @@ categories: string[]; |
+4
-1
@@ -11,3 +11,6 @@ // GEML reference parser — chart-from-table (§7 geml-chart renderer). | ||
| // Channels each type can use; supplying any other is a warning (ignored). | ||
| const USES = { | ||
| // Exported for the record-array projection (GEP-0005): it must validate only | ||
| // the channels the chart TYPE actually reads, matching buildChart's leniency | ||
| // (an unused channel is a warning here, so it must not be an error there). | ||
| export const USES = { | ||
| bar: new Set(["x", "y", "series"]), | ||
@@ -14,0 +17,0 @@ line: new Set(["x", "y", "series"]), |
@@ -1,2 +0,2 @@ | ||
| export type DiagnosticCode = "unterminated-block" | "unknown-block-type" | "unknown-attribute" | "block-nesting-too-deep" | "list-nesting-too-deep" | "inline-nesting-too-deep" | "duplicate-id" | "unresolved-reference" | "unresolved-footnote" | "unresolved-cross-document-reference" | "unresolvable-document" | "unchecked-cross-document-reference" | "embed-missing-src" | "ignored-embed-body" | "transclusion-cycle" | "embed-target-not-geml" | "media-target-is-document" | "inline-transclusion-not-inline" | "unsafe-embed-scheme" | "unresolvable-table-source" | "table-source-not-a-table" | "unknown-metadata-reference" | "table-src-and-body" | "unknown-table-format" | "bad-compute-formula" | "unlexable-compute-formula" | "compute-error" | "compute-non-numeric-cell" | "compute-not-a-number" | "bad-summary-entry" | "summary-unknown-column" | "unlexable-summary-expression" | "summary-error" | "unknown-diagram-format" | "ignored-diagram-body" | "code-graph-missing-src" | "code-graph-unresolvable-document" | "chart-missing-data" | "chart-data-not-a-table" | "chart-missing-type" | "chart-unknown-type" | "chart-unknown-rows-scope" | "chart-missing-channel" | "chart-empty-channel" | "chart-unknown-column" | "chart-unused-channel" | "chart-missing-summary-row" | "chart-summary-row-unavailable" | "chart-non-numeric-value"; | ||
| export type DiagnosticCode = "unterminated-block" | "unknown-block-type" | "unknown-attribute" | "block-nesting-too-deep" | "list-nesting-too-deep" | "inline-nesting-too-deep" | "stray-labeled-fence" | "fence-like-line" | "unresolvable-code-source" | "bad-code-source" | "bad-source-range" | "stale-code-snapshot" | "duplicate-id" | "unresolved-reference" | "unresolved-footnote" | "unresolved-cross-document-reference" | "unresolvable-document" | "unchecked-cross-document-reference" | "embed-missing-src" | "ignored-embed-body" | "transclusion-cycle" | "embed-target-not-geml" | "media-target-is-document" | "inline-transclusion-not-inline" | "unsafe-embed-scheme" | "unresolvable-table-source" | "table-source-not-a-table" | "unknown-metadata-reference" | "table-src-and-body" | "unknown-table-format" | "bad-table-delimiter" | "ignored-table-delimiter" | "bad-compute-formula" | "unlexable-compute-formula" | "compute-error" | "compute-non-numeric-cell" | "compute-not-a-number" | "bad-summary-entry" | "summary-unknown-column" | "unlexable-summary-expression" | "summary-error" | "unknown-diagram-format" | "ignored-diagram-body" | "code-graph-missing-src" | "code-graph-unresolvable-document" | "chart-missing-data" | "chart-data-not-a-table" | "chart-missing-type" | "chart-unknown-type" | "chart-unknown-rows-scope" | "chart-missing-channel" | "chart-empty-channel" | "chart-unknown-column" | "chart-unused-channel" | "chart-missing-summary-row" | "chart-summary-row-unavailable" | "chart-non-numeric-value" | "chart-data-not-records" | "data-parse" | "unknown-data-format" | "data-format-no-engine" | "bad-data-schema" | "data-src-and-body" | "bad-data-source" | "unresolvable-data-source"; | ||
| export interface Diagnostic { | ||
@@ -3,0 +3,0 @@ severity: "error" | "warning"; |
+16
-0
@@ -22,2 +22,8 @@ // GEML reference parser — the diagnostic catalogue (spec Appendix A). | ||
| "inline-nesting-too-deep": "error", | ||
| "stray-labeled-fence": "warning", | ||
| "fence-like-line": "warning", | ||
| "unresolvable-code-source": "warning", | ||
| "bad-code-source": "error", | ||
| "bad-source-range": "error", | ||
| "stale-code-snapshot": "warning", | ||
| "duplicate-id": "error", | ||
@@ -41,2 +47,4 @@ "unresolved-reference": "error", | ||
| "unknown-table-format": "warning", | ||
| "bad-table-delimiter": "error", | ||
| "ignored-table-delimiter": "warning", | ||
| "bad-compute-formula": "error", | ||
@@ -67,2 +75,10 @@ "unlexable-compute-formula": "error", | ||
| "chart-non-numeric-value": "error", | ||
| "chart-data-not-records": "error", | ||
| "data-parse": "error", | ||
| "unknown-data-format": "warning", | ||
| "data-format-no-engine": "warning", | ||
| "bad-data-schema": "error", | ||
| "data-src-and-body": "error", | ||
| "bad-data-source": "error", | ||
| "unresolvable-data-source": "error", | ||
| }; | ||
@@ -69,0 +85,0 @@ // --------------------------------------------------------------------------- |
+5
-1
@@ -11,3 +11,3 @@ #!/usr/bin/env node | ||
| export { mdToGeml, type ConvertResult } from "./from-md.js"; | ||
| export { renderHtml } from "./render-html.js"; | ||
| export { renderHtml, pageAssets } from "./render-html.js"; | ||
| export { type RenderOptions } from "./render.js"; | ||
@@ -17,2 +17,5 @@ export { serialize } from "./serialize.js"; | ||
| export type BodyMode = "raw" | "flow" | "data"; | ||
| export type DataValue = null | boolean | number | string | DataValue[] | { | ||
| [key: string]: DataValue; | ||
| }; | ||
| export interface ListItem { | ||
@@ -58,2 +61,3 @@ text: string; | ||
| chart?: ChartModel; | ||
| value?: DataValue; | ||
| hidden?: boolean; | ||
@@ -60,0 +64,0 @@ }; |
+23
-2
@@ -287,2 +287,11 @@ #!/usr/bin/env node | ||
| }, | ||
| view: { | ||
| type: "boolean", | ||
| description: "Read THROUGH an `embed` block to the entity block it stands for, following a multi-layer chain to its end. An `embed` has no content of its own, so this is the only way to see what it points at; on any other block it changes nothing. Returns {from, content}: `from` names the document the content actually came from, and its references and relative paths resolve against THAT document, not this one.", | ||
| }, | ||
| part: { | ||
| type: "string", | ||
| enum: ["whole", "head", "body"], | ||
| description: "How much of the block to return (default: whole). `body` is usually what you want together with `view`.", | ||
| }, | ||
| }, | ||
@@ -294,6 +303,18 @@ required: ["file", "id"], | ||
| const sel = selectorArg(args.id); | ||
| const run = runCli(["get", real, sel]); | ||
| // Same name, same enum, same validation as `geml_set` — one concept for a | ||
| // model to learn, and `body` is already taken there for the replacement text. | ||
| const part = args.part ?? "whole"; | ||
| if (!["whole", "head", "body"].includes(part)) | ||
| throw new Error(`part must be whole|head|body, got \`${part}\``); | ||
| const flag = part === "head" ? ["--head"] : part === "body" ? ["--body"] : []; | ||
| const run = runCli(["get", real, sel, ...flag, ...(args.view ? ["--view"] : [])]); | ||
| if (!run.ok) | ||
| throw new Error(run.stderr || `nothing matches ${sel}`); | ||
| return run.stdout; | ||
| if (!args.view) | ||
| return run.stdout; | ||
| // There is no stderr across an MCP call, and provenance is mandatory: lift | ||
| // it out of the CLI's pinned `view: <sel> -> <doc>[#<id>]` line into a | ||
| // field of its own. | ||
| const m = /^view: .*? -> (.+)$/m.exec(run.stderr); | ||
| return { from: m ? m[1].trim() : null, content: run.stdout }; | ||
| }, | ||
@@ -300,0 +321,0 @@ }, |
| import { type Document } from "./geml.js"; | ||
| import { type RenderOptions } from "./render.js"; | ||
| export declare function renderHtml(doc: Document, opts?: RenderOptions): string; | ||
| export declare const pageAssets: { | ||
| readonly css: "\n:root { --fg:#1f2328; --muted:#656d76; --bd:#d0d7de; --bg:#fff; --accent:#2563eb; --code-bg:#f6f8fa; }\n* { box-sizing: border-box; }\nbody { margin:0; color:var(--fg); background:#fafbfc; font:16px/1.6 -apple-system,BlinkMacSystemFont,\"Segoe UI\",Helvetica,Arial,\"PingFang SC\",\"Microsoft Yahei\",sans-serif; }\nmain { max-width: 860px; margin: 0 auto; padding: 48px 24px 96px; background:var(--bg); }\nh1,h2,h3,h4,h5,h6 { line-height:1.25; margin:1.6em 0 .6em; scroll-margin-top:16px; }\nh1 { font-size:2em; border-bottom:1px solid var(--bd); padding-bottom:.3em; }\nh2 { font-size:1.5em; border-bottom:1px solid var(--bd); padding-bottom:.3em; }\nh3 { font-size:1.25em; } h4 { font-size:1em; }\np { margin:.7em 0; }\na { color:var(--accent); text-decoration:none; } a:hover { text-decoration:underline; }\ncode { background:var(--code-bg); padding:.15em .35em; border-radius:6px; font:.88em ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }\npre { background:var(--code-bg); padding:14px 16px; border-radius:8px; overflow:auto; }\npre code { background:none; padding:0; font-size:.85em; }\npre.output { background:#0d1117; color:#e6edf3; }\npre.output code { color:inherit; }\nul,ol { padding-left:1.6em; } li { margin:.2em 0; }\nul.task-list { list-style:none; padding-left:.2em; }\nli.task input[type=checkbox] { appearance:none; -webkit-appearance:none; width:1.1em; height:1.1em; margin:0 .5em 0 0; vertical-align:-.2em; border:1.5px solid #c8ccd0; border-radius:4px; background:#fff; position:relative; opacity:1; cursor:default; box-sizing:border-box; }\nli.task input[type=checkbox]:checked { background-color:#1f883d; border-color:#1f883d; }\nli.task input[type=checkbox]:checked::after { content:\"✓\"; position:absolute; top:0; right:0; bottom:0; left:0; display:flex; align-items:center; justify-content:center; color:#fff; font-size:.8em; line-height:1; font-weight:700; }\naside.callout { border-left:4px solid var(--accent); background:#f0f6ff; padding:.4em 16px; border-radius:0 8px 8px 0; margin:1em 0; }\naside.aside { border-left-color:#8b949e; background:#f6f8fa; }\naside.warning { border-left-color:#d97706; background:#fff8f0; }\naside.callout > :first-child { margin-top:0; } aside.callout > :last-child { margin-bottom:0; }\nfigure { margin:1.2em 0; }\nfigcaption { color:var(--muted); font-size:.86em; text-align:center; margin-top:.5em; }\ntable.geml-table { border-collapse:collapse; width:100%; font-size:.92em; }\ntable.geml-table th, table.geml-table td { border:1px solid var(--bd); padding:6px 12px; }\ntable.geml-table thead th { background:var(--code-bg); cursor:pointer; user-select:none; white-space:nowrap; }\ntable.geml-table thead th::after { content:\" \\2195\"; color:var(--muted); font-size:.8em; }\ntable.geml-table thead th.asc::after { content:\" \\2191\"; color:var(--accent); }\ntable.geml-table thead th.desc::after { content:\" \\2193\"; color:var(--accent); }\ntable.geml-table tbody tr:nth-child(2n) { background:#fafbfc; }\ntable.geml-table td.computed { color:#0a7c52; }\ntable.geml-table tfoot td { background:var(--code-bg); font-weight:600; border-top:2px solid var(--bd); }\n.table-tools { margin-bottom:6px; } .table-filter { width:240px; max-width:100%; padding:5px 9px; border:1px solid var(--bd); border-radius:7px; font-size:.85em; }\n.table-figure details > summary { cursor:pointer; color:var(--muted); font-size:.86em; padding:4px 0; }\n.table-note { color:var(--muted); font-size:.82em; margin:6px 0 0; }\n.geml-chart { width:100%; height:auto; background:var(--bg); border:1px solid var(--bd); border-radius:8px; }\n.c-title { font-size:15px; font-weight:600; fill:var(--fg); }\n.c-grid { stroke:#eaecef; } .c-axis { stroke:#aab1b8; } .c-tick { font-size:11px; fill:var(--muted); } .c-legend { font-size:12px; fill:var(--fg); }\n.media { max-width:100%; border-radius:8px; }\n.diagram-src { color:var(--muted); } .render-error { color:#cf222e; }\n.math-block { overflow-x:auto; padding:.4em 0; }\nsup.fn a { font-size:.75em; }\n.geml-footer { max-width:860px; margin:0 auto; padding:16px 24px 40px; color:var(--muted); font-size:.82em; }\n.geml-footer code { font-size:.95em; }\n.code-graph { margin:1.4em 0; }\n.cg-mount { border:1px solid var(--bd); border-radius:8px; padding:10px 12px; background:var(--bg); }\n.cg-scroll { overflow:auto; min-height:52vh; max-height:72vh; }\n.cg-svg { display:block; }\n.cg-search-wrap { position:relative; display:inline-block; }\n.cg-search { font:12px/1.4 inherit; padding:2px 7px; border:1px solid var(--bd); border-radius:4px; background:var(--bg); color:var(--fg); min-width:13ch; }\n.cg-search-menu { position:absolute; z-index:30; top:calc(100% + 2px); left:0; min-width:24ch; max-width:52ch; max-height:52vh; overflow:auto; background:var(--bg); border:1px solid var(--bd); border-radius:6px; box-shadow:0 6px 20px rgba(0,0,0,.18); }\n.cg-search-row { display:block; width:100%; text-align:left; padding:4px 9px 4px 18px; border:0; background:none; color:var(--fg); cursor:pointer; font:12px/1.4 inherit; }\n.cg-search-row:hover { background:var(--bd); }\n.cg-search-count { position:sticky; top:0; padding:4px 9px; font-size:11px; opacity:.65; background:var(--bg); border-bottom:1px solid var(--bd); }\n.cg-search-grp { padding:6px 9px 2px; font-size:11px; font-weight:600; opacity:.7; border-top:1px solid var(--bd); }\n.cg-search-grp:first-of-type { border-top:0; }\n.cg-stage { display:flex; gap:10px; align-items:flex-start; }\n.cg-stage .cg-scroll { flex:1 1 auto; min-width:0; }\n.cg-src { flex:0 0 42%; max-width:46%; display:flex; flex-direction:column; border:1px solid var(--bd); border-radius:6px; overflow:hidden; background:var(--bg); }\n.cg-src-hd { display:flex; gap:8px; align-items:center; justify-content:space-between; padding:4px 8px; border-bottom:1px solid var(--bd); color:var(--muted); font:.76em ui-monospace,Consolas,monospace; word-break:break-all; }\n.cg-src-hd button { font:inherit; border:1px solid var(--bd); border-radius:5px; background:transparent; color:var(--muted); cursor:pointer; padding:0 6px; }\n.cg-src-body { margin:0; padding:8px 10px; overflow:auto; max-height:72vh; color:var(--fg); font:12px/1.5 ui-monospace,Consolas,monospace; white-space:pre; }\n.cg-src-note { color:var(--muted); font-style:italic; white-space:pre-wrap; }\n.cg-bar { display:flex; gap:8px; align-items:center; flex-wrap:wrap; font-size:.82em; color:var(--muted); margin-bottom:6px; }\n.cg-bar button { font:inherit; padding:1px 8px; border:1px solid var(--bd); border-radius:5px; background:transparent; cursor:pointer; }\n.cg-crumb .cg-seg { border:0; border-radius:0; padding:0; background:none; color:var(--accent); cursor:pointer; font:inherit; }\n.cg-crumb .cg-seg:hover { text-decoration:underline; }\n.cg-frame { display:block; width:100%; height:72vh; border:0; background:var(--bg); }\n.cg-flash { color:#b42318; }\n.cg-legend { display:flex; gap:14px; align-items:center; justify-content:space-between; flex-wrap:wrap; font-size:.75em; color:var(--muted); margin-top:6px; }\n.cg-upbtn { cursor:pointer; }\n.cg-upbtn circle { fill:#fff; stroke:#94a3b8; }\n.cg-upbtn text { font-size:11px; fill:#57606a; }\n.cg-upbtn:hover circle { stroke:var(--accent); stroke-width:1.6; }\n.cg-upbtn:hover text { fill:var(--accent); }\n.cg-uplink { fill:none; stroke:#94a3b8; stroke-dasharray:3 2.5; pointer-events:none; }\n.cg-groups { display:flex; flex-wrap:wrap; gap:4px 12px; margin-top:6px; font-size:.75em; color:var(--muted); }\n.cg-chip { display:inline-flex; align-items:center; gap:4px; }\n.cg-chip i { width:10px; height:10px; border-radius:2px; border:1px solid #94a3b8; display:inline-block; }\n.cg-note { font-size:.8em; color:#9a6700; }\n.cg-n rect { fill:#eef2f7; stroke:#94a3b8; }\n.cg-n text { font-size:12px; fill:var(--fg); font-family:ui-monospace,Consolas,monospace; }\n.cg-n { cursor:pointer; }\n.cg-n.root rect { fill:#dbeafe; stroke:#2563eb; stroke-width:2; }\n.cg-n.leaf { opacity:.45; }\n.cg-n.test rect { stroke-dasharray:3 2; }\n.cg-n.grp rect { stroke-width:1.8; }\n.cg-e { fill:none; stroke:#94a3b8; stroke-width:.9; }\n.cg-e.cand { stroke-dasharray:2 3; }\n.cg-e.back { stroke:#dc2626; stroke-dasharray:5 3; }\n.cg-e.http { stroke:#0891b2; stroke-width:1.5; stroke-dasharray:5 2; } /* cross-stack API link */\n.cg-e.soft { opacity:.55; }\n.cg-svg.hl .cg-n { opacity:.22; }\n.cg-svg.hl .cg-e { opacity:.1; }\n.cg-svg.hl .cg-n.hl { opacity:1; }\n.cg-svg.hl .cg-e.hl { opacity:1; stroke-width:1.6; }\n"; | ||
| readonly js: "\n(function () {\n function cmp(a, b) {\n var na = a.dataset.sort, nb = b.dataset.sort;\n if (na !== undefined && nb !== undefined) return parseFloat(na) - parseFloat(nb);\n return (a.textContent || \"\").localeCompare(b.textContent || \"\");\n }\n document.querySelectorAll(\"table.geml-table\").forEach(function (table) {\n var tbody = table.tBodies[0];\n if (!tbody) return;\n // Sort on header click.\n var ths = table.tHead ? table.tHead.rows[0].cells : [];\n Array.prototype.forEach.call(ths, function (th, col) {\n th.addEventListener(\"click\", function () {\n var dir = th.classList.contains(\"asc\") ? \"desc\" : \"asc\";\n Array.prototype.forEach.call(ths, function (h) { h.classList.remove(\"asc\", \"desc\"); });\n th.classList.add(dir);\n var rows = Array.prototype.slice.call(tbody.rows);\n rows.sort(function (r1, r2) {\n var c = cmp(r1.cells[col], r2.cells[col]);\n return dir === \"asc\" ? c : -c;\n });\n rows.forEach(function (r) { tbody.appendChild(r); });\n });\n });\n // Filter rows.\n var fig = table.closest(\".table-figure\");\n var input = fig ? fig.querySelector(\".table-filter\") : null;\n if (input) input.addEventListener(\"input\", function () {\n var q = input.value.toLowerCase();\n Array.prototype.forEach.call(tbody.rows, function (r) {\n r.style.display = (r.textContent || \"\").toLowerCase().indexOf(q) >= 0 ? \"\" : \"none\";\n });\n });\n });\n})();\n"; | ||
| readonly codeGraphJs: string; | ||
| }; |
+10
-1
@@ -36,3 +36,3 @@ // GEML CLI HTML export — the standalone, self-contained page. | ||
| const importMap = wantLive | ||
| ? `<script type="importmap">{"imports":{"node:fs":"${lg}_node-stub.js","node:path":"${lg}_node-stub.js","node:crypto":"${lg}_node-stub.js","node:url":"${lg}_node-stub.js","node:child_process":"${lg}_node-stub.js"}}</script>\n` | ||
| ? `<script type="importmap">{"imports":{"node:fs":"${lg}_node-stub.js","node:path":"${lg}_node-stub.js","node:crypto":"${lg}_node-stub.js","node:url":"${lg}_node-stub.js","node:child_process":"${lg}_node-stub.js","node:os":"${lg}_node-stub.js"}}</script>\n` | ||
| : ""; | ||
@@ -94,4 +94,13 @@ const liveJs = wantLive | ||
| } | ||
| // Fragment mode: the body markup alone, for embedding into an existing | ||
| // layout. No shell, no CDN tags, no inline CSS/JS — see RenderOptions. | ||
| if (opts.fragment) | ||
| return body + "\n"; | ||
| const title = opts.title ?? ctx.docTitle() ?? "GEML document"; | ||
| return page(title, body, ctx, opts.source); | ||
| } | ||
| // The page shell's static assets, for fragment consumers: `css` styles every | ||
| // geml-* class a fragment emits (include once per site); `js` is the tables' | ||
| // sort/filter enhancement (once per page); `codeGraphJs` matters only when a | ||
| // fragment carries a code-graph mount. The full-page output inlines all three. | ||
| export const pageAssets = { css: CSS, js: JS, codeGraphJs: CODE_GRAPH_JS }; |
+1
-0
@@ -11,2 +11,3 @@ import { type Block, type Document } from "./geml.js"; | ||
| graphSidecar?: string; | ||
| fragment?: boolean; | ||
| } | ||
@@ -13,0 +14,0 @@ export declare function esc(s: string): string; |
+12
-0
@@ -177,2 +177,14 @@ // GEML serializer (§ round-trip): document model -> GEML source. | ||
| } | ||
| else if (b.type === "data" && b.value !== undefined && b.attrs["src"] === undefined) { | ||
| // src= content lives in the FILE: a value loaded from it must never be | ||
| // inlined into the document by a re-format — the body stays as authored | ||
| // (normally empty), or the source of truth would silently fork. | ||
| // GEP-0005 canonical form — legal because this is DATA, not verbatim text | ||
| // (code stays byte-preserved): json pretty-prints at two spaces, jsonl is | ||
| // one compact value per line. Engine-less bodies (yaml/toml/unknown) | ||
| // never reach here (no `value`) and fall through byte-preserved. | ||
| body = String(b.attrs["format"] ?? "json") === "jsonl" && Array.isArray(b.value) | ||
| ? b.value.map((v) => JSON.stringify(v)) | ||
| : JSON.stringify(b.value, null, 2).split("\n"); | ||
| } | ||
| else { | ||
@@ -179,0 +191,0 @@ body = b.raw ?? []; |
+27
-1
@@ -63,2 +63,23 @@ // GEML reference parser — Milestone 3: tables (§6). | ||
| } | ||
| // The character a data body splits on: the format's natural delimiter unless | ||
| // `delim=` names another single one — `;` for a European CSV, `|` for a | ||
| // pipe-delimited export (§6). Measured in code points, so an astral character | ||
| // counts as one. §4 attribute values carry no escape syntax, so a tab | ||
| // delimiter is spelled `format=tsv`, never `delim="\t"`; a value that is not | ||
| // exactly one character is an error and the natural delimiter is used, which | ||
| // keeps the rest of the table readable. | ||
| function resolveDelim(fmt, raw, diagnostics) { | ||
| const natural = fmt === "tsv" ? "\t" : ","; | ||
| if (raw === undefined) | ||
| return natural; | ||
| const d = String(raw); | ||
| if ([...d].length === 1) | ||
| return d; | ||
| diagnostics.push({ | ||
| severity: "error", | ||
| code: "bad-table-delimiter", | ||
| message: `\`delim="${d}"\` must be exactly one character (for a tab use \`format=tsv\`); split on ${fmt === "tsv" ? "a tab" : "`,`"} instead`, | ||
| }); | ||
| return natural; | ||
| } | ||
| function letters(n) { | ||
@@ -275,3 +296,3 @@ const out = []; | ||
| const header = headerAttr === undefined ? true : headerAttr === true || headerAttr === 1 || headerAttr === "1"; | ||
| raw = parseDelimited(body, fmt === "tsv" ? "\t" : ",", header); | ||
| raw = parseDelimited(body, resolveDelim(fmt, attrs["delim"], diagnostics), header); | ||
| } | ||
@@ -281,2 +302,7 @@ else { | ||
| diagnostics.push({ severity: "warning", code: "unknown-table-format", message: `unknown table format \`${fmt}\`; parsed as visual grid` }); | ||
| // `delim` refines the data form; it does not select it. Silently dropping it | ||
| // would leave a `=== table {delim=";"}` parsed as a one-column visual grid | ||
| // with nothing to say why. | ||
| if (attrs["delim"] !== undefined) | ||
| diagnostics.push({ severity: "warning", code: "ignored-table-delimiter", message: "`delim` applies to a data body (`format=csv`/`tsv`); this body was parsed as a visual grid, so it is ignored" }); | ||
| raw = parseVisual(body); | ||
@@ -283,0 +309,0 @@ } |
+5
-0
@@ -147,2 +147,7 @@ // GEML -> Markdown projection (the inverse direction of from-md.ts). | ||
| return fence(attr(b, "lang") ?? "", raw); | ||
| // GEP-0005: a data block projects as a fenced code block in its format — | ||
| // the nearest GFM shape (Markdown has no verified-data construct; that loss | ||
| // is the usual --to md lossiness, not a defect of the projection). | ||
| if (b.type === "data") | ||
| return fence(attr(b, "format") ?? "json", raw); | ||
| if (b.type === "math") | ||
@@ -149,0 +154,0 @@ return ["$$", ...raw, "$$"].join("\n"); |
+2
-1
| { | ||
| "name": "@geml/geml", | ||
| "version": "1.6.0", | ||
| "version": "1.7.0", | ||
| "mcpName": "io.github.geml-spec/geml", | ||
@@ -18,2 +18,3 @@ "publishConfig": { | ||
| "codemap", | ||
| "skill", | ||
| "README.md", | ||
@@ -20,0 +21,0 @@ "LICENSE" |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 2 instances
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
876390
10.33%56
3.7%16227
6.16%50
8.7%