@zvndev/darkmown 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -27,6 +27,8 @@ Commands: `darkmown dev`, `darkmown build`, `darkmown serve`.
27
27
 
28
28
  `{ name }` or `{ name.path }`. Resolves in order: loop item → in-scope value → declared `:state` → otherwise literal text. **Declare state before you bind it** — compilation is line-based, top to bottom, so a `{ x }` or `:if x` must come *after* the `:state x` (or the `:form into x`) that creates it.
29
29
 
30
+ The page's frontmatter is in scope as `meta`: `{ meta.title }` prints a field, `{ meta.tags }` joins an array with `, `, and `@loop meta.tags into tag` iterates a frontmatter array (build-time, stays static). Frontmatter arrays are inline only — `tags: [a, b, "x, y"]`; a value without a leading `[` is a plain string.
31
+
30
32
  ## Directives — the complete set (nothing else exists)
31
33
 
32
34
  ### State and buttons
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  All notable changes to Darkmown are documented here. Versions follow [semver](https://semver.org); pre-1.0 minor versions may contain breaking changes.
4
4
 
5
+ ## 0.8.0 — 2026-06-14
6
+
7
+ - **Array frontmatter + readable `meta`.** Frontmatter now parses inline flow arrays — `tags: [sales, revenue, "q1, q2"]` becomes a real array (previously the literal string `"[sales, revenue]"`), with quoted items keeping their internal commas and bracket-free values staying plain scalars. Block sequences (`- item` lines) remain out of scope to keep the parser single-pass.
8
+ - A page's frontmatter is now in scope in its body as `meta`: `{ meta.title }` prints a field, `{ meta.tags }` renders an array joined with `, `, and `@loop meta.tags into tag` iterates a frontmatter array at build time (stays `runtime: false`, zero-JS).
9
+ - A correctness fix in its own right, and the groundwork for reading/emitting bundles in Google Cloud's Open Knowledge Format (markdown + `type`-bearing frontmatter + cross-links). OKF support, if pursued, will be a thin additive export — deliberately not coupled to its v0.1 Draft.
10
+
5
11
  ## 0.7.0 — 2026-06-13
6
12
 
7
13
  - **Per-row actions in reactive loops.** A `:button` inside `@loop … into item` can now act on its own row — the canonical add-to-cart / remove-line / delete-todo pattern, with no hand-written JavaScript:
package/README.md CHANGED
@@ -54,8 +54,24 @@ One syntax everywhere: `{ name }` or `{ name.path }`.
54
54
 
55
55
  - In-scope static values (include arguments, loop values) resolve at build time.
56
56
  - Declared `:state` becomes a live binding.
57
+ - The page's own frontmatter is in scope as `meta` — `{ meta.title }` prints a field.
57
58
  - Anything else stays literal text — braces in prose never break a page or pull in the runtime.
58
59
 
60
+ ## Frontmatter
61
+
62
+ YAML-style `key: value` frontmatter between `---` fences. Values are strings, plus inline arrays:
63
+
64
+ ```wd
65
+ ---
66
+ title: Customers
67
+ tags: [sales, revenue, "q1, q2"]
68
+ ---
69
+ ```
70
+
71
+ - `{ meta.title }` prints a scalar; `{ meta.tags }` prints an array joined with `, `.
72
+ - `@loop meta.tags into tag` iterates an array field at build time (stays static, zero-JS).
73
+ - Arrays are inline flow only (`[a, b]`); quoted items keep internal commas (`"q1, q2"`). A value without a leading `[` stays a plain string.
74
+
59
75
  ## Loops
60
76
 
61
77
  `@loop <things> into <thing>` is the only loop. The source decides the behavior:
@@ -0,0 +1,120 @@
1
+ # OKF integration — plan & options
2
+
3
+ Status: **parked / decision pending** (2026-06-14). Research-verified; revisit when
4
+ we decide how far to take it. v0.8.0 (array frontmatter + readable `meta`) already
5
+ shipped the enabling groundwork.
6
+
7
+ ## What OKF is (verified)
8
+
9
+ **Open Knowledge Format (OKF)** is real. Published **2026-06-12** by the Google Cloud
10
+ Data Analytics team (Sam McVeety, Amir Hormati).
11
+
12
+ - Announcement: <https://cloud.google.com/blog/products/data-analytics/how-the-open-knowledge-format-can-improve-data-sharing/>
13
+ - Spec: <https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md>
14
+ - Repo (Apache-2.0, `okf/` subdir): <https://github.com/GoogleCloudPlatform/knowledge-catalog>
15
+ - **Not** the Open Knowledge Foundation (unrelated nonprofit — do not conflate).
16
+
17
+ It is a **file format**, not a service: a directory of markdown "concept" files for
18
+ feeding organizational knowledge to AI agents. Each concept file has YAML frontmatter
19
+ where **`type` is the only required field**; `title`, `description`, `resource` (a URI
20
+ for the underlying asset), `tags` (a YAML list), and `timestamp` (ISO 8601) are
21
+ optional. Producers may add arbitrary keys; consumers must preserve unknown keys.
22
+
23
+ - **Cross-links** are ordinary markdown links between concept files (bundle-relative
24
+ absolute like `/tables/customers.md` recommended). They are treated as **directed,
25
+ untyped graph edges** — the relationship's meaning lives in surrounding prose.
26
+ - **Reserved files:** `index.md` (a directory listing for *progressive disclosure* — an
27
+ agent navigates one level at a time) and `log.md` (chronological history). The root
28
+ `index.md` frontmatter may declare `okf_version: "0.1"`.
29
+ - **Conformance is permissive:** consumers MUST tolerate broken links, unknown `type`
30
+ values, unknown keys, and missing optional fields (so agent-generated, partial bundles
31
+ still work).
32
+ - **Reference tooling (both labeled proof-of-concept):** a BigQuery *enrichment agent*
33
+ that drafts OKF docs per table, and a *visualizer* — a `visualize` command that emits a
34
+ single self-contained interactive HTML file (Cytoscape.js force-graph + marked.js body,
35
+ no backend).
36
+ - **Sample bundles:** GA4 e-commerce, Stack Overflow, Bitcoin. ("BigQuery Table /
37
+ Dataset / Metric" are concept *types inside* bundles, not separate samples.)
38
+
39
+ ## Why it's interesting for Darkmown
40
+
41
+ OKF's entire surface — YAML frontmatter + a folder tree + relative markdown links →
42
+ static HTML — **is what Darkmown already does**. A `.md` tree with a `type:` field and
43
+ cross-links is *nearly an OKF bundle by accident*. Two directions:
44
+
45
+ 1. **Darkmown as an OKF renderer.** Darkmown renders markdown bundles to static, zero-JS
46
+ HTML — directly competitive with OKF's reference visualizer, which is OKF's weakest
47
+ surface (a single JS-heavy HTML file).
48
+ 2. **Every Darkmown site emits an OKF bundle.** Any docs/content site also becomes
49
+ machine-readable agent context. No other markdown framework offers this.
50
+
51
+ ## Why to be cautious
52
+
53
+ - **v0.1 "Draft," explicitly evolving.** Google's own blog: "a starting point, not a
54
+ finished standard." Field names and required fields may change (minor versions promise
55
+ backward-compatible additions only; breaking changes need a major bump).
56
+ - **Thin traction.** ~1.2k GitHub stars but ~8 issues, ~13 PRs, ~27 commits at launch.
57
+ **No third-party adopters** — only Google's own Cloud Knowledge Catalog consumes it.
58
+ - **Governance unclear.** Lives under Google's repo with no foundation/working group.
59
+ - **Thin convention.** Net-new over "a markdown vault + Obsidian/Dataview" is small: one
60
+ required field, two reserved filenames, link-as-edge semantics. Easy for others to
61
+ ignore or fork.
62
+
63
+ ## Decision (Kirby, 2026-06-14)
64
+
65
+ **Support OKF naturally; do not tie ourselves to it.** Treat it as a cheap, additive
66
+ interop convention, not a strategic bet or a core-model change.
67
+
68
+ ## Options (revisit later)
69
+
70
+ ### Option A — Do nothing more (status quo)
71
+ v0.8.0 array frontmatter already makes Darkmown frontmatter able to carry `tags` lists
72
+ and a `type` field. A Darkmown content tree with `type:` in frontmatter is already
73
+ ~conformant. Cost: zero. Keep tracking adoption.
74
+
75
+ ### Option B — Thin OKF emit (recommended first step if we act) — increment #3
76
+ A flag-gated export from a built site: `okf: true` in config or `--emit-okf`.
77
+ - During `buildSite` (`src/builder.js`), alongside `routes.json`, write `dist/okf/`.
78
+ - For each page, emit an OKF doc: frontmatter (`type` defaulting to `page`, `title` from
79
+ meta, `tags` from meta, `timestamp` from file mtime/git) + the page's markdown body.
80
+ - Derive edges from internal links + `@include` edges (`resolveInclude`/`compileFile`
81
+ already know these). Emit per-directory `index.md` for progressive disclosure.
82
+ - **Map at emit time** — do not restructure Darkmown's internal model around OKF.
83
+ - Effort: low; it hooks the existing route loop. Largely free.
84
+
85
+ ### Option C — OKF render mode ("best OKF renderer") — increment #2
86
+ `darkmown build --okf ./bundle [--out dist-okf]`: walk an arbitrary bundle root, render
87
+ each doc (type-aware templates, generic fallback), rewrite `.md` cross-links to routes,
88
+ emit a static index/graph view. Reuse `parseFrontmatter`, markdown-it, `compileFile` —
89
+ do not fork the pipeline. Must stay `runtime: false`.
90
+ - **Gate this on OKF showing real external adoption.** Today you'd build a renderer for a
91
+ format only Google uses. Higher effort, higher coupling risk.
92
+
93
+ ### Sequencing if we proceed
94
+ 1. (done) Array frontmatter + readable `meta` — v0.8.0.
95
+ 2. Option B (emit) — additive, low-regret.
96
+ 3. Option C (render) — only if the format gets stickier.
97
+
98
+ ## Hard "avoid" list
99
+
100
+ - Don't hardcode Google/BigQuery `type` vocabularies (`BigQuery Table`, etc.) — `type` is
101
+ uncentralized and producer-defined.
102
+ - Don't bet on v0.1 field names / required-field stability — map our frontmatter → OKF at
103
+ emit time.
104
+ - Don't depend on OKF's reference enrichment agent or visualizer — both are proofs of
105
+ concept. If we want a graph view, build our own; the format is trivial.
106
+ - Don't market "OKF-native" as a moat — it's a thin convention with thin adoption. A
107
+ cheap interoperability checkbox, not a strategic bet.
108
+
109
+ ## Invariants to respect (if/when we build)
110
+
111
+ - `.md` never gains directive behavior; OKF docs are content, rendered as content.
112
+ - No eval of bundle content; the `constructor`/`prototype`/`__proto__` rejection in
113
+ `getPath` still applies.
114
+ - OKF render/emit are pure content transforms — `runtime: false` always.
115
+ - Compile/render errors include the offending file path and a corrective suggestion.
116
+
117
+ ## Source
118
+
119
+ Full research briefing (cited): the research-investigator memory
120
+ `reference_okf_open_knowledge_format.md`, and `docs/spec-alignment.md` Stage 7.
@@ -56,6 +56,10 @@ This audit compares the current Darkmown implementation with the original MARROW
56
56
  - **Editor tooling:** VS Code extension (`editors/vscode`) ships `.wd` + `.skin` TextMate grammars, snippets, and folding, grammar-tested in CI.
57
57
  - **`AGENTS.md`:** a model-facing build guide (shipped in the npm files array), tuned over a 5-round agent-eval benchmark that lifted build quality from 2.78→4.51/5.
58
58
 
59
+ ## Stage 7 additions (2026-06-14)
60
+
61
+ - **Array frontmatter + readable `meta`:** `parseFrontmatter` now parses inline flow arrays (`tags: [a, b, "x, y"]` → real array; quoted items keep internal commas; a value without a leading `[` stays a scalar — block sequences are intentionally out of scope to keep the parser single-pass). A page's frontmatter is exposed to its body under `meta`, so `{ meta.title }` prints a field, `{ meta.tags }` renders an array joined with `, `, and `@loop meta.tags into tag` iterates a frontmatter array at build time (stays `runtime: false`). Genuine correctness fix on its own, and the groundwork for reading/emitting OKF-style bundles (Google Cloud's Open Knowledge Format — markdown + `type`-bearing frontmatter + cross-links; tracked but deliberately not coupled to its v0.1 Draft).
62
+
59
63
  ## Still missing from the full vision
60
64
 
61
65
  - First-party server runtime (`site/api/`), HTML-fragment `swap` semantics, and cart server sync — parked behind the adapter decision until a real project needs them.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zvndev/darkmown",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -109,8 +109,22 @@ One syntax everywhere: `{ name }` or `{ name.path }`.
109
109
 
110
110
  - If the name is a static value in scope (include argument, loop value), it is resolved at build time.
111
111
  - If the name is declared `:state`, it becomes a live binding.
112
+ - The page's frontmatter is in scope as `meta` — `{ meta.title }` prints a field.
112
113
  - Otherwise the text is left exactly as written — braces in prose never break a page or pull in the runtime.
113
114
 
115
+ ## Frontmatter
116
+
117
+ Frontmatter sits between `---` fences. Values are strings, plus inline arrays:
118
+
119
+ ```wd
120
+ ---
121
+ title: Customers
122
+ tags: [sales, revenue, "q1, q2"]
123
+ ---
124
+ ```
125
+
126
+ `{ meta.title }` prints a scalar and `{ meta.tags }` joins an array with `, `. You can loop a frontmatter array at build time — `@loop meta.tags into tag` — and the page stays static. Arrays are inline flow only (`[a, b]`); quoted items keep their commas.
127
+
114
128
  ## Sections
115
129
 
116
130
  `::: section #id .class` opens a container and `:::` closes it. Sections scope state: a `:state` declared inside a section belongs to that section, so two sections can both declare `count` without colliding. Bindings and buttons resolve to the nearest scope.
package/src/compiler.js CHANGED
@@ -82,7 +82,9 @@ function compileFile(file, context, stack, scope, comp, sections, loopItem) {
82
82
  return { meta, html: md.render(body, {}) };
83
83
  }
84
84
 
85
- const ctx = { file, context, stack: [...stack, real], scope, comp, sections, loopItem };
85
+ // Expose this file's frontmatter to the body under `meta` so `{ meta.title }`,
86
+ // `{ meta.tags }`, and `@loop meta.tags into tag` resolve as static values.
87
+ const ctx = { file, context, stack: [...stack, real], scope: createScope(scope, { meta }), comp, sections, loopItem };
86
88
  return { meta, html: compileBody(body.replace(/\r\n?/g, "\n").split("\n"), ctx) };
87
89
  }
88
90
 
@@ -95,11 +97,49 @@ export function parseFrontmatter(raw) {
95
97
  const meta = {};
96
98
  for (const line of front.split("\n")) {
97
99
  const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
98
- if (match) meta[match[1]] = stripQuotes(match[2]);
100
+ if (match) meta[match[1]] = parseFrontmatterValue(match[2]);
99
101
  }
100
102
  return { meta, body };
101
103
  }
102
104
 
105
+ // Frontmatter values are scalars, except an inline flow array `[a, b, c]`.
106
+ // Block sequences (`- item` on following lines) are intentionally out of scope —
107
+ // the parser stays single-pass and line-based.
108
+ function parseFrontmatterValue(raw) {
109
+ const value = raw.trim();
110
+ if (value.startsWith("[") && value.endsWith("]")) return parseInlineArray(value);
111
+ return stripQuotes(value);
112
+ }
113
+
114
+ function parseInlineArray(raw) {
115
+ const inner = raw.slice(1, -1);
116
+ if (inner.trim() === "") return [];
117
+ const items = [];
118
+ let buf = "";
119
+ let quote = null;
120
+ let quoted = false;
121
+ const push = () => {
122
+ items.push(quoted ? buf : buf.trim());
123
+ buf = "";
124
+ quoted = false;
125
+ };
126
+ for (const char of inner) {
127
+ if (quote) {
128
+ if (char === quote) quote = null;
129
+ else buf += char;
130
+ } else if ((char === '"' || char === "'") && buf.trim() === "") {
131
+ quote = char;
132
+ quoted = true;
133
+ } else if (char === ",") {
134
+ push();
135
+ } else {
136
+ buf += char;
137
+ }
138
+ }
139
+ push();
140
+ return items;
141
+ }
142
+
103
143
  // ---------------------------------------------------------------------------
104
144
  // Block parser: directives + prose segments
105
145
  // ---------------------------------------------------------------------------
@@ -868,7 +908,9 @@ function resolveBindingHtml(expr, ctx) {
868
908
 
869
909
  const staticValue = lookupVar(ctx.scope, head);
870
910
  if (staticValue.found) {
871
- return escapeHtml(getPath(staticValue.value, segs.slice(1)) ?? "");
911
+ const resolved = getPath(staticValue.value, segs.slice(1));
912
+ if (Array.isArray(resolved)) return escapeHtml(resolved.join(", "));
913
+ return escapeHtml(resolved ?? "");
872
914
  }
873
915
 
874
916
  const key = resolveStateKey(head, ctx);