@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.
- package/Frontmatter.js +272 -0
- package/FrontmatterResolver.js +273 -0
- package/JsonFrontmatter.js +46 -0
- package/LICENSE +21 -0
- package/Markdown.js +251 -0
- package/MarkdownDiagnostic.js +85 -0
- package/MarkdownDocument.js +248 -0
- package/MarkdownEdit.js +52 -0
- package/MarkdownFormat.js +380 -0
- package/MarkdownNode.js +641 -0
- package/MarkdownVisitor.js +88 -0
- package/Mdast.js +384 -0
- package/README.md +255 -0
- package/TomlFrontmatter.js +44 -0
- package/YamlFrontmatter.js +45 -0
- package/index.d.ts +2190 -0
- package/index.js +15 -0
- package/internal/blockParser.js +419 -0
- package/internal/blockRegistry.js +90 -0
- package/internal/blockTypes.js +27 -0
- package/internal/blocks/atxHeading.js +54 -0
- package/internal/blocks/blockquote.js +40 -0
- package/internal/blocks/code.js +90 -0
- package/internal/blocks/document.js +17 -0
- package/internal/blocks/fencedCode.js +26 -0
- package/internal/blocks/footnoteDefinition.js +84 -0
- package/internal/blocks/frontmatter.js +56 -0
- package/internal/blocks/htmlBlock.js +72 -0
- package/internal/blocks/indentedCode.js +16 -0
- package/internal/blocks/linkReferenceDefinition.js +72 -0
- package/internal/blocks/list.js +159 -0
- package/internal/blocks/paragraph.js +27 -0
- package/internal/blocks/setextHeading.js +24 -0
- package/internal/blocks/table.js +378 -0
- package/internal/blocks/taskListItem.js +36 -0
- package/internal/blocks/thematicBreak.js +35 -0
- package/internal/carriers.js +42 -0
- package/internal/entities.js +26 -0
- package/internal/entityMap.js +7 -0
- package/internal/htmlTags.js +18 -0
- package/internal/inlineNode.js +54 -0
- package/internal/inlineParser.js +403 -0
- package/internal/inlineRegistry.js +68 -0
- package/internal/inlines/autolink.js +36 -0
- package/internal/inlines/autolinkLiteral.js +374 -0
- package/internal/inlines/codeSpan.js +32 -0
- package/internal/inlines/emphasis.js +79 -0
- package/internal/inlines/entity.js +21 -0
- package/internal/inlines/escape.js +34 -0
- package/internal/inlines/footnoteReference.js +63 -0
- package/internal/inlines/lineBreak.js +25 -0
- package/internal/inlines/link.js +212 -0
- package/internal/inlines/rawHtml.js +27 -0
- package/internal/inlines/strikethrough.js +81 -0
- package/internal/inlines/text.js +22 -0
- package/internal/lineIndex.js +76 -0
- package/internal/patterns.js +26 -0
- package/internal/preprocess.js +58 -0
- package/internal/rawInline.js +57 -0
- package/internal/references.js +160 -0
- package/internal/segments.js +36 -0
- package/internal/stringify.js +519 -0
- package/internal/unescape.js +18 -0
- package/package.json +61 -0
- 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.
|