@effected/markdown 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/Frontmatter.js +272 -0
  2. package/FrontmatterResolver.js +273 -0
  3. package/JsonFrontmatter.js +46 -0
  4. package/LICENSE +21 -0
  5. package/Markdown.js +251 -0
  6. package/MarkdownDiagnostic.js +85 -0
  7. package/MarkdownDocument.js +248 -0
  8. package/MarkdownEdit.js +52 -0
  9. package/MarkdownFormat.js +380 -0
  10. package/MarkdownNode.js +641 -0
  11. package/MarkdownVisitor.js +88 -0
  12. package/Mdast.js +384 -0
  13. package/README.md +255 -0
  14. package/TomlFrontmatter.js +44 -0
  15. package/YamlFrontmatter.js +45 -0
  16. package/index.d.ts +2190 -0
  17. package/index.js +15 -0
  18. package/internal/blockParser.js +419 -0
  19. package/internal/blockRegistry.js +90 -0
  20. package/internal/blockTypes.js +27 -0
  21. package/internal/blocks/atxHeading.js +54 -0
  22. package/internal/blocks/blockquote.js +40 -0
  23. package/internal/blocks/code.js +90 -0
  24. package/internal/blocks/document.js +17 -0
  25. package/internal/blocks/fencedCode.js +26 -0
  26. package/internal/blocks/footnoteDefinition.js +84 -0
  27. package/internal/blocks/frontmatter.js +56 -0
  28. package/internal/blocks/htmlBlock.js +72 -0
  29. package/internal/blocks/indentedCode.js +16 -0
  30. package/internal/blocks/linkReferenceDefinition.js +72 -0
  31. package/internal/blocks/list.js +159 -0
  32. package/internal/blocks/paragraph.js +27 -0
  33. package/internal/blocks/setextHeading.js +24 -0
  34. package/internal/blocks/table.js +378 -0
  35. package/internal/blocks/taskListItem.js +36 -0
  36. package/internal/blocks/thematicBreak.js +35 -0
  37. package/internal/carriers.js +42 -0
  38. package/internal/entities.js +26 -0
  39. package/internal/entityMap.js +7 -0
  40. package/internal/htmlTags.js +18 -0
  41. package/internal/inlineNode.js +54 -0
  42. package/internal/inlineParser.js +403 -0
  43. package/internal/inlineRegistry.js +68 -0
  44. package/internal/inlines/autolink.js +36 -0
  45. package/internal/inlines/autolinkLiteral.js +374 -0
  46. package/internal/inlines/codeSpan.js +32 -0
  47. package/internal/inlines/emphasis.js +79 -0
  48. package/internal/inlines/entity.js +21 -0
  49. package/internal/inlines/escape.js +34 -0
  50. package/internal/inlines/footnoteReference.js +63 -0
  51. package/internal/inlines/lineBreak.js +25 -0
  52. package/internal/inlines/link.js +212 -0
  53. package/internal/inlines/rawHtml.js +27 -0
  54. package/internal/inlines/strikethrough.js +81 -0
  55. package/internal/inlines/text.js +22 -0
  56. package/internal/lineIndex.js +76 -0
  57. package/internal/patterns.js +26 -0
  58. package/internal/preprocess.js +58 -0
  59. package/internal/rawInline.js +57 -0
  60. package/internal/references.js +160 -0
  61. package/internal/segments.js +36 -0
  62. package/internal/stringify.js +519 -0
  63. package/internal/unescape.js +18 -0
  64. package/package.json +61 -0
  65. package/tsdoc-metadata.json +11 -0
package/Frontmatter.js ADDED
@@ -0,0 +1,272 @@
1
+ import { MarkdownEdit } from "./MarkdownEdit.js";
2
+ import { FrontmatterFormat } from "./MarkdownNode.js";
3
+ import { Effect, Schema } from "effect";
4
+
5
+ //#region src/Frontmatter.ts
6
+ /**
7
+ * Indicates that a frontmatter codec was handed a capture of a different
8
+ * format — a yaml codec applied to a `+++` toml capture, for example.
9
+ *
10
+ * @remarks
11
+ * The mismatch is detected before any parsing happens, so `cause`-free: the
12
+ * node's `format` marker and the codec's declared `format` simply disagree.
13
+ * Route on the `"FrontmatterFormatMismatchError"` tag with `Effect.catchTag`.
14
+ *
15
+ * @public
16
+ */
17
+ var FrontmatterFormatMismatchError = class extends Schema.TaggedErrorClass()("FrontmatterFormatMismatchError", {
18
+ /** The format the codec decodes. */
19
+ expected: FrontmatterFormat,
20
+ /** The format the capture node actually carries. */
21
+ actual: FrontmatterFormat
22
+ }) {
23
+ get message() {
24
+ return `frontmatter format mismatch: the ${this.expected} codec cannot decode a ${this.actual} capture`;
25
+ }
26
+ };
27
+ /**
28
+ * Indicates that a frontmatter capture's content failed to parse in its
29
+ * declared format.
30
+ *
31
+ * @remarks
32
+ * The underlying format package's failure is preserved structurally in
33
+ * `cause` — never stringified — so a consumer can reach the positioned
34
+ * diagnostics the format engines carry. Route on the
35
+ * `"FrontmatterDecodeError"` tag with `Effect.catchTag`.
36
+ *
37
+ * @public
38
+ */
39
+ var FrontmatterDecodeError = class extends Schema.TaggedErrorClass()("FrontmatterDecodeError", {
40
+ /** The format that failed to parse. */
41
+ format: FrontmatterFormat,
42
+ /** The underlying format-package failure, preserved structurally. */
43
+ cause: Schema.Defect()
44
+ }) {
45
+ get message() {
46
+ return `frontmatter ${this.format} content failed to parse`;
47
+ }
48
+ };
49
+ /**
50
+ * Indicates that frontmatter data failed to serialize in a codec's format.
51
+ *
52
+ * @remarks
53
+ * The exact mirror of {@link FrontmatterDecodeError} on the write side: the
54
+ * underlying format package's failure is preserved structurally in `cause` —
55
+ * never stringified — so a consumer can reach the typed stringify error the
56
+ * format engines carry (a `JsoncStringifyError`'s `code`, for example). Route
57
+ * on the `"FrontmatterEncodeError"` tag with `Effect.catchTag`.
58
+ *
59
+ * @public
60
+ */
61
+ var FrontmatterEncodeError = class extends Schema.TaggedErrorClass()("FrontmatterEncodeError", {
62
+ /** The format that failed to serialize. */
63
+ format: FrontmatterFormat,
64
+ /** The underlying format-package failure, preserved structurally. */
65
+ cause: Schema.Defect()
66
+ }) {
67
+ get message() {
68
+ return `frontmatter ${this.format} content failed to serialize`;
69
+ }
70
+ };
71
+ /**
72
+ * Indicates that a document handed to a frontmatter decoder carries no
73
+ * frontmatter capture.
74
+ *
75
+ * @remarks
76
+ * Raised when the document genuinely has no frontmatter block — including
77
+ * when it has one in the source but was parsed with the capture toggle off
78
+ * (`MarkdownParseOptions.frontmatter` defaults to `false`). Cause-free: there
79
+ * is nothing to diagnose beyond the absence itself. Consumers wanting
80
+ * optional semantics can `Effect.catchTag("FrontmatterMissingError", ...)`
81
+ * to a default.
82
+ *
83
+ * @public
84
+ */
85
+ var FrontmatterMissingError = class extends Schema.TaggedErrorClass()("FrontmatterMissingError", {}) {
86
+ get message() {
87
+ return "the document has no frontmatter capture; was it parsed with `frontmatter: true`?";
88
+ }
89
+ };
90
+ /**
91
+ * Indicates that decoded frontmatter data did not satisfy the consumer's
92
+ * schema.
93
+ *
94
+ * @remarks
95
+ * `issue` carries the **structured** schema failure — at runtime a
96
+ * `SchemaIssue.Issue` tree, reachable through `_tag` and nested `issues` —
97
+ * never a stringified rendering (the `ConfigValidationError` precedent from
98
+ * `@effected/config-file`). It is typed `unknown` because v4 exposes no
99
+ * `Schema` for `Issue`; narrow it with the `SchemaIssue` module.
100
+ *
101
+ * @public
102
+ */
103
+ var FrontmatterValidationError = class extends Schema.TaggedErrorClass()("FrontmatterValidationError", {
104
+ /** The structured schema issue. Never a string. */
105
+ issue: Schema.Defect() }) {
106
+ get message() {
107
+ return "frontmatter data failed schema validation";
108
+ }
109
+ };
110
+ /**
111
+ * The opening and closing fence lines for a frontmatter format — the closed
112
+ * grammar the capture scanner recognizes: `---`…`---` yaml, `+++`…`+++` toml,
113
+ * `---json`…`---` json.
114
+ */
115
+ const FENCES = {
116
+ yaml: {
117
+ open: "---",
118
+ close: "---"
119
+ },
120
+ toml: {
121
+ open: "+++",
122
+ close: "+++"
123
+ },
124
+ json: {
125
+ open: "---json",
126
+ close: "---"
127
+ }
128
+ };
129
+ /**
130
+ * Render a full frontmatter block — fences included, no trailing terminator
131
+ * after the closing fence. The body's final line terminator is normalized:
132
+ * the engines disagree (yaml and toml stringify emit a trailing newline,
133
+ * jsonc does not), and an empty body (toml's encoding of `{}`) collapses to
134
+ * adjacent fence lines.
135
+ */
136
+ const renderBlock = (format, body) => {
137
+ const { open, close } = FENCES[format];
138
+ return `${open}\n${body === "" || body.endsWith("\n") ? body : `${body}\n`}${close}`;
139
+ };
140
+ /**
141
+ * The frontmatter schema composition facade — typed gray-matter parity.
142
+ *
143
+ * @remarks
144
+ * The design doc's indicative spelling was `Frontmatter.schema`, but
145
+ * `Frontmatter` names the capture node class (the node classes co-locate in
146
+ * `MarkdownNode.ts` and are named after their mdast types), so the facade
147
+ * follows the package's Markdown-prefix convention instead.
148
+ *
149
+ * @public
150
+ */
151
+ var MarkdownFrontmatter = class MarkdownFrontmatter {
152
+ /**
153
+ * Compose a consumer schema with a {@link FrontmatterCodec} into a typed
154
+ * decoder over a parsed `MarkdownDocument`.
155
+ *
156
+ * @remarks
157
+ * The decoder reads the document's frontmatter capture (parse with
158
+ * `frontmatter: true` — the toggle defaults off), decodes its raw value
159
+ * through the codec, then validates the data against `schema`. Each stage
160
+ * fails typed: no capture is {@link FrontmatterMissingError}, a
161
+ * wrong-format codec is {@link FrontmatterFormatMismatchError}, unparseable
162
+ * content is {@link FrontmatterDecodeError}, and schema-invalid data is
163
+ * {@link FrontmatterValidationError} carrying the structured issue.
164
+ *
165
+ * The seam takes the parsed document, not raw source: parse options
166
+ * (dialect, the frontmatter toggle) stay at the consumer's parse call and
167
+ * are never guessed here. Node-level composition remains available through
168
+ * `MarkdownDocument.frontmatter` plus the codec's own `decode`.
169
+ *
170
+ * Schema-producing in spirit: bind the returned decoder to a `const` when
171
+ * decoding many documents.
172
+ *
173
+ * @param schema - The schema the decoded frontmatter data must satisfy.
174
+ * @param codec - The format codec to decode the raw capture with.
175
+ * @returns A function from a parsed document to an `Effect` of the typed
176
+ * frontmatter data.
177
+ */
178
+ static schema(schema, codec) {
179
+ return (document) => {
180
+ const node = document.frontmatter;
181
+ return node === void 0 ? Effect.fail(new FrontmatterMissingError()) : codec.decode(node).pipe(Effect.flatMap((data) => Schema.decodeUnknownEffect(schema)(data).pipe(Effect.catchTag("SchemaError", (error) => Effect.fail(new FrontmatterValidationError({ issue: error.issue }))))));
182
+ };
183
+ }
184
+ /**
185
+ * Compose a consumer schema with a {@link FrontmatterCodec} into a typed
186
+ * frontmatter **writer** over a parsed `MarkdownDocument` — the write
187
+ * mirror of {@link MarkdownFrontmatter.schema}.
188
+ *
189
+ * @remarks
190
+ * The writer schema-**encodes** the typed data, serializes it through the
191
+ * codec and returns the offset-splice edits that put the block in place —
192
+ * always exactly one:
193
+ *
194
+ * - A document **with** a frontmatter capture of the codec's format gets a
195
+ * replacement of the entire block, both fence lines included; everything
196
+ * outside the block survives byte-identical. A capture of a *different*
197
+ * format fails with {@link FrontmatterFormatMismatchError} — the fences
198
+ * are never switched.
199
+ * - A document with **no** capture gets one insert at offset 0: the fenced
200
+ * block plus a blank line separating it from the existing content. Parse
201
+ * with `frontmatter: true` (the toggle defaults off) — the same
202
+ * precondition `schema` has — so absence means genuinely-no-frontmatter
203
+ * and the insert cannot double a block the parse ignored.
204
+ *
205
+ * Each stage fails typed: schema-invalid data is
206
+ * {@link FrontmatterValidationError} carrying the structured issue, and a
207
+ * value the format cannot serialize is {@link FrontmatterEncodeError}
208
+ * carrying the format package's failure structurally.
209
+ *
210
+ * The block is re-serialized **whole** from the encoded data — gray-matter
211
+ * parity, not surgical editing — so anything the format's data model does
212
+ * not carry is not preserved: comments inside a yaml frontmatter block do
213
+ * **not** survive `set`. A per-key surgical mode over the format packages'
214
+ * edit layers is a documented future refinement, not current scope.
215
+ *
216
+ * Schema-producing in spirit: bind the returned writer to a `const` when
217
+ * writing many documents.
218
+ *
219
+ * @param schema - The schema the frontmatter data is encoded through.
220
+ * @param codec - The format codec to serialize the encoded data with.
221
+ * @returns A function from a parsed document and the typed data to an
222
+ * `Effect` of the edits that install the block.
223
+ */
224
+ static set(schema, codec) {
225
+ return (document, data) => {
226
+ const node = document.frontmatter;
227
+ if (node !== void 0 && node.format !== codec.format) return Effect.fail(new FrontmatterFormatMismatchError({
228
+ expected: codec.format,
229
+ actual: node.format
230
+ }));
231
+ return Schema.encodeUnknownEffect(schema)(data).pipe(Effect.catchTag("SchemaError", (error) => Effect.fail(new FrontmatterValidationError({ issue: error.issue }))), Effect.flatMap((encoded) => codec.encode(encoded)), Effect.map((body) => {
232
+ const block = renderBlock(codec.format, body);
233
+ if (node === void 0) {
234
+ const content = document.source.length === 0 ? `${block}\n` : `${block}\n\n`;
235
+ return [MarkdownEdit.make({
236
+ offset: 0,
237
+ length: 0,
238
+ content
239
+ })];
240
+ }
241
+ const offset = node.position.start.offset;
242
+ return [MarkdownEdit.make({
243
+ offset,
244
+ length: node.position.end.offset - offset,
245
+ content: block
246
+ })];
247
+ }));
248
+ };
249
+ }
250
+ /**
251
+ * Like {@link MarkdownFrontmatter.set}, but applies the edits to the
252
+ * document's source and returns the updated markdown text — the
253
+ * `modifyToString` parallel.
254
+ *
255
+ * @remarks
256
+ * Everything `set` documents holds verbatim: the whole-block
257
+ * re-serialization, the format-mismatch posture, the `frontmatter: true`
258
+ * precondition and the typed failures.
259
+ *
260
+ * @param schema - The schema the frontmatter data is encoded through.
261
+ * @param codec - The format codec to serialize the encoded data with.
262
+ * @returns A function from a parsed document and the typed data to an
263
+ * `Effect` of the updated source text.
264
+ */
265
+ static setToString(schema, codec) {
266
+ const write = MarkdownFrontmatter.set(schema, codec);
267
+ return (document, data) => write(document, data).pipe(Effect.map((edits) => MarkdownEdit.applyAll(document.source, edits)));
268
+ }
269
+ };
270
+
271
+ //#endregion
272
+ export { FrontmatterDecodeError, FrontmatterEncodeError, FrontmatterFormatMismatchError, FrontmatterMissingError, FrontmatterValidationError, MarkdownFrontmatter };
@@ -0,0 +1,273 @@
1
+ import { Effect, Result, Schema } from "effect";
2
+
3
+ //#region src/FrontmatterResolver.ts
4
+ /**
5
+ * A `$schema` declaration referencing a schema by URL — any string containing
6
+ * `://`.
7
+ *
8
+ * @remarks
9
+ * Carried as data, never resolved in-package: the pure tier does no IO. An
10
+ * external resolver implementing {@link FrontmatterSchemaResolver} may fetch
11
+ * and interpret it.
12
+ *
13
+ * @public
14
+ */
15
+ var SchemaDeclarationByUrl = class extends Schema.TaggedClass()("ByUrl", {
16
+ /** The URL as written in the declaration. */
17
+ url: Schema.String }) {};
18
+ /**
19
+ * A `$schema` declaration referencing a schema by path — any string starting
20
+ * `./`, `../` or `/` (a bundle- or file-relative reference).
21
+ *
22
+ * @remarks
23
+ * Carried as data, never resolved in-package: the pure tier does no IO.
24
+ *
25
+ * @public
26
+ */
27
+ var SchemaDeclarationByPath = class extends Schema.TaggedClass()("ByPath", {
28
+ /** The path as written in the declaration. */
29
+ path: Schema.String }) {};
30
+ /**
31
+ * A `$schema` declaration carrying an inline JSON-Schema-like document — the
32
+ * declaration value is itself a mapping.
33
+ *
34
+ * @remarks
35
+ * Carried as data: the kit deliberately ships no JSON Schema engine
36
+ * (`@effected/json-schema` is off the roadmap), so an inline document is
37
+ * interpretable only through an external resolver plugged into the
38
+ * {@link FrontmatterSchemaResolver} seam.
39
+ *
40
+ * @public
41
+ */
42
+ var SchemaDeclarationInline = class extends Schema.TaggedClass()("Inline", {
43
+ /** The inline schema document, exactly as decoded from the frontmatter. */
44
+ document: Schema.Unknown }) {};
45
+ /**
46
+ * A `$schema` declaration referencing a registered schema by name — any other
47
+ * string, with the committed `name[@version]` grammar.
48
+ *
49
+ * @remarks
50
+ * The string splits at the **last** `@`, so a leading npm-style scope
51
+ * survives: `@savvy/skill@2.1.0` is name `@savvy/skill`, version `2.1.0`.
52
+ * The version grammar is `X[.Y[.Z]]` — one to three dot-separated
53
+ * non-negative integers; no prerelease, no build metadata, no npm range
54
+ * operators. The recorded cost: `@` in a name is reserved forever as the
55
+ * version separator, except the leading scope `@`.
56
+ *
57
+ * @public
58
+ */
59
+ var SchemaDeclarationByName = class extends Schema.TaggedClass()("ByName", {
60
+ /** The name as written, scope included. */
61
+ name: Schema.String,
62
+ /** The version as written, when the declaration carries one. */
63
+ version: Schema.optionalKey(Schema.String)
64
+ }) {};
65
+ /**
66
+ * The classified `$schema` declaration union — the full grammar contract for
67
+ * how a frontmatter block may self-describe its schema.
68
+ *
69
+ * @public
70
+ */
71
+ const SchemaDeclaration = Schema.Union([
72
+ SchemaDeclarationByUrl,
73
+ SchemaDeclarationByPath,
74
+ SchemaDeclarationInline,
75
+ SchemaDeclarationByName
76
+ ]);
77
+ /**
78
+ * Indicates that a `$schema` value does not classify: not a string or a
79
+ * mapping, an empty string, or a name whose version segment falls outside the
80
+ * committed `X[.Y[.Z]]` grammar.
81
+ *
82
+ * @public
83
+ */
84
+ var SchemaDeclarationInvalidError = class extends Schema.TaggedErrorClass()("SchemaDeclarationInvalidError", {
85
+ /** Why the value failed to classify. */
86
+ reason: Schema.String,
87
+ /** The offending value, preserved structurally. */
88
+ value: Schema.Defect()
89
+ }) {
90
+ get message() {
91
+ return `invalid $schema declaration: ${this.reason}`;
92
+ }
93
+ };
94
+ /**
95
+ * Indicates that frontmatter data carries no `$schema` declaration where one
96
+ * is required — the `requireDeclaration` strictness knob, or a registry
97
+ * resolver that has nothing to dispatch on.
98
+ *
99
+ * @public
100
+ */
101
+ var SchemaDeclarationMissingError = class extends Schema.TaggedErrorClass()("SchemaDeclarationMissingError", {}) {
102
+ get message() {
103
+ return "the frontmatter data carries no $schema declaration";
104
+ }
105
+ };
106
+ /**
107
+ * Indicates that a declaration named a schema the resolver does not know —
108
+ * an unregistered name, or a URL/path/inline declaration handed to the
109
+ * name-keyed registry resolver.
110
+ *
111
+ * @public
112
+ */
113
+ var SchemaNameUnknownError = class extends Schema.TaggedErrorClass()("SchemaNameUnknownError", {
114
+ /** The declaration that failed to resolve, when one exists. */
115
+ declaration: Schema.optionalKey(SchemaDeclaration) }) {
116
+ get message() {
117
+ return "the $schema declaration names no registered schema";
118
+ }
119
+ };
120
+ /**
121
+ * Indicates that a declaration's name is registered but its version segments
122
+ * match no registration exactly — distinct from {@link SchemaNameUnknownError}
123
+ * by design, so a legal-but-unsatisfied partial version (`skill@2` against a
124
+ * `skill@2.1.0` registration) is diagnosable as a version problem, not an
125
+ * unknown schema.
126
+ *
127
+ * @public
128
+ */
129
+ var SchemaVersionUnresolvableError = class extends Schema.TaggedErrorClass()("SchemaVersionUnresolvableError", {
130
+ /** The registered name whose version could not be satisfied. */
131
+ name: Schema.String,
132
+ /** The requested version, when the declaration carried one. */
133
+ version: Schema.optionalKey(Schema.String)
134
+ }) {
135
+ get message() {
136
+ return this.version === void 0 ? `schema "${this.name}" is registered only with versions; the declaration carries none` : `schema "${this.name}" has no registration matching version "${this.version}" exactly`;
137
+ }
138
+ };
139
+ const parseVersionSegments = (version) => {
140
+ if (!/^\d+(\.\d+){0,2}$/.test(version)) return;
141
+ return version.split(".").map((segment) => Number.parseInt(segment, 10));
142
+ };
143
+ const isMapping = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
144
+ /**
145
+ * The `$schema` declaration classifier and the package's one built-in
146
+ * resolver implementation.
147
+ *
148
+ * @public
149
+ */
150
+ var SchemaResolver = class SchemaResolver {
151
+ /**
152
+ * Classify a raw `$schema` value into the declaration union.
153
+ *
154
+ * @remarks
155
+ * Total over its legal domain and typed on junk: a string containing
156
+ * `://` is {@link SchemaDeclarationByUrl}; a string starting `./`, `../`
157
+ * or `/` is {@link SchemaDeclarationByPath}; a mapping is
158
+ * {@link SchemaDeclarationInline}; any other non-empty string is
159
+ * {@link SchemaDeclarationByName} under the `name[@version]` grammar.
160
+ * Everything else — and a name whose version falls outside `X[.Y[.Z]]` —
161
+ * fails with {@link SchemaDeclarationInvalidError}.
162
+ *
163
+ * @param value - The raw `$schema` value from decoded frontmatter data.
164
+ * @returns The classified declaration, or the typed classification error.
165
+ */
166
+ static classify(value) {
167
+ if (typeof value === "string") {
168
+ if (value.length === 0) return Result.fail(new SchemaDeclarationInvalidError({
169
+ reason: "the declaration is empty",
170
+ value
171
+ }));
172
+ if (value.includes("://")) return Result.succeed(new SchemaDeclarationByUrl({ url: value }));
173
+ if (value.startsWith("./") || value.startsWith("../") || value.startsWith("/")) return Result.succeed(new SchemaDeclarationByPath({ path: value }));
174
+ const separator = value.lastIndexOf("@");
175
+ if (separator <= 0) return Result.succeed(new SchemaDeclarationByName({ name: value }));
176
+ const name = value.slice(0, separator);
177
+ const version = value.slice(separator + 1);
178
+ if (parseVersionSegments(version) === void 0) return Result.fail(new SchemaDeclarationInvalidError({
179
+ reason: `version "${version}" is outside the X[.Y[.Z]] integer grammar`,
180
+ value
181
+ }));
182
+ return Result.succeed(new SchemaDeclarationByName({
183
+ name,
184
+ version
185
+ }));
186
+ }
187
+ if (isMapping(value)) return Result.succeed(new SchemaDeclarationInline({ document: value }));
188
+ return Result.fail(new SchemaDeclarationInvalidError({
189
+ reason: "the declaration is neither a string nor a mapping",
190
+ value
191
+ }));
192
+ }
193
+ /**
194
+ * Extract and classify the `$schema` declaration from decoded frontmatter
195
+ * data.
196
+ *
197
+ * @remarks
198
+ * Non-mapping data and a mapping without a `$schema` key both carry no
199
+ * declaration: the result succeeds with `undefined` by default, or fails
200
+ * with {@link SchemaDeclarationMissingError} under `requireDeclaration` —
201
+ * the design's strictness knob, applied at extraction where the
202
+ * present-or-absent branch actually lives.
203
+ *
204
+ * @param data - The decoded frontmatter data.
205
+ * @param options - `requireDeclaration` makes a missing `$schema` a typed
206
+ * error.
207
+ * @returns The classified declaration, `undefined` when absent and
208
+ * tolerated, or the typed error.
209
+ */
210
+ static declarationOf(data, options) {
211
+ if (!isMapping(data) || !Object.hasOwn(data, "$schema")) return options?.requireDeclaration === true ? Result.fail(new SchemaDeclarationMissingError()) : Result.succeed(void 0);
212
+ return SchemaResolver.classify(data.$schema);
213
+ }
214
+ /**
215
+ * The package's one built-in resolver: a name-keyed registry with day-one
216
+ * exact version-segment resolution.
217
+ *
218
+ * @remarks
219
+ * Registration keys use the same `name[@version]` grammar as declarations
220
+ * — carrying a concrete version or none — and are validated eagerly: a key
221
+ * outside the grammar, or two keys whose version segments collide
222
+ * numerically, throws at construction (programmer error, not input).
223
+ *
224
+ * Resolution is exact: a declaration resolves only against an identically
225
+ * written registration (version segments compared numerically), a
226
+ * versionless declaration only against a versionless registration, and a
227
+ * legal-but-unsatisfied version fails with the dedicated
228
+ * {@link SchemaVersionUnresolvableError}, distinct from
229
+ * {@link SchemaNameUnknownError}. URL, path and inline declarations are
230
+ * never resolvable here — those belong to external resolvers plugged into
231
+ * the same seam. A registry cannot dispatch without a declaration, so an
232
+ * absent one fails with {@link SchemaDeclarationMissingError}.
233
+ *
234
+ * @param registrations - Schemas keyed by `name[@version]`.
235
+ * @returns The registry-backed resolver.
236
+ */
237
+ static fromRegistry(registrations) {
238
+ const byName = /* @__PURE__ */ new Map();
239
+ for (const [key, schema] of Object.entries(registrations)) {
240
+ const classified = SchemaResolver.classify(key);
241
+ if (Result.isFailure(classified) || !(classified.success instanceof SchemaDeclarationByName)) throw new Error(`SchemaResolver.fromRegistry: registration key "${key}" is outside the name[@version] grammar`);
242
+ const declaration = classified.success;
243
+ const entry = byName.get(declaration.name) ?? { versions: /* @__PURE__ */ new Map() };
244
+ if (declaration.version === void 0) {
245
+ if (entry.versionless !== void 0) throw new Error(`SchemaResolver.fromRegistry: duplicate versionless registration for "${declaration.name}"`);
246
+ entry.versionless = schema;
247
+ } else {
248
+ const segments = parseVersionSegments(declaration.version);
249
+ if (segments === void 0) throw new Error(`SchemaResolver.fromRegistry: registration key "${key}" carries an illegal version`);
250
+ const canonical = segments.join(".");
251
+ if (entry.versions.has(canonical)) throw new Error(`SchemaResolver.fromRegistry: registrations for "${declaration.name}" collide on version ${canonical}`);
252
+ entry.versions.set(canonical, schema);
253
+ }
254
+ byName.set(declaration.name, entry);
255
+ }
256
+ return { resolve: (declaration, _data) => {
257
+ if (declaration === void 0) return Effect.fail(new SchemaDeclarationMissingError());
258
+ if (!(declaration instanceof SchemaDeclarationByName)) return Effect.fail(new SchemaNameUnknownError({ declaration }));
259
+ const entry = byName.get(declaration.name);
260
+ if (entry === void 0) return Effect.fail(new SchemaNameUnknownError({ declaration }));
261
+ if (declaration.version === void 0) return entry.versionless === void 0 ? Effect.fail(new SchemaVersionUnresolvableError({ name: declaration.name })) : Effect.succeed(entry.versionless);
262
+ const segments = parseVersionSegments(declaration.version);
263
+ const match = segments === void 0 ? void 0 : entry.versions.get(segments.join("."));
264
+ return match === void 0 ? Effect.fail(new SchemaVersionUnresolvableError({
265
+ name: declaration.name,
266
+ version: declaration.version
267
+ })) : Effect.succeed(match);
268
+ } };
269
+ }
270
+ };
271
+
272
+ //#endregion
273
+ export { SchemaDeclaration, SchemaDeclarationByName, SchemaDeclarationByPath, SchemaDeclarationByUrl, SchemaDeclarationInline, SchemaDeclarationInvalidError, SchemaDeclarationMissingError, SchemaNameUnknownError, SchemaResolver, SchemaVersionUnresolvableError };
@@ -0,0 +1,46 @@
1
+ import { FrontmatterDecodeError, FrontmatterEncodeError, FrontmatterFormatMismatchError } from "./Frontmatter.js";
2
+ import { Effect } from "effect";
3
+ import { Jsonc } from "@effected/jsonc";
4
+
5
+ //#region src/JsonFrontmatter.ts
6
+ /**
7
+ * The json frontmatter codec, over `@effected/jsonc`.
8
+ *
9
+ * @remarks
10
+ * Decodes a `---json`-fenced capture's raw value with `Jsonc.parse`, so the
11
+ * jsonc engine's nesting depth cap fails through the typed channel: a hostile
12
+ * frontmatter block surfaces as a {@link FrontmatterDecodeError} carrying the
13
+ * `JsoncParseError` structurally, never a defect. JSONC being a JSON
14
+ * superset, comments and trailing commas in a json capture decode rather than
15
+ * fail — deliberate leniency, matching the kit's one JSON-family engine. An
16
+ * empty capture fails typed: unlike yaml and toml, JSON has no
17
+ * empty-document value.
18
+ *
19
+ * Encodes with `Jsonc.stringify` — plain 2-space JSON emission, never
20
+ * comments; a circular reference, a `bigint` or a top-level unrepresentable
21
+ * value fails as a {@link FrontmatterEncodeError} carrying the
22
+ * `JsoncStringifyError` structurally. An empty object encodes to `{}`.
23
+ *
24
+ * `@effected/jsonc` is an optional peer — importing this module is what
25
+ * requires it; a consumer who never touches json frontmatter never loads the
26
+ * jsonc engine.
27
+ *
28
+ * @public
29
+ */
30
+ const JsonFrontmatter = {
31
+ format: "json",
32
+ decode: (node) => node.format !== "json" ? Effect.fail(new FrontmatterFormatMismatchError({
33
+ expected: "json",
34
+ actual: node.format
35
+ })) : Jsonc.parse(node.value).pipe(Effect.mapError((cause) => new FrontmatterDecodeError({
36
+ format: "json",
37
+ cause
38
+ }))),
39
+ encode: (data) => Jsonc.stringify(data).pipe(Effect.mapError((cause) => new FrontmatterEncodeError({
40
+ format: "json",
41
+ cause
42
+ })))
43
+ };
44
+
45
+ //#endregion
46
+ export { JsonFrontmatter };
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 C. Spencer Beggs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.