@mandujs/core 0.54.32 → 0.55.0-beta.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 +106 -210
- package/scripts/postinstall-lock.ts +153 -153
- 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 +15 -15
- package/src/a11y/types.ts +125 -125
- package/src/agent/__tests__/apply.test.ts +224 -0
- package/src/agent/__tests__/context.test.ts +97 -98
- package/src/agent/apply.ts +1031 -0
- package/src/agent/context.ts +552 -552
- package/src/agent/index.ts +4 -3
- package/src/agent/plan.ts +161 -243
- package/src/agent/repair.ts +239 -162
- package/src/agent/sync.ts +197 -198
- package/src/agent/types.ts +242 -94
- package/src/agent/verify.ts +55 -55
- package/src/auth/__tests__/login.test.ts +1 -1
- package/src/auth/__tests__/password.test.ts +15 -5
- package/src/auth/__tests__/reset.test.ts +2 -2
- 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 +14 -2
- 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/brain/doctor/analyzer.ts +7 -7
- package/src/bundler/__snapshots__/build.test.ts.snap +5 -5
- package/src/bundler/__tests__/build-runner.ts +166 -166
- package/src/bundler/__tests__/client-boundary-transform.test.ts +524 -524
- package/src/bundler/__tests__/cold-start.test.ts +60 -60
- package/src/bundler/__tests__/css.test.ts +49 -20
- 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 +24 -24
- package/src/bundler/__tests__/generation.test.ts +447 -0
- package/src/bundler/__tests__/hdr.test.ts +24 -18
- package/src/bundler/__tests__/hmr-client.test.ts +62 -24
- package/src/bundler/__tests__/jsx-runtime-shim.test.ts +179 -140
- package/src/bundler/__tests__/manifest-schema.test.ts +305 -266
- package/src/bundler/__tests__/prod-smoke.test.ts +138 -138
- package/src/bundler/__tests__/reverse-import-graph.test.ts +42 -42
- 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/analyzer.ts +15 -15
- package/src/bundler/budget.ts +404 -404
- package/src/bundler/build.test.ts +898 -915
- package/src/bundler/build.ts +113 -24
- package/src/bundler/client-boundary-transform.ts +977 -977
- package/src/bundler/css.ts +65 -44
- package/src/bundler/dev.ts +105 -73
- package/src/bundler/fast-refresh-preamble.ts +47 -47
- package/src/bundler/generation.ts +602 -0
- package/src/bundler/index.ts +3 -3
- package/src/bundler/manifest-schema.ts +55 -40
- package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +13 -13
- 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 +16 -15
- 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/prerender.ts +29 -9
- package/src/bundler/reverse-import-graph.ts +339 -339
- package/src/bundler/safe-build.test.ts +1 -1
- package/src/bundler/safe-build.ts +103 -103
- package/src/bundler/scenario-matrix.ts +229 -229
- package/src/bundler/types.ts +58 -56
- 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__/props-serialization.test.ts +37 -37
- 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 +2 -2
- package/src/client/index.ts +1 -1
- package/src/client/island.ts +79 -79
- package/src/client/prefetch-helper.ts +55 -55
- package/src/client/props-serialization.ts +233 -233
- package/src/client/runtime-entry.ts +598 -598
- package/src/client/runtime.ts +1 -1
- package/src/client/serialize.ts +50 -50
- 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-compat.ts +3 -0
- package/src/components/Image.tsx +162 -162
- package/src/config/validate.ts +1 -1
- package/src/config/watcher.ts +311 -311
- package/src/constants.ts +40 -40
- package/src/content/collection.ts +9 -9
- 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/generate-types.ts +1 -1
- package/src/content/index.ts +2 -2
- 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/types.ts +58 -58
- package/src/db/__tests__/db.test.ts +9 -9
- package/src/db/index.ts +2 -2
- 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 +82 -82
- 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__/smoke.test.ts +2 -2
- package/src/desktop/__tests__/webview-fallback.test.ts +12 -32
- package/src/desktop/__tests__/window.test.ts +20 -99
- 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/webview-fallback.ts +17 -4
- package/src/desktop/window.ts +17 -8
- 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/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 +16 -16
- package/src/devtools/client/components/panel/network-panel.tsx +291 -291
- package/src/devtools/client/components/panel/panel-container.tsx +1 -1
- 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/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/types.ts +35 -35
- package/src/devtools/worker/index.ts +25 -25
- package/src/devtools/worker/redaction-worker.ts +233 -233
- package/src/diagnose/__tests__/checks.test.ts +136 -136
- package/src/diagnose/checks.ts +185 -185
- package/src/diagnose/index.ts +17 -17
- package/src/diagnose/run.ts +10 -10
- 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/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 +5 -1
- package/src/filling/auth.ts +308 -308
- package/src/filling/body-parse.test.ts +60 -60
- package/src/filling/cookie-codec.ts +5 -3
- package/src/filling/deps.ts +265 -265
- package/src/filling/head-method.test.ts +154 -154
- 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/generate.ts +30 -30
- package/src/generator/index.ts +3 -3
- package/src/generator/templates.test.ts +48 -48
- package/src/generator/templates.ts +219 -219
- 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/check.ts +39 -24
- package/src/guard/config-guard.ts +13 -13
- 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/fs-routes-policy.ts +51 -51
- package/src/guard/graph.ts +1 -1
- package/src/guard/healing.ts +36 -36
- package/src/guard/index.ts +11 -11
- 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/__tests__/id.test.ts +3 -3
- package/src/id/index.ts +105 -105
- package/src/index.ts +9 -48
- package/src/internal/client-boundary.ts +266 -266
- package/src/internal/index.ts +2 -2
- package/src/kitchen/api/agent-devtools-api.ts +779 -779
- package/src/kitchen/api/bundle-inspector.ts +179 -0
- package/src/kitchen/api/errors-grouping.ts +126 -126
- package/src/kitchen/api/file-api.ts +11 -11
- package/src/kitchen/kitchen-handler.ts +36 -0
- package/src/kitchen/kitchen-ui.ts +456 -0
- 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/perf/user-marks.ts +1 -1
- 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 +7 -7
- package/src/resource/__tests__/schema.test.ts +14 -14
- package/src/resource/ddl/__tests__/diff.test.ts +639 -639
- package/src/resource/ddl/__tests__/emit.test.ts +165 -165
- package/src/resource/ddl/__tests__/snapshot.test.ts +499 -499
- package/src/resource/ddl/emit.ts +146 -146
- 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/generator-schema.ts +11 -11
- package/src/resource/generators/slot.ts +72 -72
- package/src/resource/schema.ts +21 -21
- package/src/router/client-entry.test.ts +227 -227
- package/src/router/client-entry.ts +218 -218
- package/src/router/fs-patterns.test.ts +96 -96
- package/src/router/fs-routes.test.ts +532 -532
- package/src/router/fs-routes.ts +53 -53
- package/src/router/fs-scanner.ts +216 -216
- package/src/router/fs-types.ts +19 -19
- package/src/router/route-source-analyzer.ts +521 -521
- 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__/devtools-adapter.test.ts +68 -68
- 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__/inline-client-hydration.test.ts +237 -237
- package/src/runtime/__tests__/not-found.test.ts +152 -152
- package/src/runtime/__tests__/observability-lifecycle.test.ts +103 -103
- package/src/runtime/__tests__/page-render-response.test.ts +517 -517
- package/src/runtime/__tests__/request-middleware.test.ts +70 -70
- package/src/runtime/__tests__/searchparams-page-props.test.ts +81 -81
- 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/devtools-adapter.ts +68 -68
- package/src/runtime/escape.ts +34 -34
- package/src/runtime/fast-refresh-runtime.ts +322 -322
- package/src/runtime/fast-refresh-types.ts +1 -1
- package/src/runtime/handler.ts +65 -65
- package/src/runtime/handlers.ts +30 -11
- 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/observability-lifecycle.ts +290 -290
- package/src/runtime/openapi-endpoint.ts +236 -236
- package/src/runtime/page-render-response.ts +287 -274
- package/src/runtime/ppr.ts +74 -74
- package/src/runtime/rate-limit.ts +1 -1
- package/src/runtime/redirect.ts +1 -1
- package/src/runtime/registry.ts +171 -171
- package/src/runtime/request-middleware.ts +31 -31
- package/src/runtime/router.test.ts +4 -4
- package/src/runtime/router.ts +10 -10
- package/src/runtime/server.ts +127 -211
- package/src/runtime/shims.ts +48 -48
- package/src/runtime/ssr.ts +43 -17
- package/src/runtime/static-files.ts +369 -289
- package/src/runtime/streaming-ssr.ts +219 -180
- package/src/runtime/trace.ts +144 -144
- package/src/scheduler/__tests__/scheduler.test.ts +12 -11
- package/src/scheduler/index.ts +11 -5
- 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/spec/schema.ts +35 -35
- 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/index.ts +9 -3
- package/src/testing/lcov.ts +192 -0
- 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/__tests__/watcher.test.ts +59 -59
- package/src/watcher/watcher.ts +61 -61
package/src/design/parser.ts
CHANGED
|
@@ -1,555 +1,555 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DESIGN.md parser — the 9-section heading walker.
|
|
3
|
-
*
|
|
4
|
-
* Strategy:
|
|
5
|
-
* 1. Optional H1 title at the top of the file.
|
|
6
|
-
* 2. Scan H2 headings. Match each against the 9 canonical sections
|
|
7
|
-
* via a fuzzy-but-bounded resolver (lowercase, strip `&` / `'`,
|
|
8
|
-
* collapse whitespace). Unrecognized headings land in
|
|
9
|
-
* `extraSections` so the file round-trips cleanly.
|
|
10
|
-
* 3. For each matched section, parse the body into structured
|
|
11
|
-
* tokens with a section-specific extractor. Extractors are
|
|
12
|
-
* forgiving — a malformed row is skipped, never fatal.
|
|
13
|
-
*
|
|
14
|
-
* Parsing must NEVER throw on real-world DESIGN.md files. The 69
|
|
15
|
-
* brand entries in awesome-design-md vary wildly in formatting; the
|
|
16
|
-
* parser is the contract that smooths them out for the rest of Mandu.
|
|
17
|
-
*
|
|
18
|
-
* @module core/design/parser
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import type {
|
|
22
|
-
AgentPrompt,
|
|
23
|
-
AnyDesignSection,
|
|
24
|
-
ColorToken,
|
|
25
|
-
ComponentToken,
|
|
26
|
-
DesignSectionId,
|
|
27
|
-
DesignSpec,
|
|
28
|
-
DoDontRule,
|
|
29
|
-
ResponsiveBreakpoint,
|
|
30
|
-
ShadowToken,
|
|
31
|
-
SpacingToken,
|
|
32
|
-
TypographyToken,
|
|
33
|
-
ValidationIssue,
|
|
34
|
-
ValidationResult,
|
|
35
|
-
} from "./types";
|
|
36
|
-
|
|
37
|
-
import { DESIGN_SECTION_IDS } from "./types";
|
|
38
|
-
|
|
39
|
-
// ────────────────────────────────────────────────────────────────────
|
|
40
|
-
// Heading → section-id resolution
|
|
41
|
-
// ────────────────────────────────────────────────────────────────────
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Match heading text against the 9 canonical section ids. Returns the
|
|
45
|
-
* id when the heading "looks like" the section by keyword, else null.
|
|
46
|
-
* The keyword sets are intentionally redundant to absorb the wording
|
|
47
|
-
* differences across the awesome-design-md catalog.
|
|
48
|
-
*/
|
|
49
|
-
function resolveSectionId(heading: string): DesignSectionId | null {
|
|
50
|
-
const h = heading
|
|
51
|
-
.toLowerCase()
|
|
52
|
-
.replace(/[&'`*_~]/g, "")
|
|
53
|
-
.replace(/\s+/g, " ")
|
|
54
|
-
.trim();
|
|
55
|
-
if (/^visual|^theme|^philosophy|^vibe|^aesthetic/.test(h)) return "theme";
|
|
56
|
-
if (/color|palette/.test(h)) return "color-palette";
|
|
57
|
-
if (/typograph|typeface|font|type scale/.test(h)) return "typography";
|
|
58
|
-
if (/component|button|card|input/.test(h)) return "components";
|
|
59
|
-
if (/layout|spacing|grid|whitespace/.test(h)) return "layout";
|
|
60
|
-
if (/shadow|elevation|depth/.test(h)) return "shadows";
|
|
61
|
-
if (/do.{0,3}dont|guideline|principle|rule/.test(h)) return "dos-donts";
|
|
62
|
-
if (/responsive|breakpoint|mobile|tablet|desktop/.test(h)) return "responsive";
|
|
63
|
-
if (/agent|prompt|llm|ai/.test(h)) return "agent-prompts";
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// ────────────────────────────────────────────────────────────────────
|
|
68
|
-
// H2 splitter
|
|
69
|
-
// ────────────────────────────────────────────────────────────────────
|
|
70
|
-
|
|
71
|
-
interface RawSection {
|
|
72
|
-
heading: string;
|
|
73
|
-
/** `1` or `2` — H1 or H2. */
|
|
74
|
-
level: number;
|
|
75
|
-
body: string;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Strip HTML comments (`<!-- … -->`) before structural parsing. The
|
|
80
|
-
* empty-skeleton template uses HTML comments to show example tokens
|
|
81
|
-
* without having them counted as real tokens; the parser must respect
|
|
82
|
-
* that convention. Multi-line comments are supported.
|
|
83
|
-
*/
|
|
84
|
-
function stripHtmlComments(source: string): string {
|
|
85
|
-
return source.replace(/<!--[\s\S]*?-->/g, "");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function splitByHeadings(source: string): {
|
|
89
|
-
title?: string;
|
|
90
|
-
sections: RawSection[];
|
|
91
|
-
} {
|
|
92
|
-
const lines = stripHtmlComments(source).split(/\r?\n/);
|
|
93
|
-
let title: string | undefined;
|
|
94
|
-
const sections: RawSection[] = [];
|
|
95
|
-
let current: RawSection | null = null;
|
|
96
|
-
|
|
97
|
-
// H1 detection — first non-empty line is `# Foo` or first `# Foo` before
|
|
98
|
-
// any H2. Only the first H1 is treated as title.
|
|
99
|
-
let titleConsumed = false;
|
|
100
|
-
|
|
101
|
-
for (const line of lines) {
|
|
102
|
-
const h1 = /^#\s+(.+?)\s*$/.exec(line);
|
|
103
|
-
const h2 = /^##\s+(.+?)\s*$/.exec(line);
|
|
104
|
-
if (h1 && !titleConsumed && current === null) {
|
|
105
|
-
title = h1[1].trim();
|
|
106
|
-
titleConsumed = true;
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
if (h2) {
|
|
110
|
-
if (current) sections.push(current);
|
|
111
|
-
current = { heading: h2[1].trim(), level: 2, body: "" };
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
if (current) {
|
|
115
|
-
current.body += (current.body ? "\n" : "") + line;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
if (current) sections.push(current);
|
|
119
|
-
return { title, sections };
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// ────────────────────────────────────────────────────────────────────
|
|
123
|
-
// Section extractors
|
|
124
|
-
// ────────────────────────────────────────────────────────────────────
|
|
125
|
-
|
|
126
|
-
const COLOR_VALUE_RX =
|
|
127
|
-
/(#[0-9a-fA-F]{3,8}|rgba?\([^)]+\)|hsla?\([^)]+\)|oklch\([^)]+\)|oklab\([^)]+\))/;
|
|
128
|
-
|
|
129
|
-
function extractColorTokens(body: string): ColorToken[] {
|
|
130
|
-
const tokens: ColorToken[] = [];
|
|
131
|
-
for (const line of body.split(/\r?\n/)) {
|
|
132
|
-
const trimmed = line.trim().replace(/^[-*+]\s*/, "").replace(/^\|\s*/, "").replace(/\s*\|.*$/, "");
|
|
133
|
-
if (!trimmed) continue;
|
|
134
|
-
const valueMatch = COLOR_VALUE_RX.exec(line);
|
|
135
|
-
// Try patterns:
|
|
136
|
-
// - `name — #hex — role`
|
|
137
|
-
// - `name: #hex (role)`
|
|
138
|
-
// - `**name** \`#hex\` — role`
|
|
139
|
-
// - `| name | #hex | role |` (markdown table row)
|
|
140
|
-
const stripped = line
|
|
141
|
-
.replace(/[`*_]/g, "")
|
|
142
|
-
.replace(/^[\s|>-]+/, "")
|
|
143
|
-
.replace(/\s*\|.*$/, "");
|
|
144
|
-
const sepMatch = /^([^:—–\-]+?)[\s]*[:—–\-][\s]*(.+)$/.exec(stripped);
|
|
145
|
-
let name: string | undefined;
|
|
146
|
-
let role: string | undefined;
|
|
147
|
-
if (sepMatch) {
|
|
148
|
-
name = sepMatch[1].trim();
|
|
149
|
-
const rest = sepMatch[2];
|
|
150
|
-
// role is whatever isn't the colour value
|
|
151
|
-
if (valueMatch) {
|
|
152
|
-
role = rest.replace(valueMatch[0], "").replace(/[\s—–\-:|]+/g, " ").trim() || undefined;
|
|
153
|
-
} else {
|
|
154
|
-
role = rest.trim() || undefined;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
if (!name && valueMatch) {
|
|
158
|
-
// Fallback — colour value present but no clear "name : value" split.
|
|
159
|
-
name = stripped.replace(valueMatch[0], "").trim() || valueMatch[0];
|
|
160
|
-
}
|
|
161
|
-
if (!name) continue;
|
|
162
|
-
if (name.length > 100) continue; // garbage line
|
|
163
|
-
// Filter out heading-like rows ("Color Palette", "Functional Roles", ...).
|
|
164
|
-
if (/palette|role/i.test(name) && !valueMatch) continue;
|
|
165
|
-
tokens.push({
|
|
166
|
-
name,
|
|
167
|
-
value: valueMatch ? valueMatch[0] : undefined,
|
|
168
|
-
role,
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
return tokens;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function extractTypographyTokens(body: string): TypographyToken[] {
|
|
175
|
-
const tokens: TypographyToken[] = [];
|
|
176
|
-
for (const line of body.split(/\r?\n/)) {
|
|
177
|
-
const trimmed = line.trim().replace(/^[-*+]\s*/, "");
|
|
178
|
-
if (!trimmed) continue;
|
|
179
|
-
if (trimmed.startsWith("#")) continue; // sub-headings
|
|
180
|
-
const stripped = trimmed.replace(/[`*_]/g, "");
|
|
181
|
-
const nameMatch = /^([^:—–]+?)[\s]*[:—–]/.exec(stripped);
|
|
182
|
-
const name = nameMatch ? nameMatch[1].trim() : stripped.split(/\s{2,}|—|–/)[0]?.trim();
|
|
183
|
-
if (!name) continue;
|
|
184
|
-
const fontFamily = /font[\s-]*family[:\s]+([^,;]+)/i.exec(stripped)?.[1]?.trim();
|
|
185
|
-
const weight = /weight[:\s]+([0-9]{3}|bold|semi-?bold|medium|regular|light|thin)/i.exec(stripped)?.[1]?.trim();
|
|
186
|
-
const size = /(\d+(?:\.\d+)?(?:px|rem|em|pt))/.exec(stripped)?.[1];
|
|
187
|
-
const lineHeight = /line[\s-]*height[:\s]+([0-9.]+)/i.exec(stripped)?.[1];
|
|
188
|
-
tokens.push({
|
|
189
|
-
name,
|
|
190
|
-
fontFamily,
|
|
191
|
-
weight,
|
|
192
|
-
size,
|
|
193
|
-
lineHeight,
|
|
194
|
-
usage: stripped !== name ? stripped : undefined,
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
return tokens;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function extractComponentTokens(body: string): ComponentToken[] {
|
|
201
|
-
// Components are typically organised under H3 sub-headings ("### Button",
|
|
202
|
-
// "### Card"). When present, treat each H3 as one component; otherwise
|
|
203
|
-
// fall back to bullet rows.
|
|
204
|
-
const tokens: ComponentToken[] = [];
|
|
205
|
-
const h3Rx = /^###\s+(.+?)\s*$/;
|
|
206
|
-
const lines = body.split(/\r?\n/);
|
|
207
|
-
|
|
208
|
-
let current: ComponentToken | null = null;
|
|
209
|
-
for (const line of lines) {
|
|
210
|
-
const h3 = h3Rx.exec(line);
|
|
211
|
-
if (h3) {
|
|
212
|
-
if (current) tokens.push(current);
|
|
213
|
-
current = { name: h3[1].trim(), variants: {}, notes: "" };
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
if (!current) continue;
|
|
217
|
-
// Variant detection — `variant: primary | secondary | ghost`
|
|
218
|
-
const variantMatch = /^[\s-]*(\w[\w\s-]*?)[\s]*:[\s]*(.+)$/.exec(line.trim());
|
|
219
|
-
if (variantMatch) {
|
|
220
|
-
const key = variantMatch[1].trim();
|
|
221
|
-
const valuesRaw = variantMatch[2].trim();
|
|
222
|
-
if (/[|,]/.test(valuesRaw) && valuesRaw.length < 200) {
|
|
223
|
-
const values = valuesRaw
|
|
224
|
-
.split(/[|,]/)
|
|
225
|
-
.map((v) => v.trim().replace(/[`*_"']/g, ""))
|
|
226
|
-
.filter(Boolean);
|
|
227
|
-
if (values.length >= 2) {
|
|
228
|
-
current.variants[key] = values;
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
if (line.trim()) {
|
|
234
|
-
current.notes = (current.notes ?? "") + (current.notes ? "\n" : "") + line;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
if (current) tokens.push(current);
|
|
238
|
-
|
|
239
|
-
if (tokens.length === 0) {
|
|
240
|
-
// Fallback — flat bullet list of components.
|
|
241
|
-
for (const line of lines) {
|
|
242
|
-
const m = /^[\s-*+]+([A-Z][\w\s]+?)(?::|—|–|$)/.exec(line);
|
|
243
|
-
if (m) tokens.push({ name: m[1].trim(), variants: {}, notes: undefined });
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
return tokens;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function extractSpacingTokens(body: string): SpacingToken[] {
|
|
250
|
-
const tokens: SpacingToken[] = [];
|
|
251
|
-
for (const line of body.split(/\r?\n/)) {
|
|
252
|
-
const stripped = line.trim().replace(/^[-*+]\s*/, "").replace(/[`*_]/g, "");
|
|
253
|
-
if (!stripped) continue;
|
|
254
|
-
const m = /^([\w-]+)[\s:—–-]+(\d+(?:\.\d+)?(?:px|rem|em|%)?)/.exec(stripped);
|
|
255
|
-
if (m) tokens.push({ name: m[1], value: m[2] });
|
|
256
|
-
}
|
|
257
|
-
return tokens;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function extractShadowTokens(body: string): ShadowToken[] {
|
|
261
|
-
const tokens: ShadowToken[] = [];
|
|
262
|
-
// Shadows are typically `name: <css value>` rows. CSS values contain
|
|
263
|
-
// commas / parens / `0 1px 2px rgba(...)` so we don't attempt to fully
|
|
264
|
-
// parse them — capture the whole right-hand side verbatim.
|
|
265
|
-
for (const line of body.split(/\r?\n/)) {
|
|
266
|
-
const stripped = line.trim().replace(/^[-*+]\s*/, "").replace(/[`*_]/g, "");
|
|
267
|
-
if (!stripped) continue;
|
|
268
|
-
const m = /^([\w-]+)\s*[:—–]\s*(.+)$/.exec(stripped);
|
|
269
|
-
if (m && (m[2].includes("px") || m[2].includes("rgba"))) {
|
|
270
|
-
tokens.push({ name: m[1], value: m[2].trim() });
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
return tokens;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function extractDoDontRules(body: string): DoDontRule[] {
|
|
277
|
-
const rules: DoDontRule[] = [];
|
|
278
|
-
let mode: "do" | "dont" | null = null;
|
|
279
|
-
for (const line of body.split(/\r?\n/)) {
|
|
280
|
-
const stripped = line.trim();
|
|
281
|
-
if (!stripped) continue;
|
|
282
|
-
if (/^#{2,}\s+do\b|^\*\*do\*\*|^do['s]*[:\s]/i.test(stripped) && !/don.?t/i.test(stripped)) {
|
|
283
|
-
mode = "do";
|
|
284
|
-
continue;
|
|
285
|
-
}
|
|
286
|
-
if (/^#{2,}\s+don.?t|^\*\*don.?t\*\*|^don.?t[s]?[:\s]/i.test(stripped)) {
|
|
287
|
-
mode = "dont";
|
|
288
|
-
continue;
|
|
289
|
-
}
|
|
290
|
-
const bullet = /^[-*+]\s+(.+)$/.exec(stripped);
|
|
291
|
-
if (bullet && mode) {
|
|
292
|
-
// Preserve backticks — they mark identifier tokens
|
|
293
|
-
// (`btn-hard`, `shadow-hard`) that downstream consumers
|
|
294
|
-
// (Guard `autoFromDesignMd`) extract from the rule text.
|
|
295
|
-
rules.push({ kind: mode, text: bullet[1].replace(/[*_]/g, "").trim() });
|
|
296
|
-
} else if (bullet) {
|
|
297
|
-
// ✅ / ❌ inline markers.
|
|
298
|
-
const text = bullet[1];
|
|
299
|
-
if (text.startsWith("✅") || /^do\b/i.test(text)) {
|
|
300
|
-
rules.push({ kind: "do", text: text.replace(/^[✅do:\s]+/i, "").trim() });
|
|
301
|
-
} else if (text.startsWith("❌") || /^don.?t\b/i.test(text)) {
|
|
302
|
-
rules.push({ kind: "dont", text: text.replace(/^[❌don'?t:\s]+/i, "").trim() });
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
return rules;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function extractBreakpoints(body: string): ResponsiveBreakpoint[] {
|
|
310
|
-
const bps: ResponsiveBreakpoint[] = [];
|
|
311
|
-
for (const line of body.split(/\r?\n/)) {
|
|
312
|
-
const stripped = line.trim().replace(/^[-*+]\s*/, "").replace(/[`*_]/g, "");
|
|
313
|
-
if (!stripped) continue;
|
|
314
|
-
const m = /^([\w-]+)[\s:—–-]+(\d+(?:px|rem|em)?)\b(.*)$/.exec(stripped);
|
|
315
|
-
if (m) {
|
|
316
|
-
bps.push({
|
|
317
|
-
name: m[1],
|
|
318
|
-
value: m[2],
|
|
319
|
-
notes: m[3].replace(/^[\s—–-]+/, "").trim() || undefined,
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
return bps;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
function extractAgentPrompts(body: string): AgentPrompt[] {
|
|
327
|
-
// Group by H3; everything else collapses into a single "default" prompt.
|
|
328
|
-
const prompts: AgentPrompt[] = [];
|
|
329
|
-
const lines = body.split(/\r?\n/);
|
|
330
|
-
const h3Rx = /^###\s+(.+?)\s*$/;
|
|
331
|
-
let current: AgentPrompt | null = null;
|
|
332
|
-
let buf: string[] = [];
|
|
333
|
-
for (const line of lines) {
|
|
334
|
-
const h3 = h3Rx.exec(line);
|
|
335
|
-
if (h3) {
|
|
336
|
-
if (current) {
|
|
337
|
-
current.body = buf.join("\n").trim();
|
|
338
|
-
prompts.push(current);
|
|
339
|
-
}
|
|
340
|
-
current = { title: h3[1].trim(), body: "" };
|
|
341
|
-
buf = [];
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
|
-
buf.push(line);
|
|
345
|
-
}
|
|
346
|
-
if (current) {
|
|
347
|
-
current.body = buf.join("\n").trim();
|
|
348
|
-
prompts.push(current);
|
|
349
|
-
} else if (buf.join("").trim()) {
|
|
350
|
-
prompts.push({ title: "default", body: buf.join("\n").trim() });
|
|
351
|
-
}
|
|
352
|
-
return prompts;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// ────────────────────────────────────────────────────────────────────
|
|
356
|
-
// Public API
|
|
357
|
-
// ────────────────────────────────────────────────────────────────────
|
|
358
|
-
|
|
359
|
-
function emptySections(): DesignSpec["sections"] {
|
|
360
|
-
return {
|
|
361
|
-
theme: { id: "theme", present: false, rawBody: "" },
|
|
362
|
-
"color-palette": { id: "color-palette", present: false, rawBody: "", tokens: [] },
|
|
363
|
-
typography: { id: "typography", present: false, rawBody: "", tokens: [] },
|
|
364
|
-
components: { id: "components", present: false, rawBody: "", tokens: [] },
|
|
365
|
-
layout: { id: "layout", present: false, rawBody: "", tokens: [] },
|
|
366
|
-
shadows: { id: "shadows", present: false, rawBody: "", tokens: [] },
|
|
367
|
-
"dos-donts": { id: "dos-donts", present: false, rawBody: "", rules: [] },
|
|
368
|
-
responsive: { id: "responsive", present: false, rawBody: "", breakpoints: [] },
|
|
369
|
-
"agent-prompts": { id: "agent-prompts", present: false, rawBody: "", prompts: [] },
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/** Parse a DESIGN.md source string into the structured spec. Never throws. */
|
|
374
|
-
export function parseDesignMd(source: string): DesignSpec {
|
|
375
|
-
const { title, sections: rawSections } = splitByHeadings(source);
|
|
376
|
-
const result: DesignSpec = {
|
|
377
|
-
source,
|
|
378
|
-
title,
|
|
379
|
-
sections: emptySections(),
|
|
380
|
-
extraSections: [],
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
for (const raw of rawSections) {
|
|
384
|
-
const id = resolveSectionId(raw.heading);
|
|
385
|
-
if (!id) {
|
|
386
|
-
result.extraSections.push({ heading: raw.heading, body: raw.body });
|
|
387
|
-
continue;
|
|
388
|
-
}
|
|
389
|
-
const body = raw.body.trim();
|
|
390
|
-
switch (id) {
|
|
391
|
-
case "theme": {
|
|
392
|
-
const summary = body.split(/\n\s*\n/)[0]?.trim() || undefined;
|
|
393
|
-
result.sections.theme = {
|
|
394
|
-
id,
|
|
395
|
-
present: true,
|
|
396
|
-
headingText: raw.heading,
|
|
397
|
-
rawBody: body,
|
|
398
|
-
summary,
|
|
399
|
-
};
|
|
400
|
-
break;
|
|
401
|
-
}
|
|
402
|
-
case "color-palette":
|
|
403
|
-
result.sections["color-palette"] = {
|
|
404
|
-
id,
|
|
405
|
-
present: true,
|
|
406
|
-
headingText: raw.heading,
|
|
407
|
-
rawBody: body,
|
|
408
|
-
tokens: extractColorTokens(body),
|
|
409
|
-
};
|
|
410
|
-
break;
|
|
411
|
-
case "typography":
|
|
412
|
-
result.sections.typography = {
|
|
413
|
-
id,
|
|
414
|
-
present: true,
|
|
415
|
-
headingText: raw.heading,
|
|
416
|
-
rawBody: body,
|
|
417
|
-
tokens: extractTypographyTokens(body),
|
|
418
|
-
};
|
|
419
|
-
break;
|
|
420
|
-
case "components":
|
|
421
|
-
result.sections.components = {
|
|
422
|
-
id,
|
|
423
|
-
present: true,
|
|
424
|
-
headingText: raw.heading,
|
|
425
|
-
rawBody: body,
|
|
426
|
-
tokens: extractComponentTokens(body),
|
|
427
|
-
};
|
|
428
|
-
break;
|
|
429
|
-
case "layout":
|
|
430
|
-
result.sections.layout = {
|
|
431
|
-
id,
|
|
432
|
-
present: true,
|
|
433
|
-
headingText: raw.heading,
|
|
434
|
-
rawBody: body,
|
|
435
|
-
tokens: extractSpacingTokens(body),
|
|
436
|
-
};
|
|
437
|
-
break;
|
|
438
|
-
case "shadows":
|
|
439
|
-
result.sections.shadows = {
|
|
440
|
-
id,
|
|
441
|
-
present: true,
|
|
442
|
-
headingText: raw.heading,
|
|
443
|
-
rawBody: body,
|
|
444
|
-
tokens: extractShadowTokens(body),
|
|
445
|
-
};
|
|
446
|
-
break;
|
|
447
|
-
case "dos-donts":
|
|
448
|
-
result.sections["dos-donts"] = {
|
|
449
|
-
id,
|
|
450
|
-
present: true,
|
|
451
|
-
headingText: raw.heading,
|
|
452
|
-
rawBody: body,
|
|
453
|
-
rules: extractDoDontRules(body),
|
|
454
|
-
};
|
|
455
|
-
break;
|
|
456
|
-
case "responsive":
|
|
457
|
-
result.sections.responsive = {
|
|
458
|
-
id,
|
|
459
|
-
present: true,
|
|
460
|
-
headingText: raw.heading,
|
|
461
|
-
rawBody: body,
|
|
462
|
-
breakpoints: extractBreakpoints(body),
|
|
463
|
-
};
|
|
464
|
-
break;
|
|
465
|
-
case "agent-prompts":
|
|
466
|
-
result.sections["agent-prompts"] = {
|
|
467
|
-
id,
|
|
468
|
-
present: true,
|
|
469
|
-
headingText: raw.heading,
|
|
470
|
-
rawBody: body,
|
|
471
|
-
prompts: extractAgentPrompts(body),
|
|
472
|
-
};
|
|
473
|
-
break;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
return result;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// ────────────────────────────────────────────────────────────────────
|
|
480
|
-
// Validation
|
|
481
|
-
// ────────────────────────────────────────────────────────────────────
|
|
482
|
-
|
|
483
|
-
/**
|
|
484
|
-
* Surface gaps in a parsed DESIGN.md. The validator never returns
|
|
485
|
-
* errors that block builds — Mandu's enforcement layer (Guard) is the
|
|
486
|
-
* gate. This function is the diagnostic that tells the user "your
|
|
487
|
-
* DESIGN.md is missing colour tokens" so they can fill it in.
|
|
488
|
-
*/
|
|
489
|
-
export function validateDesignSpec(spec: DesignSpec): ValidationResult {
|
|
490
|
-
const issues: ValidationIssue[] = [];
|
|
491
|
-
for (const id of DESIGN_SECTION_IDS) {
|
|
492
|
-
const section = spec.sections[id] as AnyDesignSection;
|
|
493
|
-
if (!section.present) {
|
|
494
|
-
issues.push({
|
|
495
|
-
kind: "missing",
|
|
496
|
-
section: id,
|
|
497
|
-
message: `Section "${id}" not found. Add a "## ${humanizeSectionId(id)}" heading.`,
|
|
498
|
-
});
|
|
499
|
-
continue;
|
|
500
|
-
}
|
|
501
|
-
const empty = isSectionEmpty(section);
|
|
502
|
-
if (empty) {
|
|
503
|
-
issues.push({
|
|
504
|
-
kind: "empty",
|
|
505
|
-
section: id,
|
|
506
|
-
message: `Section "${id}" is present but has no structured tokens.`,
|
|
507
|
-
});
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
return { ok: issues.length === 0, issues };
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
function isSectionEmpty(section: AnyDesignSection): boolean {
|
|
514
|
-
switch (section.id) {
|
|
515
|
-
case "theme":
|
|
516
|
-
return !section.summary || section.summary.length === 0;
|
|
517
|
-
case "color-palette":
|
|
518
|
-
case "typography":
|
|
519
|
-
case "components":
|
|
520
|
-
case "layout":
|
|
521
|
-
case "shadows":
|
|
522
|
-
return section.tokens.length === 0;
|
|
523
|
-
case "dos-donts":
|
|
524
|
-
return section.rules.length === 0;
|
|
525
|
-
case "responsive":
|
|
526
|
-
return section.breakpoints.length === 0;
|
|
527
|
-
case "agent-prompts":
|
|
528
|
-
return section.prompts.length === 0;
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
function humanizeSectionId(id: DesignSectionId): string {
|
|
533
|
-
switch (id) {
|
|
534
|
-
case "theme":
|
|
535
|
-
return "Visual Theme & Philosophy";
|
|
536
|
-
case "color-palette":
|
|
537
|
-
return "Color Palette";
|
|
538
|
-
case "typography":
|
|
539
|
-
return "Typography";
|
|
540
|
-
case "components":
|
|
541
|
-
return "Components";
|
|
542
|
-
case "layout":
|
|
543
|
-
return "Layout";
|
|
544
|
-
case "shadows":
|
|
545
|
-
return "Depth & Elevation";
|
|
546
|
-
case "dos-donts":
|
|
547
|
-
return "Do's & Don'ts";
|
|
548
|
-
case "responsive":
|
|
549
|
-
return "Responsive";
|
|
550
|
-
case "agent-prompts":
|
|
551
|
-
return "Agent Prompts";
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
export { humanizeSectionId };
|
|
1
|
+
/**
|
|
2
|
+
* DESIGN.md parser — the 9-section heading walker.
|
|
3
|
+
*
|
|
4
|
+
* Strategy:
|
|
5
|
+
* 1. Optional H1 title at the top of the file.
|
|
6
|
+
* 2. Scan H2 headings. Match each against the 9 canonical sections
|
|
7
|
+
* via a fuzzy-but-bounded resolver (lowercase, strip `&` / `'`,
|
|
8
|
+
* collapse whitespace). Unrecognized headings land in
|
|
9
|
+
* `extraSections` so the file round-trips cleanly.
|
|
10
|
+
* 3. For each matched section, parse the body into structured
|
|
11
|
+
* tokens with a section-specific extractor. Extractors are
|
|
12
|
+
* forgiving — a malformed row is skipped, never fatal.
|
|
13
|
+
*
|
|
14
|
+
* Parsing must NEVER throw on real-world DESIGN.md files. The 69
|
|
15
|
+
* brand entries in awesome-design-md vary wildly in formatting; the
|
|
16
|
+
* parser is the contract that smooths them out for the rest of Mandu.
|
|
17
|
+
*
|
|
18
|
+
* @module core/design/parser
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type {
|
|
22
|
+
AgentPrompt,
|
|
23
|
+
AnyDesignSection,
|
|
24
|
+
ColorToken,
|
|
25
|
+
ComponentToken,
|
|
26
|
+
DesignSectionId,
|
|
27
|
+
DesignSpec,
|
|
28
|
+
DoDontRule,
|
|
29
|
+
ResponsiveBreakpoint,
|
|
30
|
+
ShadowToken,
|
|
31
|
+
SpacingToken,
|
|
32
|
+
TypographyToken,
|
|
33
|
+
ValidationIssue,
|
|
34
|
+
ValidationResult,
|
|
35
|
+
} from "./types";
|
|
36
|
+
|
|
37
|
+
import { DESIGN_SECTION_IDS } from "./types";
|
|
38
|
+
|
|
39
|
+
// ────────────────────────────────────────────────────────────────────
|
|
40
|
+
// Heading → section-id resolution
|
|
41
|
+
// ────────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Match heading text against the 9 canonical section ids. Returns the
|
|
45
|
+
* id when the heading "looks like" the section by keyword, else null.
|
|
46
|
+
* The keyword sets are intentionally redundant to absorb the wording
|
|
47
|
+
* differences across the awesome-design-md catalog.
|
|
48
|
+
*/
|
|
49
|
+
function resolveSectionId(heading: string): DesignSectionId | null {
|
|
50
|
+
const h = heading
|
|
51
|
+
.toLowerCase()
|
|
52
|
+
.replace(/[&'`*_~]/g, "")
|
|
53
|
+
.replace(/\s+/g, " ")
|
|
54
|
+
.trim();
|
|
55
|
+
if (/^visual|^theme|^philosophy|^vibe|^aesthetic/.test(h)) return "theme";
|
|
56
|
+
if (/color|palette/.test(h)) return "color-palette";
|
|
57
|
+
if (/typograph|typeface|font|type scale/.test(h)) return "typography";
|
|
58
|
+
if (/component|button|card|input/.test(h)) return "components";
|
|
59
|
+
if (/layout|spacing|grid|whitespace/.test(h)) return "layout";
|
|
60
|
+
if (/shadow|elevation|depth/.test(h)) return "shadows";
|
|
61
|
+
if (/do.{0,3}dont|guideline|principle|rule/.test(h)) return "dos-donts";
|
|
62
|
+
if (/responsive|breakpoint|mobile|tablet|desktop/.test(h)) return "responsive";
|
|
63
|
+
if (/agent|prompt|llm|ai/.test(h)) return "agent-prompts";
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ────────────────────────────────────────────────────────────────────
|
|
68
|
+
// H2 splitter
|
|
69
|
+
// ────────────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
interface RawSection {
|
|
72
|
+
heading: string;
|
|
73
|
+
/** `1` or `2` — H1 or H2. */
|
|
74
|
+
level: number;
|
|
75
|
+
body: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Strip HTML comments (`<!-- … -->`) before structural parsing. The
|
|
80
|
+
* empty-skeleton template uses HTML comments to show example tokens
|
|
81
|
+
* without having them counted as real tokens; the parser must respect
|
|
82
|
+
* that convention. Multi-line comments are supported.
|
|
83
|
+
*/
|
|
84
|
+
function stripHtmlComments(source: string): string {
|
|
85
|
+
return source.replace(/<!--[\s\S]*?-->/g, "");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function splitByHeadings(source: string): {
|
|
89
|
+
title?: string;
|
|
90
|
+
sections: RawSection[];
|
|
91
|
+
} {
|
|
92
|
+
const lines = stripHtmlComments(source).split(/\r?\n/);
|
|
93
|
+
let title: string | undefined;
|
|
94
|
+
const sections: RawSection[] = [];
|
|
95
|
+
let current: RawSection | null = null;
|
|
96
|
+
|
|
97
|
+
// H1 detection — first non-empty line is `# Foo` or first `# Foo` before
|
|
98
|
+
// any H2. Only the first H1 is treated as title.
|
|
99
|
+
let titleConsumed = false;
|
|
100
|
+
|
|
101
|
+
for (const line of lines) {
|
|
102
|
+
const h1 = /^#\s+(.+?)\s*$/.exec(line);
|
|
103
|
+
const h2 = /^##\s+(.+?)\s*$/.exec(line);
|
|
104
|
+
if (h1 && !titleConsumed && current === null) {
|
|
105
|
+
title = h1[1].trim();
|
|
106
|
+
titleConsumed = true;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (h2) {
|
|
110
|
+
if (current) sections.push(current);
|
|
111
|
+
current = { heading: h2[1].trim(), level: 2, body: "" };
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (current) {
|
|
115
|
+
current.body += (current.body ? "\n" : "") + line;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (current) sections.push(current);
|
|
119
|
+
return { title, sections };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ────────────────────────────────────────────────────────────────────
|
|
123
|
+
// Section extractors
|
|
124
|
+
// ────────────────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
const COLOR_VALUE_RX =
|
|
127
|
+
/(#[0-9a-fA-F]{3,8}|rgba?\([^)]+\)|hsla?\([^)]+\)|oklch\([^)]+\)|oklab\([^)]+\))/;
|
|
128
|
+
|
|
129
|
+
function extractColorTokens(body: string): ColorToken[] {
|
|
130
|
+
const tokens: ColorToken[] = [];
|
|
131
|
+
for (const line of body.split(/\r?\n/)) {
|
|
132
|
+
const trimmed = line.trim().replace(/^[-*+]\s*/, "").replace(/^\|\s*/, "").replace(/\s*\|.*$/, "");
|
|
133
|
+
if (!trimmed) continue;
|
|
134
|
+
const valueMatch = COLOR_VALUE_RX.exec(line);
|
|
135
|
+
// Try patterns:
|
|
136
|
+
// - `name — #hex — role`
|
|
137
|
+
// - `name: #hex (role)`
|
|
138
|
+
// - `**name** \`#hex\` — role`
|
|
139
|
+
// - `| name | #hex | role |` (markdown table row)
|
|
140
|
+
const stripped = line
|
|
141
|
+
.replace(/[`*_]/g, "")
|
|
142
|
+
.replace(/^[\s|>-]+/, "")
|
|
143
|
+
.replace(/\s*\|.*$/, "");
|
|
144
|
+
const sepMatch = /^([^:—–\-]+?)[\s]*[:—–\-][\s]*(.+)$/.exec(stripped);
|
|
145
|
+
let name: string | undefined;
|
|
146
|
+
let role: string | undefined;
|
|
147
|
+
if (sepMatch) {
|
|
148
|
+
name = sepMatch[1].trim();
|
|
149
|
+
const rest = sepMatch[2];
|
|
150
|
+
// role is whatever isn't the colour value
|
|
151
|
+
if (valueMatch) {
|
|
152
|
+
role = rest.replace(valueMatch[0], "").replace(/[\s—–\-:|]+/g, " ").trim() || undefined;
|
|
153
|
+
} else {
|
|
154
|
+
role = rest.trim() || undefined;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (!name && valueMatch) {
|
|
158
|
+
// Fallback — colour value present but no clear "name : value" split.
|
|
159
|
+
name = stripped.replace(valueMatch[0], "").trim() || valueMatch[0];
|
|
160
|
+
}
|
|
161
|
+
if (!name) continue;
|
|
162
|
+
if (name.length > 100) continue; // garbage line
|
|
163
|
+
// Filter out heading-like rows ("Color Palette", "Functional Roles", ...).
|
|
164
|
+
if (/palette|role/i.test(name) && !valueMatch) continue;
|
|
165
|
+
tokens.push({
|
|
166
|
+
name,
|
|
167
|
+
value: valueMatch ? valueMatch[0] : undefined,
|
|
168
|
+
role,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return tokens;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function extractTypographyTokens(body: string): TypographyToken[] {
|
|
175
|
+
const tokens: TypographyToken[] = [];
|
|
176
|
+
for (const line of body.split(/\r?\n/)) {
|
|
177
|
+
const trimmed = line.trim().replace(/^[-*+]\s*/, "");
|
|
178
|
+
if (!trimmed) continue;
|
|
179
|
+
if (trimmed.startsWith("#")) continue; // sub-headings
|
|
180
|
+
const stripped = trimmed.replace(/[`*_]/g, "");
|
|
181
|
+
const nameMatch = /^([^:—–]+?)[\s]*[:—–]/.exec(stripped);
|
|
182
|
+
const name = nameMatch ? nameMatch[1].trim() : stripped.split(/\s{2,}|—|–/)[0]?.trim();
|
|
183
|
+
if (!name) continue;
|
|
184
|
+
const fontFamily = /font[\s-]*family[:\s]+([^,;]+)/i.exec(stripped)?.[1]?.trim();
|
|
185
|
+
const weight = /weight[:\s]+([0-9]{3}|bold|semi-?bold|medium|regular|light|thin)/i.exec(stripped)?.[1]?.trim();
|
|
186
|
+
const size = /(\d+(?:\.\d+)?(?:px|rem|em|pt))/.exec(stripped)?.[1];
|
|
187
|
+
const lineHeight = /line[\s-]*height[:\s]+([0-9.]+)/i.exec(stripped)?.[1];
|
|
188
|
+
tokens.push({
|
|
189
|
+
name,
|
|
190
|
+
fontFamily,
|
|
191
|
+
weight,
|
|
192
|
+
size,
|
|
193
|
+
lineHeight,
|
|
194
|
+
usage: stripped !== name ? stripped : undefined,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return tokens;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function extractComponentTokens(body: string): ComponentToken[] {
|
|
201
|
+
// Components are typically organised under H3 sub-headings ("### Button",
|
|
202
|
+
// "### Card"). When present, treat each H3 as one component; otherwise
|
|
203
|
+
// fall back to bullet rows.
|
|
204
|
+
const tokens: ComponentToken[] = [];
|
|
205
|
+
const h3Rx = /^###\s+(.+?)\s*$/;
|
|
206
|
+
const lines = body.split(/\r?\n/);
|
|
207
|
+
|
|
208
|
+
let current: ComponentToken | null = null;
|
|
209
|
+
for (const line of lines) {
|
|
210
|
+
const h3 = h3Rx.exec(line);
|
|
211
|
+
if (h3) {
|
|
212
|
+
if (current) tokens.push(current);
|
|
213
|
+
current = { name: h3[1].trim(), variants: {}, notes: "" };
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (!current) continue;
|
|
217
|
+
// Variant detection — `variant: primary | secondary | ghost`
|
|
218
|
+
const variantMatch = /^[\s-]*(\w[\w\s-]*?)[\s]*:[\s]*(.+)$/.exec(line.trim());
|
|
219
|
+
if (variantMatch) {
|
|
220
|
+
const key = variantMatch[1].trim();
|
|
221
|
+
const valuesRaw = variantMatch[2].trim();
|
|
222
|
+
if (/[|,]/.test(valuesRaw) && valuesRaw.length < 200) {
|
|
223
|
+
const values = valuesRaw
|
|
224
|
+
.split(/[|,]/)
|
|
225
|
+
.map((v) => v.trim().replace(/[`*_"']/g, ""))
|
|
226
|
+
.filter(Boolean);
|
|
227
|
+
if (values.length >= 2) {
|
|
228
|
+
current.variants[key] = values;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (line.trim()) {
|
|
234
|
+
current.notes = (current.notes ?? "") + (current.notes ? "\n" : "") + line;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (current) tokens.push(current);
|
|
238
|
+
|
|
239
|
+
if (tokens.length === 0) {
|
|
240
|
+
// Fallback — flat bullet list of components.
|
|
241
|
+
for (const line of lines) {
|
|
242
|
+
const m = /^[\s-*+]+([A-Z][\w\s]+?)(?::|—|–|$)/.exec(line);
|
|
243
|
+
if (m) tokens.push({ name: m[1].trim(), variants: {}, notes: undefined });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return tokens;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function extractSpacingTokens(body: string): SpacingToken[] {
|
|
250
|
+
const tokens: SpacingToken[] = [];
|
|
251
|
+
for (const line of body.split(/\r?\n/)) {
|
|
252
|
+
const stripped = line.trim().replace(/^[-*+]\s*/, "").replace(/[`*_]/g, "");
|
|
253
|
+
if (!stripped) continue;
|
|
254
|
+
const m = /^([\w-]+)[\s:—–-]+(\d+(?:\.\d+)?(?:px|rem|em|%)?)/.exec(stripped);
|
|
255
|
+
if (m) tokens.push({ name: m[1], value: m[2] });
|
|
256
|
+
}
|
|
257
|
+
return tokens;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function extractShadowTokens(body: string): ShadowToken[] {
|
|
261
|
+
const tokens: ShadowToken[] = [];
|
|
262
|
+
// Shadows are typically `name: <css value>` rows. CSS values contain
|
|
263
|
+
// commas / parens / `0 1px 2px rgba(...)` so we don't attempt to fully
|
|
264
|
+
// parse them — capture the whole right-hand side verbatim.
|
|
265
|
+
for (const line of body.split(/\r?\n/)) {
|
|
266
|
+
const stripped = line.trim().replace(/^[-*+]\s*/, "").replace(/[`*_]/g, "");
|
|
267
|
+
if (!stripped) continue;
|
|
268
|
+
const m = /^([\w-]+)\s*[:—–]\s*(.+)$/.exec(stripped);
|
|
269
|
+
if (m && (m[2].includes("px") || m[2].includes("rgba"))) {
|
|
270
|
+
tokens.push({ name: m[1], value: m[2].trim() });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return tokens;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function extractDoDontRules(body: string): DoDontRule[] {
|
|
277
|
+
const rules: DoDontRule[] = [];
|
|
278
|
+
let mode: "do" | "dont" | null = null;
|
|
279
|
+
for (const line of body.split(/\r?\n/)) {
|
|
280
|
+
const stripped = line.trim();
|
|
281
|
+
if (!stripped) continue;
|
|
282
|
+
if (/^#{2,}\s+do\b|^\*\*do\*\*|^do['s]*[:\s]/i.test(stripped) && !/don.?t/i.test(stripped)) {
|
|
283
|
+
mode = "do";
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (/^#{2,}\s+don.?t|^\*\*don.?t\*\*|^don.?t[s]?[:\s]/i.test(stripped)) {
|
|
287
|
+
mode = "dont";
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const bullet = /^[-*+]\s+(.+)$/.exec(stripped);
|
|
291
|
+
if (bullet && mode) {
|
|
292
|
+
// Preserve backticks — they mark identifier tokens
|
|
293
|
+
// (`btn-hard`, `shadow-hard`) that downstream consumers
|
|
294
|
+
// (Guard `autoFromDesignMd`) extract from the rule text.
|
|
295
|
+
rules.push({ kind: mode, text: bullet[1].replace(/[*_]/g, "").trim() });
|
|
296
|
+
} else if (bullet) {
|
|
297
|
+
// ✅ / ❌ inline markers.
|
|
298
|
+
const text = bullet[1];
|
|
299
|
+
if (text.startsWith("✅") || /^do\b/i.test(text)) {
|
|
300
|
+
rules.push({ kind: "do", text: text.replace(/^[✅do:\s]+/i, "").trim() });
|
|
301
|
+
} else if (text.startsWith("❌") || /^don.?t\b/i.test(text)) {
|
|
302
|
+
rules.push({ kind: "dont", text: text.replace(/^[❌don'?t:\s]+/i, "").trim() });
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return rules;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function extractBreakpoints(body: string): ResponsiveBreakpoint[] {
|
|
310
|
+
const bps: ResponsiveBreakpoint[] = [];
|
|
311
|
+
for (const line of body.split(/\r?\n/)) {
|
|
312
|
+
const stripped = line.trim().replace(/^[-*+]\s*/, "").replace(/[`*_]/g, "");
|
|
313
|
+
if (!stripped) continue;
|
|
314
|
+
const m = /^([\w-]+)[\s:—–-]+(\d+(?:px|rem|em)?)\b(.*)$/.exec(stripped);
|
|
315
|
+
if (m) {
|
|
316
|
+
bps.push({
|
|
317
|
+
name: m[1],
|
|
318
|
+
value: m[2],
|
|
319
|
+
notes: m[3].replace(/^[\s—–-]+/, "").trim() || undefined,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return bps;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function extractAgentPrompts(body: string): AgentPrompt[] {
|
|
327
|
+
// Group by H3; everything else collapses into a single "default" prompt.
|
|
328
|
+
const prompts: AgentPrompt[] = [];
|
|
329
|
+
const lines = body.split(/\r?\n/);
|
|
330
|
+
const h3Rx = /^###\s+(.+?)\s*$/;
|
|
331
|
+
let current: AgentPrompt | null = null;
|
|
332
|
+
let buf: string[] = [];
|
|
333
|
+
for (const line of lines) {
|
|
334
|
+
const h3 = h3Rx.exec(line);
|
|
335
|
+
if (h3) {
|
|
336
|
+
if (current) {
|
|
337
|
+
current.body = buf.join("\n").trim();
|
|
338
|
+
prompts.push(current);
|
|
339
|
+
}
|
|
340
|
+
current = { title: h3[1].trim(), body: "" };
|
|
341
|
+
buf = [];
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
buf.push(line);
|
|
345
|
+
}
|
|
346
|
+
if (current) {
|
|
347
|
+
current.body = buf.join("\n").trim();
|
|
348
|
+
prompts.push(current);
|
|
349
|
+
} else if (buf.join("").trim()) {
|
|
350
|
+
prompts.push({ title: "default", body: buf.join("\n").trim() });
|
|
351
|
+
}
|
|
352
|
+
return prompts;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ────────────────────────────────────────────────────────────────────
|
|
356
|
+
// Public API
|
|
357
|
+
// ────────────────────────────────────────────────────────────────────
|
|
358
|
+
|
|
359
|
+
function emptySections(): DesignSpec["sections"] {
|
|
360
|
+
return {
|
|
361
|
+
theme: { id: "theme", present: false, rawBody: "" },
|
|
362
|
+
"color-palette": { id: "color-palette", present: false, rawBody: "", tokens: [] },
|
|
363
|
+
typography: { id: "typography", present: false, rawBody: "", tokens: [] },
|
|
364
|
+
components: { id: "components", present: false, rawBody: "", tokens: [] },
|
|
365
|
+
layout: { id: "layout", present: false, rawBody: "", tokens: [] },
|
|
366
|
+
shadows: { id: "shadows", present: false, rawBody: "", tokens: [] },
|
|
367
|
+
"dos-donts": { id: "dos-donts", present: false, rawBody: "", rules: [] },
|
|
368
|
+
responsive: { id: "responsive", present: false, rawBody: "", breakpoints: [] },
|
|
369
|
+
"agent-prompts": { id: "agent-prompts", present: false, rawBody: "", prompts: [] },
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Parse a DESIGN.md source string into the structured spec. Never throws. */
|
|
374
|
+
export function parseDesignMd(source: string): DesignSpec {
|
|
375
|
+
const { title, sections: rawSections } = splitByHeadings(source);
|
|
376
|
+
const result: DesignSpec = {
|
|
377
|
+
source,
|
|
378
|
+
title,
|
|
379
|
+
sections: emptySections(),
|
|
380
|
+
extraSections: [],
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
for (const raw of rawSections) {
|
|
384
|
+
const id = resolveSectionId(raw.heading);
|
|
385
|
+
if (!id) {
|
|
386
|
+
result.extraSections.push({ heading: raw.heading, body: raw.body });
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const body = raw.body.trim();
|
|
390
|
+
switch (id) {
|
|
391
|
+
case "theme": {
|
|
392
|
+
const summary = body.split(/\n\s*\n/)[0]?.trim() || undefined;
|
|
393
|
+
result.sections.theme = {
|
|
394
|
+
id,
|
|
395
|
+
present: true,
|
|
396
|
+
headingText: raw.heading,
|
|
397
|
+
rawBody: body,
|
|
398
|
+
summary,
|
|
399
|
+
};
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
case "color-palette":
|
|
403
|
+
result.sections["color-palette"] = {
|
|
404
|
+
id,
|
|
405
|
+
present: true,
|
|
406
|
+
headingText: raw.heading,
|
|
407
|
+
rawBody: body,
|
|
408
|
+
tokens: extractColorTokens(body),
|
|
409
|
+
};
|
|
410
|
+
break;
|
|
411
|
+
case "typography":
|
|
412
|
+
result.sections.typography = {
|
|
413
|
+
id,
|
|
414
|
+
present: true,
|
|
415
|
+
headingText: raw.heading,
|
|
416
|
+
rawBody: body,
|
|
417
|
+
tokens: extractTypographyTokens(body),
|
|
418
|
+
};
|
|
419
|
+
break;
|
|
420
|
+
case "components":
|
|
421
|
+
result.sections.components = {
|
|
422
|
+
id,
|
|
423
|
+
present: true,
|
|
424
|
+
headingText: raw.heading,
|
|
425
|
+
rawBody: body,
|
|
426
|
+
tokens: extractComponentTokens(body),
|
|
427
|
+
};
|
|
428
|
+
break;
|
|
429
|
+
case "layout":
|
|
430
|
+
result.sections.layout = {
|
|
431
|
+
id,
|
|
432
|
+
present: true,
|
|
433
|
+
headingText: raw.heading,
|
|
434
|
+
rawBody: body,
|
|
435
|
+
tokens: extractSpacingTokens(body),
|
|
436
|
+
};
|
|
437
|
+
break;
|
|
438
|
+
case "shadows":
|
|
439
|
+
result.sections.shadows = {
|
|
440
|
+
id,
|
|
441
|
+
present: true,
|
|
442
|
+
headingText: raw.heading,
|
|
443
|
+
rawBody: body,
|
|
444
|
+
tokens: extractShadowTokens(body),
|
|
445
|
+
};
|
|
446
|
+
break;
|
|
447
|
+
case "dos-donts":
|
|
448
|
+
result.sections["dos-donts"] = {
|
|
449
|
+
id,
|
|
450
|
+
present: true,
|
|
451
|
+
headingText: raw.heading,
|
|
452
|
+
rawBody: body,
|
|
453
|
+
rules: extractDoDontRules(body),
|
|
454
|
+
};
|
|
455
|
+
break;
|
|
456
|
+
case "responsive":
|
|
457
|
+
result.sections.responsive = {
|
|
458
|
+
id,
|
|
459
|
+
present: true,
|
|
460
|
+
headingText: raw.heading,
|
|
461
|
+
rawBody: body,
|
|
462
|
+
breakpoints: extractBreakpoints(body),
|
|
463
|
+
};
|
|
464
|
+
break;
|
|
465
|
+
case "agent-prompts":
|
|
466
|
+
result.sections["agent-prompts"] = {
|
|
467
|
+
id,
|
|
468
|
+
present: true,
|
|
469
|
+
headingText: raw.heading,
|
|
470
|
+
rawBody: body,
|
|
471
|
+
prompts: extractAgentPrompts(body),
|
|
472
|
+
};
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return result;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// ────────────────────────────────────────────────────────────────────
|
|
480
|
+
// Validation
|
|
481
|
+
// ────────────────────────────────────────────────────────────────────
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Surface gaps in a parsed DESIGN.md. The validator never returns
|
|
485
|
+
* errors that block builds — Mandu's enforcement layer (Guard) is the
|
|
486
|
+
* gate. This function is the diagnostic that tells the user "your
|
|
487
|
+
* DESIGN.md is missing colour tokens" so they can fill it in.
|
|
488
|
+
*/
|
|
489
|
+
export function validateDesignSpec(spec: DesignSpec): ValidationResult {
|
|
490
|
+
const issues: ValidationIssue[] = [];
|
|
491
|
+
for (const id of DESIGN_SECTION_IDS) {
|
|
492
|
+
const section = spec.sections[id] as AnyDesignSection;
|
|
493
|
+
if (!section.present) {
|
|
494
|
+
issues.push({
|
|
495
|
+
kind: "missing",
|
|
496
|
+
section: id,
|
|
497
|
+
message: `Section "${id}" not found. Add a "## ${humanizeSectionId(id)}" heading.`,
|
|
498
|
+
});
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
const empty = isSectionEmpty(section);
|
|
502
|
+
if (empty) {
|
|
503
|
+
issues.push({
|
|
504
|
+
kind: "empty",
|
|
505
|
+
section: id,
|
|
506
|
+
message: `Section "${id}" is present but has no structured tokens.`,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return { ok: issues.length === 0, issues };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function isSectionEmpty(section: AnyDesignSection): boolean {
|
|
514
|
+
switch (section.id) {
|
|
515
|
+
case "theme":
|
|
516
|
+
return !section.summary || section.summary.length === 0;
|
|
517
|
+
case "color-palette":
|
|
518
|
+
case "typography":
|
|
519
|
+
case "components":
|
|
520
|
+
case "layout":
|
|
521
|
+
case "shadows":
|
|
522
|
+
return section.tokens.length === 0;
|
|
523
|
+
case "dos-donts":
|
|
524
|
+
return section.rules.length === 0;
|
|
525
|
+
case "responsive":
|
|
526
|
+
return section.breakpoints.length === 0;
|
|
527
|
+
case "agent-prompts":
|
|
528
|
+
return section.prompts.length === 0;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function humanizeSectionId(id: DesignSectionId): string {
|
|
533
|
+
switch (id) {
|
|
534
|
+
case "theme":
|
|
535
|
+
return "Visual Theme & Philosophy";
|
|
536
|
+
case "color-palette":
|
|
537
|
+
return "Color Palette";
|
|
538
|
+
case "typography":
|
|
539
|
+
return "Typography";
|
|
540
|
+
case "components":
|
|
541
|
+
return "Components";
|
|
542
|
+
case "layout":
|
|
543
|
+
return "Layout";
|
|
544
|
+
case "shadows":
|
|
545
|
+
return "Depth & Elevation";
|
|
546
|
+
case "dos-donts":
|
|
547
|
+
return "Do's & Don'ts";
|
|
548
|
+
case "responsive":
|
|
549
|
+
return "Responsive";
|
|
550
|
+
case "agent-prompts":
|
|
551
|
+
return "Agent Prompts";
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
export { humanizeSectionId };
|