@mandujs/core 0.53.3 → 0.54.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +654 -654
- package/package.json +1 -1
- package/src/a11y/__tests__/run-audit.test.ts +333 -333
- package/src/a11y/fix-hints.ts +76 -76
- package/src/a11y/index.ts +18 -18
- package/src/a11y/run-audit.ts +394 -394
- package/src/a11y/types.ts +125 -125
- package/src/auth/__tests__/login.test.ts +1 -1
- package/src/auth/__tests__/password.test.ts +122 -122
- package/src/auth/__tests__/tokens.test.ts +274 -274
- package/src/auth/__tests__/verification.test.ts +274 -274
- package/src/auth/index.ts +76 -76
- package/src/auth/login.ts +225 -225
- package/src/auth/password.ts +120 -120
- package/src/auth/reset.ts +243 -243
- package/src/auth/tokens.ts +612 -612
- package/src/auth/verification.ts +253 -253
- package/src/brain/__tests__/redactor.test.ts +94 -94
- package/src/brain/adapters/__tests__/_helpers.ts +83 -83
- package/src/brain/adapters/__tests__/anthropic-oauth.test.ts +196 -196
- package/src/brain/adapters/__tests__/chatgpt-auth.test.ts +193 -193
- package/src/brain/adapters/__tests__/openai-oauth.test.ts +209 -209
- package/src/brain/adapters/__tests__/resolver.test.ts +143 -143
- package/src/brain/adapters/anthropic-oauth.ts +1 -1
- package/src/brain/adapters/chatgpt-auth.ts +300 -300
- package/src/brain/adapters/index.ts +319 -319
- package/src/brain/adapters/oauth-flow.ts +439 -439
- package/src/brain/consent.ts +240 -240
- package/src/brain/credentials.ts +396 -396
- package/src/bundler/__tests__/build-runner.ts +113 -113
- package/src/bundler/__tests__/dev-reliability.test.ts +619 -619
- package/src/bundler/__tests__/extended-watch.test.ts +711 -711
- package/src/bundler/__tests__/fast-refresh.test.ts +10 -10
- package/src/bundler/__tests__/hdr.test.ts +353 -353
- package/src/bundler/__tests__/hmr-client.test.ts +532 -532
- package/src/bundler/__tests__/manifest-schema.test.ts +266 -266
- package/src/bundler/__tests__/prod-smoke.test.ts +138 -138
- package/src/bundler/__tests__/reverse-import-graph.test.ts +519 -519
- package/src/bundler/__tests__/slot-dispatch.test.ts +573 -573
- package/src/bundler/__tests__/url-cap-and-slot-regex.test.ts +286 -286
- package/src/bundler/__tests__/vendor-cache.test.ts +455 -455
- package/src/bundler/budget.ts +404 -404
- package/src/bundler/build.test.ts +179 -179
- package/src/bundler/build.ts +55 -55
- package/src/bundler/dev.ts +42 -42
- package/src/bundler/manifest-schema.ts +301 -301
- package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +263 -263
- package/src/bundler/plugins/__tests__/react-compiler-config.test.ts +83 -83
- package/src/bundler/plugins/__tests__/react-compiler-lint.test.ts +110 -110
- package/src/bundler/plugins/block-generated-imports.ts +155 -155
- package/src/bundler/plugins/index.ts +83 -83
- package/src/bundler/plugins/react-compiler-config.ts +108 -108
- package/src/bundler/plugins/react-compiler-lint.ts +253 -253
- package/src/bundler/plugins/react-compiler.ts +162 -162
- package/src/bundler/reverse-import-graph.ts +339 -339
- package/src/bundler/safe-build.test.ts +201 -201
- package/src/bundler/safe-build.ts +103 -103
- package/src/bundler/scenario-matrix.ts +229 -229
- package/src/bundler/types.ts +10 -10
- package/src/bundler/vendor-cache-types.ts +130 -130
- package/src/bundler/vendor-cache.ts +526 -526
- package/src/change/snapshot.ts +18 -18
- package/src/client/Form.tsx +105 -105
- package/src/client/Link.tsx +9 -9
- package/src/client/__tests__/use-sse.test.ts +153 -153
- package/src/client/globals.ts +1 -1
- package/src/client/hooks.ts +362 -362
- package/src/client/hydrate.ts +340 -340
- package/src/client/prefetch-helper.ts +55 -55
- package/src/client/router.ts +11 -11
- package/src/client/runtime.ts +47 -47
- package/src/client/serialize.ts +404 -404
- package/src/client/use-fetch.ts +6 -6
- package/src/client/use-head.ts +197 -197
- package/src/client/use-sse.ts +378 -378
- package/src/client/window-state.ts +101 -101
- package/src/components/Image.tsx +162 -162
- package/src/config/validate.ts +3 -3
- package/src/config/watcher.ts +311 -311
- package/src/constants.ts +40 -40
- package/src/content/collection.ts +8 -8
- package/src/content/content-layer.ts +7 -7
- package/src/content/data-store.ts +245 -245
- package/src/content/frontmatter.ts +189 -189
- package/src/content/loader-context.ts +171 -171
- package/src/content/loaders/api.ts +216 -216
- package/src/content/loaders/file.ts +172 -172
- package/src/content/loaders/glob.ts +253 -253
- package/src/content/loaders/index.ts +34 -34
- package/src/content/loaders/types.ts +137 -137
- package/src/content/meta-store.ts +209 -209
- package/src/content/prebuild.test.ts +571 -571
- package/src/content/prebuild.ts +636 -636
- package/src/content/schema.ts +20 -20
- package/src/content/sidebar.ts +630 -630
- package/src/content/slug.ts +110 -110
- package/src/content/types.ts +282 -282
- package/src/content/watcher.ts +135 -135
- package/src/contract/client-safe.test.ts +42 -42
- package/src/contract/client-safe.ts +114 -114
- package/src/contract/define.ts +11 -11
- package/src/contract/index.ts +1 -1
- package/src/contract/normalize.test.ts +276 -276
- package/src/contract/normalize.ts +410 -410
- package/src/contract/registry.test.ts +206 -206
- package/src/contract/route-helpers.ts +1 -1
- package/src/contract/rpc.ts +443 -443
- package/src/contract/schema.ts +48 -48
- package/src/contract/types.ts +58 -58
- package/src/db/__tests__/db.test.ts +482 -482
- package/src/db/index.ts +138 -138
- package/src/db/migrations/history-table.ts +345 -345
- package/src/db/migrations/index.ts +3 -3
- package/src/db/migrations/lock.ts +324 -324
- package/src/db/migrations/runner.ts +650 -650
- package/src/deploy/cache.ts +140 -140
- package/src/deploy/compile/vercel.ts +344 -344
- package/src/deploy/index.ts +87 -87
- package/src/deploy/inference/brain.ts +268 -268
- package/src/deploy/inference/context.ts +182 -182
- package/src/deploy/inference/filling-extract.ts +245 -245
- package/src/deploy/inference/heuristic.ts +182 -182
- package/src/deploy/intent.ts +173 -173
- package/src/deploy/plan.ts +178 -178
- package/src/design/__tests__/agents-link.test.ts +109 -109
- package/src/design/__tests__/extract-patch-diff.test.ts +265 -265
- package/src/design/__tests__/lint.test.ts +110 -110
- package/src/design/__tests__/parser.test.ts +195 -195
- package/src/design/__tests__/tailwind-theme.test.ts +229 -229
- package/src/design/agents-link.ts +165 -165
- package/src/design/diff.ts +138 -138
- package/src/design/extract.ts +285 -285
- package/src/design/index.ts +102 -102
- package/src/design/lint.ts +209 -209
- package/src/design/parser.ts +555 -555
- package/src/design/patch.ts +241 -241
- package/src/design/scaffold.ts +147 -147
- package/src/design/tailwind-theme.ts +441 -441
- package/src/design/types.ts +210 -210
- package/src/desktop/__tests__/webview-fallback.test.ts +254 -254
- package/src/desktop/__tests__/window.test.ts +248 -248
- package/src/desktop/__tests__/worker.test.ts +266 -266
- package/src/desktop/index.ts +43 -43
- package/src/desktop/types.ts +158 -158
- package/src/desktop/worker.ts +180 -180
- package/src/dev-error-overlay/__tests__/overlay-injector.test.ts +241 -241
- package/src/dev-error-overlay/index.ts +30 -30
- package/src/dev-error-overlay/overlay-injector.ts +243 -243
- package/src/dev-error-overlay/overlay-styles.ts +52 -52
- package/src/dev-error-overlay/types.ts +66 -66
- package/src/devtools/ai/context-builder.ts +375 -375
- package/src/devtools/ai/index.ts +25 -25
- package/src/devtools/ai/mcp-connector.ts +25 -25
- package/src/devtools/client/catchers/error-catcher.ts +344 -344
- package/src/devtools/client/catchers/index.ts +18 -18
- package/src/devtools/client/components/index.ts +39 -39
- package/src/devtools/client/components/mandu-character.tsx +331 -331
- package/src/devtools/client/components/overlay.tsx +368 -368
- package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
- package/src/devtools/client/components/panel/guard-panel.tsx +30 -30
- package/src/devtools/client/components/panel/islands-panel.tsx +320 -320
- package/src/devtools/client/components/panel/network-panel.tsx +291 -291
- package/src/devtools/client/components/panel/panel-container.tsx +500 -500
- package/src/devtools/client/components/panel/preview-panel.tsx +46 -46
- package/src/devtools/client/filters/context-filters.ts +282 -282
- package/src/devtools/client/filters/index.ts +16 -16
- package/src/devtools/client/index.ts +63 -63
- package/src/devtools/client/persistence.ts +335 -335
- package/src/devtools/hook/create-hook.ts +207 -207
- package/src/devtools/hook/index.ts +13 -13
- package/src/devtools/index.ts +439 -439
- package/src/devtools/init.ts +265 -265
- package/src/devtools/protocol.ts +237 -237
- package/src/devtools/server/index.ts +17 -17
- package/src/devtools/server/source-context.ts +450 -450
- package/src/devtools/types.ts +35 -35
- package/src/devtools/worker/index.ts +25 -25
- package/src/devtools/worker/redaction-worker.ts +233 -233
- package/src/devtools/worker/worker-manager.ts +410 -410
- package/src/diagnose/__tests__/checks.test.ts +451 -451
- package/src/diagnose/checks.ts +832 -832
- package/src/diagnose/index.ts +17 -17
- package/src/diagnose/run.ts +93 -93
- package/src/diagnose/types.ts +53 -53
- package/src/email/__tests__/email.test.ts +355 -355
- package/src/email/index.ts +282 -282
- package/src/email/smtp.ts +64 -64
- package/src/error/domains.ts +265 -265
- package/src/error/result.ts +60 -60
- package/src/error/types.ts +6 -6
- package/src/errors/extractor.ts +409 -409
- package/src/errors/index.ts +19 -19
- package/src/filling/__tests__/session-sqlite.test.ts +454 -454
- package/src/filling/auth.ts +308 -308
- package/src/filling/cookie-codec.ts +299 -299
- package/src/filling/deps.ts +265 -265
- package/src/filling/session-sqlite.ts +617 -617
- package/src/filling/sse.ts +5 -5
- package/src/filling/ws.ts +78 -78
- package/src/generator/index.ts +3 -3
- package/src/guard/__tests__/design-inline-class.test.ts +219 -219
- package/src/guard/__tests__/tsgolint-bridge.test.ts +347 -347
- package/src/guard/analyzer.ts +360 -360
- package/src/guard/auto-correct.ts +1 -1
- package/src/guard/contract-guard.ts +9 -9
- package/src/guard/define-rule.ts +243 -243
- package/src/guard/design-inline-class.ts +393 -393
- package/src/guard/file-type.test.ts +24 -24
- package/src/guard/graph.ts +1 -1
- package/src/guard/healing.ts +36 -36
- package/src/guard/presets/atomic.ts +70 -70
- package/src/guard/presets/clean.ts +77 -77
- package/src/guard/presets/fsd.ts +79 -79
- package/src/guard/presets/hexagonal.ts +68 -68
- package/src/guard/reporter.ts +442 -442
- package/src/guard/rule-presets.ts +379 -379
- package/src/guard/semantic-slots.ts +1 -1
- package/src/guard/suggestions.ts +358 -358
- package/src/guard/tsgolint-bridge.ts +512 -512
- package/src/guard/types.ts +348 -348
- package/src/guard/watcher.ts +405 -405
- package/src/i18n/define.ts +126 -126
- package/src/i18n/index.ts +52 -52
- package/src/i18n/message-registry.ts +173 -173
- package/src/i18n/types.ts +112 -112
- package/src/id/index.ts +105 -105
- package/src/index.ts +2 -2
- package/src/kitchen/api/agent-devtools-api.ts +779 -544
- package/src/kitchen/api/errors-grouping.ts +126 -0
- package/src/kitchen/kitchen-handler.ts +192 -63
- package/src/kitchen/kitchen-ui.ts +842 -464
- package/src/logging/index.ts +22 -22
- package/src/logging/transports.ts +365 -365
- package/src/middleware/bridge.ts +147 -147
- package/src/middleware/compose.ts +134 -134
- package/src/middleware/compress.ts +62 -62
- package/src/middleware/cors.ts +47 -47
- package/src/middleware/csrf.ts +328 -328
- package/src/middleware/define.ts +132 -132
- package/src/middleware/jwt.ts +134 -134
- package/src/middleware/logger.ts +58 -58
- package/src/middleware/oauth/__tests__/oauth.test.ts +1 -1
- package/src/middleware/oauth/index.ts +505 -505
- package/src/middleware/oauth/providers.ts +115 -115
- package/src/middleware/rate-limit/index.ts +522 -522
- package/src/middleware/rate-limit/sqlite-store.ts +382 -382
- package/src/middleware/scheduler-cron.ts +96 -96
- package/src/middleware/secure/__tests__/secure.test.ts +360 -360
- package/src/middleware/secure/csp.ts +193 -193
- package/src/middleware/session.ts +174 -174
- package/src/middleware/timeout.ts +55 -55
- package/src/observability/logger-adapter.ts +36 -36
- package/src/observability/sqlite-store.ts +254 -254
- package/src/openapi/generator.ts +1 -1
- package/src/openapi/openapi.test.ts +43 -43
- package/src/perf/__tests__/user-marks.test.ts +354 -354
- package/src/perf/index.ts +133 -133
- package/src/plugins/__tests__/lifecycle-integration.test.ts +272 -272
- package/src/plugins/__tests__/runner.test.ts +409 -409
- package/src/plugins/define.ts +124 -124
- package/src/plugins/examples/dep-check-plugin.ts +80 -80
- package/src/plugins/examples/prerender-cache-plugin.ts +111 -111
- package/src/plugins/examples/sitemap-plugin.ts +65 -65
- package/src/plugins/runner.ts +361 -361
- package/src/plugins/types.ts +368 -368
- package/src/report/index.ts +1 -1
- package/src/resource/__tests__/generator.test.ts +32 -32
- package/src/resource/ddl/__tests__/diff.test.ts +639 -639
- package/src/resource/ddl/__tests__/emit.test.ts +823 -823
- package/src/resource/ddl/__tests__/snapshot.test.ts +499 -499
- package/src/resource/ddl/emit.ts +559 -559
- package/src/resource/ddl/persistence-types.ts +218 -218
- package/src/resource/ddl/type-map.ts +223 -223
- package/src/resource/ddl/types.ts +232 -232
- package/src/resource/generator-repo.ts +630 -630
- package/src/resource/schema.ts +1 -1
- package/src/router/fs-patterns.test.ts +96 -96
- package/src/router/fs-routes.ts +6 -6
- package/src/routes/index.ts +74 -74
- package/src/routes/metadata-routes.ts +427 -427
- package/src/routes/types.ts +341 -341
- package/src/runtime/__tests__/error-boundary-redaction.test.ts +141 -141
- package/src/runtime/__tests__/hdr-client.test.ts +223 -223
- package/src/runtime/__tests__/http-errors.test.ts +117 -117
- package/src/runtime/__tests__/not-found.test.ts +152 -152
- package/src/runtime/adapter.ts +47 -47
- package/src/runtime/boundary.tsx +252 -252
- package/src/runtime/cache.ts +494 -494
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/fast-refresh-runtime.ts +322 -322
- package/src/runtime/handler.ts +65 -65
- package/src/runtime/handlers.ts +300 -300
- package/src/runtime/http-errors.ts +113 -113
- package/src/runtime/image-handler.ts +1 -1
- package/src/runtime/lifecycle.ts +381 -381
- package/src/runtime/logger.test.ts +345 -345
- package/src/runtime/middleware.ts +264 -264
- package/src/runtime/not-found.ts +93 -93
- package/src/runtime/openapi-endpoint.ts +236 -236
- package/src/runtime/ppr.ts +74 -74
- package/src/runtime/registry.ts +171 -171
- package/src/runtime/router.ts +105 -105
- package/src/runtime/server.ts +67 -67
- package/src/runtime/shims.ts +48 -48
- package/src/runtime/trace.ts +144 -144
- package/src/scheduler/validate.ts +169 -169
- package/src/seo/index.ts +219 -219
- package/src/seo/integration/ssr.ts +306 -306
- package/src/seo/render/basic.ts +435 -435
- package/src/seo/render/index.ts +143 -143
- package/src/seo/render/jsonld.ts +539 -539
- package/src/seo/render/opengraph.ts +197 -197
- package/src/seo/render/robots.ts +116 -116
- package/src/seo/render/sitemap.ts +137 -137
- package/src/seo/render/twitter.ts +127 -127
- package/src/seo/resolve/opengraph.ts +143 -143
- package/src/seo/resolve/robots.ts +73 -73
- package/src/seo/resolve/title.ts +94 -94
- package/src/seo/resolve/twitter.ts +73 -73
- package/src/seo/resolve/url.ts +104 -104
- package/src/seo/routes/index.ts +290 -290
- package/src/seo/types.ts +588 -588
- package/src/slot/validator.ts +39 -39
- package/src/storage/s3/__tests__/s3.test.ts +479 -479
- package/src/storage/s3/index.ts +412 -412
- package/src/testing/__tests__/assertions.test.ts +632 -632
- package/src/testing/__tests__/reporter.test.ts +454 -454
- package/src/testing/assertions.ts +986 -986
- package/src/testing/db.ts +157 -157
- package/src/testing/mocks.ts +203 -203
- package/src/testing/session.ts +190 -190
- package/src/types/branded.ts +56 -56
- package/src/types/index.ts +1 -1
- package/src/utils/safe-io.ts +188 -188
- package/src/utils/string-safe.ts +298 -298
- package/src/watcher/watcher.ts +18 -18
|
@@ -1,189 +1,189 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Frontmatter Parser (Issue #199)
|
|
3
|
-
*
|
|
4
|
-
* Minimal, dependency-free YAML frontmatter parser for the MVP
|
|
5
|
-
* `defineCollection()` API. Handles the common subset used in docs
|
|
6
|
-
* collections — enough to cover the `title`/`order`/`draft`/`tags`
|
|
7
|
-
* patterns called out by the feature spec without pulling in
|
|
8
|
-
* `gray-matter` or `yaml` as a runtime dep.
|
|
9
|
-
*
|
|
10
|
-
* # Supported frontmatter syntax
|
|
11
|
-
*
|
|
12
|
-
* - Standard `---` fenced YAML at the top of the file
|
|
13
|
-
* - CRLF or LF line endings (so Windows authors do not have to
|
|
14
|
-
* convert before committing)
|
|
15
|
-
* - Scalar values: strings, numbers (int/float), booleans, null
|
|
16
|
-
* - Quoted strings: single + double quotes (with trailing content
|
|
17
|
-
* preservation — escape sequences NOT interpreted)
|
|
18
|
-
* - Simple inline arrays: `tags: [a, b, c]`
|
|
19
|
-
* - Block arrays:
|
|
20
|
-
* tags:
|
|
21
|
-
* - a
|
|
22
|
-
* - b
|
|
23
|
-
* - Comments (`#`) at end of line and on their own line
|
|
24
|
-
*
|
|
25
|
-
* # Explicitly unsupported (out of MVP scope)
|
|
26
|
-
*
|
|
27
|
-
* - Nested maps (`author: { name: ... }`) — use JSON-style inline
|
|
28
|
-
* or restructure with a Zod transform
|
|
29
|
-
* - Anchors, aliases, tags (`&anchor`, `*ref`, `!!tag`)
|
|
30
|
-
* - Multi-line scalars (`>` / `|`)
|
|
31
|
-
* - Complex quoted string escape sequences
|
|
32
|
-
*
|
|
33
|
-
* Projects that outgrow this parser can install `yaml` themselves and
|
|
34
|
-
* wrap their collection with a custom loader — the existing
|
|
35
|
-
* `glob()` loader in `./loaders/glob.ts` already dynamic-imports
|
|
36
|
-
* `yaml` when present.
|
|
37
|
-
*/
|
|
38
|
-
|
|
39
|
-
/** Result of parsing a Markdown file with optional frontmatter. */
|
|
40
|
-
export interface ParsedFrontmatter {
|
|
41
|
-
/** Parsed key/value pairs from the YAML block (empty if no frontmatter). */
|
|
42
|
-
data: Record<string, unknown>;
|
|
43
|
-
/** Body content following the closing `---` fence (or whole file if none). */
|
|
44
|
-
body: string;
|
|
45
|
-
/** Raw YAML text inside the fences — useful for diagnostics. */
|
|
46
|
-
raw?: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Parse a Markdown-style source string into frontmatter data + body.
|
|
51
|
-
*
|
|
52
|
-
* When the source does NOT start with `---`, returns `{ data: {}, body: src }`
|
|
53
|
-
* verbatim — callers should treat that as "no frontmatter" rather than a
|
|
54
|
-
* parse error. When the source DOES open with `---` but the block is
|
|
55
|
-
* malformed (no closing fence), throws so the calling `Collection.load()`
|
|
56
|
-
* can attach the file path for reporting.
|
|
57
|
-
*/
|
|
58
|
-
export function parseFrontmatter(src: string): ParsedFrontmatter {
|
|
59
|
-
// Normalize line endings once up front so the fence regex and the
|
|
60
|
-
// per-line scanner agree on `\n` as the only delimiter. We preserve
|
|
61
|
-
// the body exactly as authored except for this normalization.
|
|
62
|
-
const normalized = src.replace(/\r\n/g, "\n");
|
|
63
|
-
const match = normalized.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
64
|
-
if (!match) {
|
|
65
|
-
// The spec's MVP never requires frontmatter — a .md file without
|
|
66
|
-
// it just becomes an entry with empty `data` and the whole file
|
|
67
|
-
// as `body`. Zod validation downstream will catch missing required
|
|
68
|
-
// fields and report them with a helpful collection/entry label.
|
|
69
|
-
return { data: {}, body: src };
|
|
70
|
-
}
|
|
71
|
-
const [, rawFront, body] = match;
|
|
72
|
-
const data = parseSimpleYaml(rawFront);
|
|
73
|
-
return { data, body: body.replace(/^\n/, ""), raw: rawFront };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Parse a small, opinionated subset of YAML. Not a general-purpose YAML
|
|
78
|
-
* parser — see the file header for the supported subset.
|
|
79
|
-
*/
|
|
80
|
-
export function parseSimpleYaml(text: string): Record<string, unknown> {
|
|
81
|
-
const out: Record<string, unknown> = {};
|
|
82
|
-
const lines = text.split("\n");
|
|
83
|
-
let i = 0;
|
|
84
|
-
while (i < lines.length) {
|
|
85
|
-
const rawLine = lines[i] ?? "";
|
|
86
|
-
const line = stripComment(rawLine);
|
|
87
|
-
if (line.trim() === "") {
|
|
88
|
-
i++;
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
// Top-level entries must be unindented — nested maps are out of
|
|
92
|
-
// scope, and treating indented lines as their own keys would mask
|
|
93
|
-
// real parse errors.
|
|
94
|
-
if (/^\s/.test(line) && !/^\s*-\s/.test(line)) {
|
|
95
|
-
// Unexpected indentation at top level — skip defensively rather
|
|
96
|
-
// than throw, since some authors add stray spaces.
|
|
97
|
-
i++;
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
const m = line.match(/^([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)$/);
|
|
101
|
-
if (!m) {
|
|
102
|
-
i++;
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
const [, key, rest] = m;
|
|
106
|
-
const value = rest.trim();
|
|
107
|
-
if (value === "") {
|
|
108
|
-
// Could be a block array — peek ahead for ` - item` lines.
|
|
109
|
-
const blockItems: unknown[] = [];
|
|
110
|
-
let j = i + 1;
|
|
111
|
-
while (j < lines.length) {
|
|
112
|
-
const next = stripComment(lines[j] ?? "");
|
|
113
|
-
const itemMatch = next.match(/^\s+-\s+(.+)$/);
|
|
114
|
-
if (!itemMatch) break;
|
|
115
|
-
blockItems.push(parseScalar(itemMatch[1].trim()));
|
|
116
|
-
j++;
|
|
117
|
-
}
|
|
118
|
-
if (blockItems.length > 0) {
|
|
119
|
-
out[key] = blockItems;
|
|
120
|
-
i = j;
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
// Empty value with no following list — store as null rather than
|
|
124
|
-
// empty string so downstream Zod `.optional()` behaves correctly.
|
|
125
|
-
out[key] = null;
|
|
126
|
-
i++;
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
out[key] = parseScalar(value);
|
|
130
|
-
i++;
|
|
131
|
-
}
|
|
132
|
-
return out;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Strip a trailing `# comment` from a line, respecting quoted strings
|
|
137
|
-
* so `title: "pricing # free"` does not lose the `# free` tail.
|
|
138
|
-
*/
|
|
139
|
-
function stripComment(line: string): string {
|
|
140
|
-
let inSingle = false;
|
|
141
|
-
let inDouble = false;
|
|
142
|
-
for (let k = 0; k < line.length; k++) {
|
|
143
|
-
const ch = line[k];
|
|
144
|
-
if (ch === "'" && !inDouble) inSingle = !inSingle;
|
|
145
|
-
else if (ch === '"' && !inSingle) inDouble = !inDouble;
|
|
146
|
-
else if (ch === "#" && !inSingle && !inDouble) {
|
|
147
|
-
return line.slice(0, k);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
return line;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Coerce a trimmed scalar token into the most appropriate JS primitive.
|
|
155
|
-
* The ordering here matters — booleans and null must come before the
|
|
156
|
-
* number check so `null`/`true` aren't accidentally parsed as NaN.
|
|
157
|
-
*/
|
|
158
|
-
export function parseScalar(v: string): unknown {
|
|
159
|
-
if (v === "") return null;
|
|
160
|
-
// Inline array: `[a, b, c]` — parse each element as a scalar. We
|
|
161
|
-
// intentionally use a non-quoted split because the spec's MVP only
|
|
162
|
-
// needs flat tag lists; nested arrays go via the block form.
|
|
163
|
-
if (v.startsWith("[") && v.endsWith("]")) {
|
|
164
|
-
const inner = v.slice(1, -1).trim();
|
|
165
|
-
if (inner === "") return [];
|
|
166
|
-
return inner.split(",").map((x) => parseScalar(x.trim()));
|
|
167
|
-
}
|
|
168
|
-
if (
|
|
169
|
-
(v.startsWith('"') && v.endsWith('"')) ||
|
|
170
|
-
(v.startsWith("'") && v.endsWith("'"))
|
|
171
|
-
) {
|
|
172
|
-
return v.slice(1, -1);
|
|
173
|
-
}
|
|
174
|
-
if (v === "true") return true;
|
|
175
|
-
if (v === "false") return false;
|
|
176
|
-
if (v === "null" || v === "~") return null;
|
|
177
|
-
// Number check: accept int + float, reject leading-zero-padded (e.g.
|
|
178
|
-
// `007`) to stay consistent with YAML 1.2 — treat those as strings
|
|
179
|
-
// so ID fields are preserved.
|
|
180
|
-
if (/^-?\d+$/.test(v) && !/^-?0\d/.test(v)) {
|
|
181
|
-
const n = Number(v);
|
|
182
|
-
if (Number.isFinite(n)) return n;
|
|
183
|
-
}
|
|
184
|
-
if (/^-?\d+\.\d+$/.test(v)) {
|
|
185
|
-
const n = Number(v);
|
|
186
|
-
if (Number.isFinite(n)) return n;
|
|
187
|
-
}
|
|
188
|
-
return v;
|
|
189
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Frontmatter Parser (Issue #199)
|
|
3
|
+
*
|
|
4
|
+
* Minimal, dependency-free YAML frontmatter parser for the MVP
|
|
5
|
+
* `defineCollection()` API. Handles the common subset used in docs
|
|
6
|
+
* collections — enough to cover the `title`/`order`/`draft`/`tags`
|
|
7
|
+
* patterns called out by the feature spec without pulling in
|
|
8
|
+
* `gray-matter` or `yaml` as a runtime dep.
|
|
9
|
+
*
|
|
10
|
+
* # Supported frontmatter syntax
|
|
11
|
+
*
|
|
12
|
+
* - Standard `---` fenced YAML at the top of the file
|
|
13
|
+
* - CRLF or LF line endings (so Windows authors do not have to
|
|
14
|
+
* convert before committing)
|
|
15
|
+
* - Scalar values: strings, numbers (int/float), booleans, null
|
|
16
|
+
* - Quoted strings: single + double quotes (with trailing content
|
|
17
|
+
* preservation — escape sequences NOT interpreted)
|
|
18
|
+
* - Simple inline arrays: `tags: [a, b, c]`
|
|
19
|
+
* - Block arrays:
|
|
20
|
+
* tags:
|
|
21
|
+
* - a
|
|
22
|
+
* - b
|
|
23
|
+
* - Comments (`#`) at end of line and on their own line
|
|
24
|
+
*
|
|
25
|
+
* # Explicitly unsupported (out of MVP scope)
|
|
26
|
+
*
|
|
27
|
+
* - Nested maps (`author: { name: ... }`) — use JSON-style inline
|
|
28
|
+
* or restructure with a Zod transform
|
|
29
|
+
* - Anchors, aliases, tags (`&anchor`, `*ref`, `!!tag`)
|
|
30
|
+
* - Multi-line scalars (`>` / `|`)
|
|
31
|
+
* - Complex quoted string escape sequences
|
|
32
|
+
*
|
|
33
|
+
* Projects that outgrow this parser can install `yaml` themselves and
|
|
34
|
+
* wrap their collection with a custom loader — the existing
|
|
35
|
+
* `glob()` loader in `./loaders/glob.ts` already dynamic-imports
|
|
36
|
+
* `yaml` when present.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/** Result of parsing a Markdown file with optional frontmatter. */
|
|
40
|
+
export interface ParsedFrontmatter {
|
|
41
|
+
/** Parsed key/value pairs from the YAML block (empty if no frontmatter). */
|
|
42
|
+
data: Record<string, unknown>;
|
|
43
|
+
/** Body content following the closing `---` fence (or whole file if none). */
|
|
44
|
+
body: string;
|
|
45
|
+
/** Raw YAML text inside the fences — useful for diagnostics. */
|
|
46
|
+
raw?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parse a Markdown-style source string into frontmatter data + body.
|
|
51
|
+
*
|
|
52
|
+
* When the source does NOT start with `---`, returns `{ data: {}, body: src }`
|
|
53
|
+
* verbatim — callers should treat that as "no frontmatter" rather than a
|
|
54
|
+
* parse error. When the source DOES open with `---` but the block is
|
|
55
|
+
* malformed (no closing fence), throws so the calling `Collection.load()`
|
|
56
|
+
* can attach the file path for reporting.
|
|
57
|
+
*/
|
|
58
|
+
export function parseFrontmatter(src: string): ParsedFrontmatter {
|
|
59
|
+
// Normalize line endings once up front so the fence regex and the
|
|
60
|
+
// per-line scanner agree on `\n` as the only delimiter. We preserve
|
|
61
|
+
// the body exactly as authored except for this normalization.
|
|
62
|
+
const normalized = src.replace(/\r\n/g, "\n");
|
|
63
|
+
const match = normalized.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
64
|
+
if (!match) {
|
|
65
|
+
// The spec's MVP never requires frontmatter — a .md file without
|
|
66
|
+
// it just becomes an entry with empty `data` and the whole file
|
|
67
|
+
// as `body`. Zod validation downstream will catch missing required
|
|
68
|
+
// fields and report them with a helpful collection/entry label.
|
|
69
|
+
return { data: {}, body: src };
|
|
70
|
+
}
|
|
71
|
+
const [, rawFront, body] = match;
|
|
72
|
+
const data = parseSimpleYaml(rawFront);
|
|
73
|
+
return { data, body: body.replace(/^\n/, ""), raw: rawFront };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Parse a small, opinionated subset of YAML. Not a general-purpose YAML
|
|
78
|
+
* parser — see the file header for the supported subset.
|
|
79
|
+
*/
|
|
80
|
+
export function parseSimpleYaml(text: string): Record<string, unknown> {
|
|
81
|
+
const out: Record<string, unknown> = {};
|
|
82
|
+
const lines = text.split("\n");
|
|
83
|
+
let i = 0;
|
|
84
|
+
while (i < lines.length) {
|
|
85
|
+
const rawLine = lines[i] ?? "";
|
|
86
|
+
const line = stripComment(rawLine);
|
|
87
|
+
if (line.trim() === "") {
|
|
88
|
+
i++;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
// Top-level entries must be unindented — nested maps are out of
|
|
92
|
+
// scope, and treating indented lines as their own keys would mask
|
|
93
|
+
// real parse errors.
|
|
94
|
+
if (/^\s/.test(line) && !/^\s*-\s/.test(line)) {
|
|
95
|
+
// Unexpected indentation at top level — skip defensively rather
|
|
96
|
+
// than throw, since some authors add stray spaces.
|
|
97
|
+
i++;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const m = line.match(/^([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)$/);
|
|
101
|
+
if (!m) {
|
|
102
|
+
i++;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const [, key, rest] = m;
|
|
106
|
+
const value = rest.trim();
|
|
107
|
+
if (value === "") {
|
|
108
|
+
// Could be a block array — peek ahead for ` - item` lines.
|
|
109
|
+
const blockItems: unknown[] = [];
|
|
110
|
+
let j = i + 1;
|
|
111
|
+
while (j < lines.length) {
|
|
112
|
+
const next = stripComment(lines[j] ?? "");
|
|
113
|
+
const itemMatch = next.match(/^\s+-\s+(.+)$/);
|
|
114
|
+
if (!itemMatch) break;
|
|
115
|
+
blockItems.push(parseScalar(itemMatch[1].trim()));
|
|
116
|
+
j++;
|
|
117
|
+
}
|
|
118
|
+
if (blockItems.length > 0) {
|
|
119
|
+
out[key] = blockItems;
|
|
120
|
+
i = j;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
// Empty value with no following list — store as null rather than
|
|
124
|
+
// empty string so downstream Zod `.optional()` behaves correctly.
|
|
125
|
+
out[key] = null;
|
|
126
|
+
i++;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
out[key] = parseScalar(value);
|
|
130
|
+
i++;
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Strip a trailing `# comment` from a line, respecting quoted strings
|
|
137
|
+
* so `title: "pricing # free"` does not lose the `# free` tail.
|
|
138
|
+
*/
|
|
139
|
+
function stripComment(line: string): string {
|
|
140
|
+
let inSingle = false;
|
|
141
|
+
let inDouble = false;
|
|
142
|
+
for (let k = 0; k < line.length; k++) {
|
|
143
|
+
const ch = line[k];
|
|
144
|
+
if (ch === "'" && !inDouble) inSingle = !inSingle;
|
|
145
|
+
else if (ch === '"' && !inSingle) inDouble = !inDouble;
|
|
146
|
+
else if (ch === "#" && !inSingle && !inDouble) {
|
|
147
|
+
return line.slice(0, k);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return line;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Coerce a trimmed scalar token into the most appropriate JS primitive.
|
|
155
|
+
* The ordering here matters — booleans and null must come before the
|
|
156
|
+
* number check so `null`/`true` aren't accidentally parsed as NaN.
|
|
157
|
+
*/
|
|
158
|
+
export function parseScalar(v: string): unknown {
|
|
159
|
+
if (v === "") return null;
|
|
160
|
+
// Inline array: `[a, b, c]` — parse each element as a scalar. We
|
|
161
|
+
// intentionally use a non-quoted split because the spec's MVP only
|
|
162
|
+
// needs flat tag lists; nested arrays go via the block form.
|
|
163
|
+
if (v.startsWith("[") && v.endsWith("]")) {
|
|
164
|
+
const inner = v.slice(1, -1).trim();
|
|
165
|
+
if (inner === "") return [];
|
|
166
|
+
return inner.split(",").map((x) => parseScalar(x.trim()));
|
|
167
|
+
}
|
|
168
|
+
if (
|
|
169
|
+
(v.startsWith('"') && v.endsWith('"')) ||
|
|
170
|
+
(v.startsWith("'") && v.endsWith("'"))
|
|
171
|
+
) {
|
|
172
|
+
return v.slice(1, -1);
|
|
173
|
+
}
|
|
174
|
+
if (v === "true") return true;
|
|
175
|
+
if (v === "false") return false;
|
|
176
|
+
if (v === "null" || v === "~") return null;
|
|
177
|
+
// Number check: accept int + float, reject leading-zero-padded (e.g.
|
|
178
|
+
// `007`) to stay consistent with YAML 1.2 — treat those as strings
|
|
179
|
+
// so ID fields are preserved.
|
|
180
|
+
if (/^-?\d+$/.test(v) && !/^-?0\d/.test(v)) {
|
|
181
|
+
const n = Number(v);
|
|
182
|
+
if (Number.isFinite(n)) return n;
|
|
183
|
+
}
|
|
184
|
+
if (/^-?\d+\.\d+$/.test(v)) {
|
|
185
|
+
const n = Number(v);
|
|
186
|
+
if (Number.isFinite(n)) return n;
|
|
187
|
+
}
|
|
188
|
+
return v;
|
|
189
|
+
}
|