@geml/geml
Advanced tools
| #!/usr/bin/env node | ||
| export {}; |
Sorry, the diff of this file is too big to display
+15
-1
@@ -28,2 +28,16 @@ // Structural id-rewriting for `geml set`. `set #id` names the block to edit, | ||
| } | ||
| // A local mirror of geml.ts's trimSpaceTabEnd — geml.ts already imports THIS | ||
| // module (normalizeBlockId), so importing back would close a cycle. Linear on | ||
| // purpose: `/[ \t]+$/` restarts the run at every index inside a run that never | ||
| // reaches the end of the line, which is quadratic in the line's length. | ||
| function trimSpaceTabEnd(s) { | ||
| let i = s.length; | ||
| while (i > 0) { | ||
| const c = s.charCodeAt(i - 1); | ||
| if (c !== 0x20 && c !== 0x09) | ||
| break; | ||
| i--; | ||
| } | ||
| return i === s.length ? s : s.slice(0, i); | ||
| } | ||
| // Rewrite the id inside a `{…}` attribute block to `#newId`, keeping the braces | ||
@@ -102,3 +116,3 @@ // and every other class/attr byte. If no id is present, insert `#newId` as the | ||
| const ct = stripEnding(lines[j]); | ||
| const trimmed = ct.replace(/[ \t]+$/, ""); | ||
| const trimmed = trimSpaceTabEnd(ct); | ||
| if (/^=+$/.test(trimmed) && trimmed.length === openLen) | ||
@@ -105,0 +119,0 @@ break; // plain close: done |
+1
-1
@@ -190,3 +190,3 @@ // GEML reference parser — Markdown → GEML conversion. | ||
| const indent = raw.length - raw.trimStart().length; | ||
| const c = raw.replace(/\s+$/, "").trimStart(); | ||
| const c = raw.trimEnd().trimStart(); | ||
| if (indent <= 3 && c.length >= marker.length && c[0] === marker[0] && /^[`~]+$/.test(c)) | ||
@@ -193,0 +193,0 @@ break; |
+32
-0
@@ -7,2 +7,3 @@ #!/usr/bin/env node | ||
| import { type ChartModel } from "./chart.js"; | ||
| import { type Addressed } from "./selector.js"; | ||
| export { type Value } from "./attrs.js"; | ||
@@ -16,2 +17,3 @@ export { type Inline } from "./inline.js"; | ||
| export { gemlToMd } from "./to-md.js"; | ||
| export declare function reLit(s: string): string; | ||
| export type BodyMode = "raw" | "flow" | "data"; | ||
@@ -73,7 +75,17 @@ export type DataValue = null | boolean | number | string | DataValue[] | { | ||
| resolveDoc?: (doc: string) => string | null; | ||
| docExists?: (doc: string) => boolean; | ||
| self?: string; | ||
| } | ||
| export declare const FENCE_OPEN: RegExp; | ||
| export declare function isCloseFence(line: string, openLen: number): boolean; | ||
| export declare const EMBED_DEPTH_LIMIT = 8; | ||
| export declare function projectableInlines(blocks: Block[], id: string): { | ||
| inlines: Inline[]; | ||
| } | "not-inline" | null; | ||
| export declare function relJoinPath(base: string, target: string): string; | ||
| export declare function relDirPath(p: string): string; | ||
| export declare function gatherEmbeds(source: string): { | ||
| doc: string; | ||
| anchor?: string; | ||
| }[]; | ||
| export declare function parse(source: string, opts?: ParseOptions): Document; | ||
@@ -84,3 +96,23 @@ export interface Span { | ||
| } | ||
| export declare function stripEol(line: string): string; | ||
| export declare function trimSpaceTabEnd(s: string): string; | ||
| export declare function blockSpans(source: string): Map<string, Span>; | ||
| export declare function addressedUnits(source: string): Addressed[]; | ||
| export declare function splitLines(source: string): string[]; | ||
| export declare function newlineOf(text: string): string; | ||
| export declare function toLf(text: string): string; | ||
| export declare function toNewline(text: string, nl: string): string; | ||
| export declare function narrowToHead(span: Span): Span; | ||
| export declare function closeFenceLine(lines: string[], span: Span): string | null; | ||
| export declare function narrowToIntro(source: string, span: Span): Span; | ||
| /** Which part of one unit to output. `intro` applies to headings only. */ | ||
| export type UnitPart = "whole" | "head" | "body" | "intro"; | ||
| export declare function sliceUnit(source: string, span: Span, part?: UnitPart): string; | ||
| export declare function findBlockSite(blocks: Block[], id: string): { | ||
| siblings: Block[]; | ||
| index: number; | ||
| } | undefined; | ||
| export declare function sectionEndIndex(siblings: Block[], k: number): number; | ||
| export declare function historyPathFor(geml: string): string; | ||
| export declare const VERSION = "1.0"; | ||
| export declare const PARSER_VERSION: string; |
+7
-3
@@ -41,3 +41,7 @@ // GEML History extension — save / restore / verify. | ||
| // --------------------------------------------------------------------------- | ||
| const FENCE_OPEN = /^(={3,})[ \t]+([A-Za-z][A-Za-z0-9_-]*)[ \t]*(\{.*\})?[ \t]*$/; | ||
| // Trailing whitespace nested inside the optional attrs group — the flat form | ||
| // let two runs split the same tabs every possible way (quadratic; see the note | ||
| // on geml.ts's FENCE_OPEN, which this mirrors). Same language, verified over | ||
| // 150k random strings. | ||
| const FENCE_OPEN = /^(={3,})[ \t]+([A-Za-z][A-Za-z0-9_-]*)[ \t]*(?:(\{.*\})[ \t]*)?$/; | ||
| function locate(lines) { | ||
@@ -54,3 +58,3 @@ const out = []; | ||
| while (j < lines.length) { | ||
| const t = lines[j].replace(/\s+$/, ""); | ||
| const t = lines[j].trimEnd(); | ||
| if (/^=+$/.test(t) && t.length === fenceLen) | ||
@@ -113,3 +117,3 @@ break; | ||
| while (i < n) { | ||
| const t = lines[i].replace(/\s+$/, ""); | ||
| const t = lines[i].trimEnd(); | ||
| const close = /^=+$/.test(t) && t.length === fenceLen; | ||
@@ -116,0 +120,0 @@ i++; |
+46
-8
| #!/usr/bin/env node | ||
| // `geml mcp` — MCP server for GEML documents and the code graph. | ||
| // | ||
| // Ten tools over a confined root directory of `.geml` documents: five read-only, | ||
| // five that write, each named after the CLI verb it wraps (`geml set` -> | ||
| // `geml_set`, the bare transform entry -> `geml_to`). When that root holds a code graph, the four read-only | ||
| // Eleven tools over a confined root directory of `.geml` documents: six | ||
| // read-only, five that write, each named after the CLI verb it wraps (`geml set` | ||
| // -> `geml_set`, the bare transform entry -> `geml_to`). When that root holds a code graph, the four read-only | ||
| // code-graph tools from `codemap/mcp-server.mjs` are served from this SAME | ||
@@ -277,2 +277,40 @@ // process, so a client registers one server instead of two. That file stays a | ||
| { | ||
| name: "geml_find", | ||
| description: "Search block CONTENT across the served documents and get back ADDRESSES, one row of `<file>\\t<address>` per hit. This is the other half of geml_list: `list` says what a document contains, `find` says which block holds the words you are looking for — and it answers with an address that pastes straight into geml_get or geml_set, never a line number that the next edit invalidates. The address is the innermost block holding the match, and a block that matches on many lines is reported once. Substring, case-insensitive unless `case` is true. Omit `path` to search every `*.geml` under the server root, or give a file or directory to narrow it. No match is not an error: the result is empty.", | ||
| inputSchema: { | ||
| type: "object", | ||
| properties: { | ||
| pattern: { type: "string", description: "Text to look for inside block bodies" }, | ||
| path: { type: "string", description: "Optional file or directory under the server root; default: the whole root" }, | ||
| case: { type: "boolean", description: "Match case exactly (default: case-insensitive)" }, | ||
| head: { type: "boolean", description: "Add the matching line as a third column" }, | ||
| }, | ||
| required: ["pattern"], | ||
| }, | ||
| run: (args) => { | ||
| if (typeof args.pattern !== "string" || args.pattern === "") | ||
| throw new Error("`pattern` is required"); | ||
| // `path` goes through the same confinement gate as every other file | ||
| // argument; omitting it searches the root, which is confined by being it. | ||
| const where = args.path === undefined ? OPTS.root : resolveInRoot(args.path); | ||
| const flags = [...(args.case ? ["--case"] : []), ...(args.head ? ["--head"] : [])]; | ||
| const run = runCli(["find", args.pattern, where, ...flags]); | ||
| // Exit 1 means "nothing matched" — a result, not a failure. Anything on | ||
| // stderr is the real error case. | ||
| if (!run.ok && run.stderr) | ||
| throw new Error(run.stderr); | ||
| // The CLI prints the path it was given, which here is absolute. Every | ||
| // other tool in this server speaks paths relative to the root, and a | ||
| // model is meant to paste a row's file straight into geml_get — so put | ||
| // the rows in those coordinates, and keep the server's own layout out of | ||
| // the client's view while we are at it. | ||
| const base = realpathSync(OPTS.root) + sep; | ||
| return run.stdout | ||
| .split("\n") | ||
| .map((line) => (line.startsWith(base) ? line.slice(base.length).replace(/\\/g, "/") : line)) | ||
| .join("\n") | ||
| .trim(); | ||
| }, | ||
| }, | ||
| { | ||
| name: "geml_get", | ||
@@ -286,3 +324,3 @@ description: "Read ONE block from a GEML document. Use this instead of reading the whole file: it returns only that block, typically a few percent of the document. Call `geml_list` first and pass back the `address` it gives — that also reaches blocks with no `#id`, which an id alone cannot.", | ||
| type: "string", | ||
| description: "What to read: a block id (with or without `#`), a `## Heading` line (its whole section), `=== type` for every block of a type, or a `@<hex>` content address for a block with no id — the forms `geml_list` prints", | ||
| description: "What to read: a block id (with or without `#`), a `## Heading` line (its whole section), `=== type` for every block of a type, a `@<hex>` content address for a block with no id, or `L27`/`L27-58` for the smallest block holding those lines — the forms `geml_list` prints, plus the line numbers an editor or a diff hunk speaks", | ||
| }, | ||
@@ -295,4 +333,4 @@ view: { | ||
| 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`.", | ||
| enum: ["whole", "head", "intro", "body"], | ||
| description: "How much of the block to return (default: whole). For a SECTION these cut it three ways: `head` is the heading line, `intro` everything under it up to its first subheading, `body` everything under it — so `body` always contains `intro`, and equals it when the section has no subheading. Reach for `intro` to read a section's opening without pulling its subsections into the conversation; a whole `#id` on a top-level heading is often the entire document. Only a heading has an intro. `body` is usually what you want together with `view`.", | ||
| }, | ||
@@ -435,3 +473,3 @@ }, | ||
| name: "geml_set", | ||
| description: "Replace ONE block, leaving every other byte of the document untouched. Prefer this over rewriting a file. The replacement is VALIDATED BEFORE it is written: if it would break the document, nothing is written and you get the diagnostics back — re-read them and fix the body rather than retrying the same content. `part` selects whole block (default), just the head/fence line, or just the body. An address matching SEVERAL blocks is refused — this writes one block, so narrow it first.", | ||
| description: "Replace ONE block, leaving every other byte of the document untouched. Prefer this over rewriting a file. The replacement is VALIDATED BEFORE it is written: if it would break the document, nothing is written and you get the diagnostics back — re-read them and fix the body rather than retrying the same content. Breaking the document is what gets refused — removing content is not: if your replacement drops blocks, the write goes through and the result names every block that went, unnamed ones included, so check that line whenever you shortened a section. `geml_revert` puts one back. The way to keep a block is to keep it in the text you send; `geml_get` on the same address just handed it to you. `part` selects whole block (default), the head/fence line, a section's `intro`, or the body. An address matching SEVERAL blocks is refused — this writes one block, so narrow it first.", | ||
| inputSchema: { | ||
@@ -446,3 +484,3 @@ type: "object", | ||
| body: { type: "string", description: "The replacement text" }, | ||
| part: { type: "string", enum: ["whole", "head", "body"], description: "What to replace (default: whole)" }, | ||
| part: { type: "string", enum: ["whole", "head", "intro", "body"], description: "What to replace (default: whole). `intro` replaces a section's opening — everything under the heading up to its first subheading — and leaves every subsection byte-identical, which is what makes a read-edit-write cycle on a long section safe. An empty intro (a subheading follows the heading immediately) is written into, so this also adds an opening where there was none." }, | ||
| }, | ||
@@ -449,0 +487,0 @@ required: ["file", "id", "body"], |
@@ -5,5 +5,5 @@ import { type Document } from "./geml.js"; | ||
| 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 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/* A table wider than the column scrolls INSIDE its own box — the page itself\n must never scroll sideways. Only the table is in here: the filter box above\n and the caption below stay put instead of sliding out of view with it. */\n.table-scroll { overflow-x:auto; max-width:100%; }\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/* The graph is the widest artifact on the page: let it break out of main's\n 860px reading column and take the viewport, centred, leaving the prose\n around it untouched. Negative inline margins, NOT transform — a transform\n would become the containing block for the fullscreen overlay below. */\n@media (min-width:900px) { .code-graph { margin-inline: calc((100% - min(96vw, 1600px)) / 2); } }\n.cg-mount { border:1px solid var(--bd); border-radius:8px; padding:10px 12px; background:var(--bg); }\n.cg-scroll { overflow:auto; min-height:60vh; max-height:84vh; }\n.cg-svg { display:block; }\n/* Fullscreen: one class drives the layout, whether the native Fullscreen API\n took (mount is in the top layer) or we fell back to a fixed overlay. The\n :fullscreen pseudo-class is deliberately NOT in these selectors — a browser\n that doesn't know it would drop the whole rule; the runtime keeps the class\n in sync with fullscreenchange instead. Toolbar/legend keep their height,\n the stage eats the rest. */\n.cg-mount.cg-full { position:fixed; inset:0; z-index:9999; margin:0; border:0; border-radius:0; padding:10px 14px; background:var(--bg); display:flex; flex-direction:column; }\n.cg-mount.cg-full > .cg-bar, .cg-mount.cg-full > .cg-legend, .cg-mount.cg-full > .cg-groups { flex:0 0 auto; }\n.cg-mount.cg-full .cg-stage { flex:1 1 auto; min-height:0; }\n.cg-mount.cg-full .cg-scroll { min-height:0; max-height:none; height:100%; }\n.cg-mount.cg-full .cg-src-body { max-height:none; }\n.cg-mount.cg-full .cg-frame { flex:1 1 auto; height:auto; }\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:84vh; 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:84vh; 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; | ||
| }; |
+1
-1
@@ -98,3 +98,3 @@ import { type Block, type Document } from "./geml.js"; | ||
| }; | ||
| export declare const 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:\"\u2713\"; 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"; | ||
| export declare const 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:\"\u2713\"; 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/* A table wider than the column scrolls INSIDE its own box \u2014 the page itself\n must never scroll sideways. Only the table is in here: the filter box above\n and the caption below stay put instead of sliding out of view with it. */\n.table-scroll { overflow-x:auto; max-width:100%; }\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/* The graph is the widest artifact on the page: let it break out of main's\n 860px reading column and take the viewport, centred, leaving the prose\n around it untouched. Negative inline margins, NOT transform \u2014 a transform\n would become the containing block for the fullscreen overlay below. */\n@media (min-width:900px) { .code-graph { margin-inline: calc((100% - min(96vw, 1600px)) / 2); } }\n.cg-mount { border:1px solid var(--bd); border-radius:8px; padding:10px 12px; background:var(--bg); }\n.cg-scroll { overflow:auto; min-height:60vh; max-height:84vh; }\n.cg-svg { display:block; }\n/* Fullscreen: one class drives the layout, whether the native Fullscreen API\n took (mount is in the top layer) or we fell back to a fixed overlay. The\n :fullscreen pseudo-class is deliberately NOT in these selectors \u2014 a browser\n that doesn't know it would drop the whole rule; the runtime keeps the class\n in sync with fullscreenchange instead. Toolbar/legend keep their height,\n the stage eats the rest. */\n.cg-mount.cg-full { position:fixed; inset:0; z-index:9999; margin:0; border:0; border-radius:0; padding:10px 14px; background:var(--bg); display:flex; flex-direction:column; }\n.cg-mount.cg-full > .cg-bar, .cg-mount.cg-full > .cg-legend, .cg-mount.cg-full > .cg-groups { flex:0 0 auto; }\n.cg-mount.cg-full .cg-stage { flex:1 1 auto; min-height:0; }\n.cg-mount.cg-full .cg-scroll { min-height:0; max-height:none; height:100%; }\n.cg-mount.cg-full .cg-src-body { max-height:none; }\n.cg-mount.cg-full .cg-frame { flex:1 1 auto; height:auto; }\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:84vh; 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:84vh; 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"; | ||
| export declare const 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"; | ||
@@ -101,0 +101,0 @@ export declare function codeGraphRuntime(root: { |
@@ -27,2 +27,6 @@ export interface Span { | ||
| } | { | ||
| form: "line"; | ||
| from: number; | ||
| to: number; | ||
| } | { | ||
| form: "attr"; | ||
@@ -56,2 +60,5 @@ type: string; | ||
| export declare function discoveryHint(where: string): string; | ||
| export declare function matchLine(sel: Extract<Selector, { | ||
| form: "line"; | ||
| }>, all: Addressed[]): Unit | undefined; | ||
| export declare function matchType(type: string, all: Addressed[]): Unit[]; |
+41
-1
@@ -23,4 +23,11 @@ // Block selectors — the one syntax `get`, `set` and `history get` all address | ||
| } | ||
| const FENCE_SEL = /^={3,}[ \t]*([A-Za-z][A-Za-z0-9_-]*)[ \t]*(@[0-9a-fA-F]{1,}(?:~\d+)?)?[ \t]*(\{.*\})?[ \t]*$/; | ||
| // Each optional part carries its OWN trailing whitespace. Written flat — | ||
| // `…[ \t]*(@…)?[ \t]*(\{.*\})?[ \t]*$` — three runs competed for the same tabs | ||
| // and the engine tried every way to divide them: `=== note` plus 8k tabs and | ||
| // one stray byte took 84 SECONDS. Nested, each absent part leaves exactly one | ||
| // run and the match is immediate. Same language: 150k random strings, identical | ||
| // groups. (Mirrors geml.ts's FENCE_OPEN, which had the same shape.) | ||
| const FENCE_SEL = /^={3,}[ \t]*([A-Za-z][A-Za-z0-9_-]*)[ \t]*(?:(@[0-9a-fA-F]{1,}(?:~\d+)?)[ \t]*)?(?:(\{.*\})[ \t]*)?$/; | ||
| const BARE_AT = /^@([0-9a-fA-F]+)(?:~(\d+))?$/; | ||
| const BARE_LINE = /^[Ll](\d+)(?:-(\d+))?$/; | ||
| // Parse selector TEXT. Never touches a document: every form is decided by | ||
@@ -57,2 +64,15 @@ // lexis alone, which is also what keeps the two selector namespaces on | ||
| } | ||
| // Checked before the id fallthrough, so a bare `L27` is a position. An id | ||
| // that really is spelled `L27` keeps its explicit key form, `#L27` — the | ||
| // same "short form for the common case, key form always available" rule | ||
| // `@<hex>` already relies on. | ||
| const line = s.match(BARE_LINE); | ||
| if (line) { | ||
| const from = Number(line[1]); | ||
| const to = line[2] !== undefined ? Number(line[2]) : from; | ||
| // Reject rather than clamp: `L0` and `L10-5` are typos, and a selector that | ||
| // silently means something else is worse than one that refuses. | ||
| if (from >= 1 && to >= from) | ||
| return { form: "line", from, to }; | ||
| } | ||
| // Anything else is an id or a pasted heading line; the caller resolves it. | ||
@@ -109,2 +129,22 @@ return { form: "id", raw: s }; | ||
| } | ||
| // Match a position selector: the SMALLEST unit that fully contains the range. | ||
| // "Smallest" is what makes this ≤1 match instead of N. Spans nest — a heading's | ||
| // span covers its whole section, so line 30 of the comparison doc sits inside | ||
| // both `#capability-matrix` (L25-65) and `#caps` (L27-58) — and returning both | ||
| // would emit the inner block twice, once alone and once inside its section. | ||
| // The innermost is also the answer the question actually wants: a line number | ||
| // arrived from grep or a stack trace, and the caller means "the thing I have to | ||
| // edit", which is never the enclosing chapter. | ||
| export function matchLine(sel, all) { | ||
| let best; | ||
| for (const a of all) { | ||
| const start = a.unit.span.start + 1; // spans are 0-based; selectors are 1-based, as the listing prints them | ||
| const end = a.unit.span.end; | ||
| if (start > sel.from || end < sel.to) | ||
| continue; // must FULLY contain the range | ||
| if (best === undefined || end - start < best.span.end - (best.span.start + 1)) | ||
| best = a.unit; | ||
| } | ||
| return best; | ||
| } | ||
| // Match a type filter: every block of that type in document order. Blocks that | ||
@@ -111,0 +151,0 @@ // carry an id are INCLUDED — the selector says nothing about ids, so filtering |
+2
-2
| { | ||
| "name": "@geml/geml", | ||
| "version": "1.7.1", | ||
| "version": "1.7.2", | ||
| "mcpName": "io.github.geml-spec/geml", | ||
@@ -11,3 +11,3 @@ "publishConfig": { | ||
| "bin": { | ||
| "geml": "dist/geml.js" | ||
| "geml": "dist/cli.js" | ||
| }, | ||
@@ -14,0 +14,0 @@ "main": "dist/geml.js", |
+20
-4
@@ -99,4 +99,7 @@ <p align="center"> | ||
| geml doc.geml --to md|html|geml # convert; geml notes.md -> GEML | ||
| geml list doc.geml # CALL FIRST: every block, its address, kind, line range | ||
| geml find "text" doc.geml|dir # search block CONTENT -> file<TAB>address; exit 1 = no hit | ||
| geml get doc.geml ['<selector>'] # list addressable blocks, or print what the selector matches | ||
| geml set doc.geml '<selector>' [--head|--body] [--in F[#src]] # replace ONE block's content | ||
| geml get doc.geml '#sec' --intro # a section cuts three ways: --head | --intro | --body | ||
| geml set doc.geml '<selector>' [--head|--intro|--body] [--in F[#src]] # replace ONE block's content | ||
| geml add doc.geml (--append|--before #id|--after #id) [--in F[#src]] # insert a fragment | ||
@@ -130,6 +133,19 @@ geml delete doc.geml '#id' ['#id2' …] # remove one or more blocks | ||
| `set` writes ONE block, so a selector matching several is refused (exit 2) with | ||
| the unique address of each candidate. `--head` is the head line, `--body` the | ||
| body; both round-trip — `geml get f X --body | geml set f X --body` leaves the | ||
| file byte-identical. | ||
| the unique address of each candidate. A section cuts three ways: `--head` is the | ||
| heading line, `--intro` its opening region — everything under it up to its first | ||
| subheading — and `--body` everything under it, so `--body` always contains | ||
| `--intro`, and equals it when the section has no subheading. All three | ||
| round-trip — `geml get f X --body | geml set f X --body` leaves the file | ||
| byte-identical — and `--intro` is how a section's opening is edited without | ||
| pulling its subsections into context. A block has no intro; asking for one is a | ||
| usage error rather than a quiet fall back to the body. | ||
| A write is refused when it would break the document, never merely because it | ||
| removes something. A replacement that drops blocks is carried out and the | ||
| dropped blocks are named on stderr — unnamed ones counted, references left | ||
| dangling reported — with `geml revert` as the way back. That is the same stance | ||
| `delete` takes, so removing content has one rule rather than two, and no region | ||
| becomes uneditable because something inside it happens to carry an id. The | ||
| round trip above drops nothing: the blocks came back in the text you sent. | ||
| A `@<hex>` **content address** is the first 8 hex of the SHA-256 of the block's | ||
@@ -136,0 +152,0 @@ own text (line endings normalized to LF, no trailing newline), with `~1`, `~2`… |
+25
-0
@@ -53,5 +53,11 @@ --- | ||
| `geml skill install` sets all of this up user-global, and installs this text | ||
| into whatever other agent tools it detects — a tool's directory has to be there | ||
| already; none is ever created for you. `--dry-run` shows what it would do. | ||
| ## Work blockwise (agent editing) | ||
| ```sh | ||
| geml list file.geml # CALL THIS FIRST — every block, its address, kind, lines | ||
| geml find "text" file.geml|dir # search block CONTENT -> file<TAB>address (exit 1 = no hit) | ||
| geml get file.geml '#id' # read ONE block (a heading id = its whole section) | ||
@@ -63,2 +69,21 @@ geml set file.geml '#id' --in f # replace ONE block (re-parsed; never writes a broken doc) | ||
| Address a block, never a line range: `#id` · `'## Heading'` (its whole section) | ||
| · `'=== type'` · `@<hex>` (no id) · `L27` or `L27-58` (the smallest block holding | ||
| those lines — how a line number from an editor, a linter or a diff hunk becomes | ||
| an address). `list` and `find` print addresses that paste straight into the | ||
| others, so neither `grep` nor a line count is needed to locate anything. | ||
| Any section can be cut three ways, on `get` and `set` alike: `--head` (the | ||
| heading line), `--intro` (what it says before its first subheading — empty when | ||
| one follows immediately, the whole body when none does), `--body` (everything | ||
| under it, so it always contains the intro). `--intro` is how you edit a | ||
| section's opening without pulling its subsections into context, and setting an | ||
| empty one writes an opening where the section had none. | ||
| A write is refused when it would break the document, never merely because it | ||
| removes something: a replacement that drops blocks is carried out and NAMED on | ||
| stderr — unnamed blocks included — with `geml revert` as the way back. Read, | ||
| edit, write back, and nothing is dropped, because `get` handed those blocks to | ||
| you. Send content that omits them only when removing them is the point. | ||
| ## Full reference — pull ONE section, not the whole file | ||
@@ -65,0 +90,0 @@ |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
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.
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.
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.
939612
6.47%58
3.57%17208
5.46%265
6.43%52
4%