@foldkit/markdown 0.2.0 → 0.3.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/README.md CHANGED
@@ -54,6 +54,8 @@ const view = (h: HtmlBuilder<Message>): Html =>
54
54
  h.div([], [Markdown.view(about)])
55
55
  ```
56
56
 
57
+ `decodeDocument` memoizes on the wire object, so calling it inside a view decodes each module once rather than once per render.
58
+
57
59
  `Markdown.view` renders every node through unstyled semantic defaults. Restyle any node by overriding its view:
58
60
 
59
61
  ```typescript
@@ -144,11 +146,54 @@ const postView = (
144
146
 
145
147
  Attribute values are strings on the wire, so transforming field schemas decode past them: `S.NumberFromString` turns `::Chart{height="240"}` into `height: number`. A plain `islands` record of untyped views (`Readonly<Record<string, IslandView>>`) also works when you want to skip the schemas.
146
148
 
149
+ ## Frontmatter
150
+
151
+ Documents can open with a frontmatter block when the plugin is given a schema for it. Declare the fields as a Schema struct, once, in a module both your Vite config and your application import:
152
+
153
+ ```typescript
154
+ // src/postFrontmatter.ts
155
+ import { Schema as S } from 'effect'
156
+
157
+ export const PostFrontmatter = S.Struct({
158
+ title: S.String.check(S.isNonEmpty()),
159
+ date: S.String,
160
+ })
161
+ ```
162
+
163
+ ```typescript
164
+ import { markdown } from '@foldkit/markdown/vite'
165
+
166
+ import { PostFrontmatter } from './src/postFrontmatter'
167
+
168
+ markdown({ frontmatter: PostFrontmatter })
169
+ ```
170
+
171
+ ```markdown
172
+ ---
173
+ title: 'Introducing the blog'
174
+ date: 2026-08-01
175
+ ---
176
+
177
+ The prose starts here.
178
+ ```
179
+
180
+ Every field validates at build time. An unknown field, a missing required field, or a value the schema rejects fails the build with the file and line. Without a `frontmatter` schema, a frontmatter block fails the build.
181
+
182
+ The supported shape is deliberately flat: one `key: value` pair per line, every value a string. A value wrapped in matching single or double quotes has that outer pair stripped, so values containing special characters like `:` stay unambiguous. Only the first and last characters decide, so a value that itself starts and ends with the same quote character loses that pair; wrap it in the other quote style to keep it. Nesting, lists, and multi-line values are not supported.
183
+
184
+ Every compiled `.md` module carries a `frontmatter` named export alongside the default document export. It holds the block's validated fields, and it is `undefined` when the document has no block:
185
+
186
+ ```typescript
187
+ import postRaw, { frontmatter } from './post/introducing-the-blog.md'
188
+ ```
189
+
190
+ The fields arrive as the raw strings the block declares. The build validates them against the schema and discards the decoded result, so where the application needs typed values, decode the export with the same schema at runtime; validation at build time means that decode cannot fail. `S.NumberFromString` and friends do their transformation in that runtime decode, not in the emitted module.
191
+
147
192
  ## Vocabulary
148
193
 
149
194
  The schema accepts CommonMark plus GFM tables and strikethrough: headings, paragraphs, emphasis, strong, strikethrough, inline code, links, images, hard breaks, nested lists, code blocks, blockquotes, thematic breaks, and tables. Directives (`::Name`, `:::Name`) become Island nodes.
150
195
 
151
- Anything outside the vocabulary fails the build with an error naming the construct and its line. Raw HTML is rejected by design; islands are the escape hatch. Reference-style links, footnotes, task lists, YAML frontmatter, and directive labels (`::Name[label]`) are not supported; keep document metadata in application code. Link and image URLs must be relative or use the `http:`, `https:`, `mailto:`, or `tel:` schemes; executable schemes like `javascript:` fail the build.
196
+ Anything outside the vocabulary fails the build with an error naming the construct and its line. Raw HTML is rejected by design; islands are the escape hatch. Reference-style links, footnotes, task lists, and directive labels (`::Name[label]`) are not supported. Frontmatter is supported only with a `frontmatter` schema configured, in the flat shape described above. Link and image URLs must be relative or use the `http:`, `https:`, `mailto:`, or `tel:` schemes; executable schemes like `javascript:` fail the build.
152
197
 
153
198
  ## One-off compilation
154
199
 
@@ -161,3 +206,15 @@ import { islandAttributes } from './islands'
161
206
 
162
207
  const document = parseMarkdown('# Title', { islands: islandAttributes })
163
208
  ```
209
+
210
+ `parseMarkdownWithFrontmatter` also returns the document's frontmatter fields, as an `Option` of the raw string record:
211
+
212
+ ```typescript
213
+ import { parseMarkdownWithFrontmatter } from '@foldkit/markdown/vite'
214
+
215
+ import { PostFrontmatter } from './postFrontmatter'
216
+
217
+ const { document, maybeFrontmatter } = parseMarkdownWithFrontmatter(source, {
218
+ frontmatter: PostFrontmatter,
219
+ })
220
+ ```
package/content.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  declare module '*.md' {
2
2
  const document: unknown
3
3
  export default document
4
+ export const frontmatter: unknown
4
5
  }
package/dist/ast/ast.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Option, Schema as S } from 'effect';
2
- import type { Array } from 'effect';
2
+ import type { Array, SchemaAST } from 'effect';
3
3
  /** Plain text. */
4
4
  export type Text = Readonly<{
5
5
  _tag: 'Text';
@@ -309,14 +309,23 @@ export type MarkdownDocumentEncoded = typeof MarkdownDocument.Encoded;
309
309
  /**
310
310
  * Decodes the default export of a compiled markdown module into a typed
311
311
  * {@link MarkdownDocument}. Throws on input outside the markdown vocabulary.
312
+ *
313
+ * Results are memoized on a `WeakMap` keyed by the wire object, so decoding the
314
+ * same compiled module again returns the document from the first decode. A
315
+ * module's wire object is immutable build output and the decode is
316
+ * deterministic, so a cached document can never disagree with a fresh one, and
317
+ * each entry is collected along with the module holding its key. Calling this
318
+ * from a view costs one decode per module rather than one per render.
319
+ *
320
+ * Passing `overrideOptions` bypasses the cache both ways: the decode ignores
321
+ * cached entries and its result is not stored, since a document decoded under
322
+ * one set of options cannot answer for another.
312
323
  */
313
- export declare const decodeDocument: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => {
314
- readonly blocks: readonly Block[];
315
- };
324
+ export declare const decodeDocument: (wire: unknown, overrideOptions?: SchemaAST.ParseOptions) => MarkdownDocument;
316
325
  /** Encodes a {@link MarkdownDocument} into its JSON-safe wire form. */
317
326
  export declare const encodeDocument: (input: {
318
327
  readonly blocks: readonly Block[];
319
- }, options?: import("effect/SchemaAST").ParseOptions) => {
328
+ }, options?: SchemaAST.ParseOptions) => {
320
329
  readonly blocks: readonly BlockEncoded[];
321
330
  };
322
331
  //# sourceMappingURL=ast.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../../src/ast/ast.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../../src/ast/ast.ts"],"names":[],"mappings":"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"}
package/dist/ast/ast.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Schema as S } from 'effect';
1
+ import { Function, Option, Predicate, Schema as S } from 'effect';
2
2
  import { ts } from 'foldkit/schema';
3
3
  /** Schema for {@link Text}. */
4
4
  export const Text = ts('Text', { value: S.String });
@@ -107,10 +107,37 @@ export const Island = ts('Island', {
107
107
  // DOCUMENT
108
108
  /** A compiled markdown document: the block sequence of one source file. */
109
109
  export const MarkdownDocument = S.Struct({ blocks: S.Array(Block) });
110
+ const decodeDocumentUncached = S.decodeUnknownSync(MarkdownDocument);
111
+ const documentByWire = new WeakMap();
110
112
  /**
111
113
  * Decodes the default export of a compiled markdown module into a typed
112
114
  * {@link MarkdownDocument}. Throws on input outside the markdown vocabulary.
115
+ *
116
+ * Results are memoized on a `WeakMap` keyed by the wire object, so decoding the
117
+ * same compiled module again returns the document from the first decode. A
118
+ * module's wire object is immutable build output and the decode is
119
+ * deterministic, so a cached document can never disagree with a fresh one, and
120
+ * each entry is collected along with the module holding its key. Calling this
121
+ * from a view costs one decode per module rather than one per render.
122
+ *
123
+ * Passing `overrideOptions` bypasses the cache both ways: the decode ignores
124
+ * cached entries and its result is not stored, since a document decoded under
125
+ * one set of options cannot answer for another.
113
126
  */
114
- export const decodeDocument = S.decodeUnknownSync(MarkdownDocument);
127
+ export const decodeDocument = (wire, overrideOptions) => {
128
+ if (overrideOptions === undefined && Predicate.isObject(wire)) {
129
+ return Option.match(Option.fromNullishOr(documentByWire.get(wire)), {
130
+ onNone: () => {
131
+ const document = decodeDocumentUncached(wire);
132
+ documentByWire.set(wire, document);
133
+ return document;
134
+ },
135
+ onSome: Function.identity,
136
+ });
137
+ }
138
+ else {
139
+ return decodeDocumentUncached(wire, overrideOptions);
140
+ }
141
+ };
115
142
  /** Encodes a {@link MarkdownDocument} into its JSON-safe wire form. */
116
143
  export const encodeDocument = S.encodeSync(MarkdownDocument);
@@ -0,0 +1,32 @@
1
+ import type { Position } from 'unist';
2
+ import type { FieldsSchema } from './validateFields.js';
3
+ /**
4
+ * Schema for a document's frontmatter: a struct that validates the flat string
5
+ * fields at build time. Values stay the raw strings the block declares; where
6
+ * an app needs typed values, decode the exported `frontmatter` object with the
7
+ * same schema at runtime.
8
+ */
9
+ export type FrontmatterDefinition = FieldsSchema;
10
+ /**
11
+ * A frontmatter block's parsed string fields, alongside each field's line
12
+ * offset within the block so validation errors can point at the offending
13
+ * line rather than the opening fence.
14
+ */
15
+ export type ParsedFrontmatterFields = Readonly<{
16
+ fields: Readonly<Record<string, string>>;
17
+ fieldLineOffsets: ReadonlyMap<string, number>;
18
+ }>;
19
+ /**
20
+ * Parses a frontmatter block's raw text into its string fields. The supported
21
+ * shape is deliberately flat: one `key: value` pair per line, every value a
22
+ * string, with optional surrounding quotes for values that contain special
23
+ * characters. Nesting, lists, and multi-line values all fail with guidance.
24
+ */
25
+ export declare const parseFrontmatterFields: (value: string, maybePosition: Position | undefined) => ParsedFrontmatterFields;
26
+ /**
27
+ * Validates parsed frontmatter fields against the plugin's frontmatter schema.
28
+ * Unknown fields and values outside the schema both fail with an error naming
29
+ * the offender and its line, mirroring how island attributes are validated.
30
+ */
31
+ export declare const validateFrontmatterFields: (definition: FrontmatterDefinition, parsed: ParsedFrontmatterFields, maybePosition: Position | undefined) => void;
32
+ //# sourceMappingURL=frontmatter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frontmatter.d.ts","sourceRoot":"","sources":["../../src/vite/frontmatter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAErC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAKvD;;;;;GAKG;AACH,MAAM,MAAM,qBAAqB,GAAG,YAAY,CAAA;AAwBhD;;;;GAIG;AACH,MAAM,MAAM,uBAAuB,GAAG,QAAQ,CAAC;IAC7C,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACxC,gBAAgB,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAC9C,CAAC,CAAA;AAEF;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,GACjC,OAAO,MAAM,EACb,eAAe,QAAQ,GAAG,SAAS,KAClC,uBAuCF,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,GACpC,YAAY,qBAAqB,EACjC,QAAQ,uBAAuB,EAC/B,eAAe,QAAQ,GAAG,SAAS,KAClC,IAoBF,CAAA"}
@@ -0,0 +1,72 @@
1
+ import { Array, Option, String as String_, pipe } from 'effect';
2
+ import { validateFields } from './validateFields.js';
3
+ const FRONTMATTER_ENTRY_PATTERN = /^([A-Za-z][A-Za-z0-9_-]*):(.*)$/;
4
+ const sourceLocation = (maybePosition, lineOffset) => {
5
+ const startLine = maybePosition?.start.line;
6
+ if (startLine === undefined) {
7
+ return '';
8
+ }
9
+ else {
10
+ return ` (line ${startLine + lineOffset})`;
11
+ }
12
+ };
13
+ const isWrappedIn = (value, quote) => value.length >= 2 && value.startsWith(quote) && value.endsWith(quote);
14
+ const unquote = (value) => isWrappedIn(value, '"') || isWrappedIn(value, "'")
15
+ ? value.slice(1, -1)
16
+ : value;
17
+ /**
18
+ * Parses a frontmatter block's raw text into its string fields. The supported
19
+ * shape is deliberately flat: one `key: value` pair per line, every value a
20
+ * string, with optional surrounding quotes for values that contain special
21
+ * characters. Nesting, lists, and multi-line values all fail with guidance.
22
+ */
23
+ export const parseFrontmatterFields = (value, maybePosition) => {
24
+ // NOTE: collected in a Map so field names inherited from Object.prototype
25
+ // (`toString`, `constructor`) neither trip the duplicate check nor land
26
+ // anywhere but an own property of the returned record.
27
+ const fieldValues = new Map();
28
+ const fieldLineOffsets = new Map();
29
+ const entryLines = pipe(String_.split(value, '\n'), Array.map((line, lineIndex) => ({ line, lineIndex })), Array.filter(({ line }) => String_.isNonEmpty(line.trim())));
30
+ for (const { line, lineIndex } of entryLines) {
31
+ const location = sourceLocation(maybePosition, lineIndex + 1);
32
+ const [fieldName, rawFieldValue] = Option.match(String_.match(FRONTMATTER_ENTRY_PATTERN)(line), {
33
+ onNone: () => {
34
+ throw new Error(`Invalid frontmatter entry${location}. ` +
35
+ 'Frontmatter supports flat `key: value` pairs of strings only. ' +
36
+ 'Nesting, lists, and multi-line values are not supported.');
37
+ },
38
+ onSome: ([, name = '', fieldValue = '']) => [
39
+ name,
40
+ fieldValue,
41
+ ],
42
+ });
43
+ if (fieldValues.has(fieldName)) {
44
+ throw new Error(`Duplicate frontmatter field "${fieldName}"${location}.`);
45
+ }
46
+ fieldValues.set(fieldName, unquote(rawFieldValue.trim()));
47
+ fieldLineOffsets.set(fieldName, lineIndex + 1);
48
+ }
49
+ return { fields: Object.fromEntries(fieldValues), fieldLineOffsets };
50
+ };
51
+ /**
52
+ * Validates parsed frontmatter fields against the plugin's frontmatter schema.
53
+ * Unknown fields and values outside the schema both fail with an error naming
54
+ * the offender and its line, mirroring how island attributes are validated.
55
+ */
56
+ export const validateFrontmatterFields = (definition, parsed, maybePosition) => {
57
+ const fieldLocation = (fieldName) => Option.match(Option.fromNullishOr(parsed.fieldLineOffsets.get(fieldName)), {
58
+ onNone: () => sourceLocation(maybePosition, 0),
59
+ onSome: lineOffset => sourceLocation(maybePosition, lineOffset),
60
+ });
61
+ validateFields({
62
+ schema: definition,
63
+ values: parsed.fields,
64
+ memberNounPlural: 'fields',
65
+ unknownField: (fieldName, allowedDescription) => `Unknown frontmatter field "${fieldName}"${fieldLocation(fieldName)}. ` +
66
+ allowedDescription,
67
+ invalidValues: (detail, maybeFieldName) => `Invalid frontmatter${Option.match(maybeFieldName, {
68
+ onNone: () => sourceLocation(maybePosition, 0),
69
+ onSome: fieldLocation,
70
+ })}. ${detail}`,
71
+ });
72
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"normalize.d.ts","sourceRoot":"","sources":["../../src/vite/normalize.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAKV,IAAI,EAEL,MAAM,OAAO,CAAA;AAId,OAAO,EAeL,gBAAgB,EASjB,MAAM,iBAAiB,CAAA;AACxB,OAAO,KAAK,EAAoB,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAE7E;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC;IACtC,OAAO,CAAC,EAAE,iBAAiB,GAAG,SAAS,CAAA;CACxC,CAAC,CAAA;AAoIF;;;GAGG;AACH,eAAO,MAAM,aAAa,GACxB,MAAM,IAAI,EACV,UAAS,gBAAqB,KAC7B,gBAwHF,CAAA"}
1
+ {"version":3,"file":"normalize.d.ts","sourceRoot":"","sources":["../../src/vite/normalize.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAKV,IAAI,EAEL,MAAM,OAAO,CAAA;AAId,OAAO,EAeL,gBAAgB,EASjB,MAAM,iBAAiB,CAAA;AACxB,OAAO,KAAK,EAAoB,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAG7E;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC;IACtC,OAAO,CAAC,EAAE,iBAAiB,GAAG,SAAS,CAAA;CACxC,CAAC,CAAA;AAoIF;;;GAGG;AACH,eAAO,MAAM,aAAa,GACxB,MAAM,IAAI,EACV,UAAS,gBAAqB,KAC7B,gBA0GF,CAAA"}
@@ -1,5 +1,6 @@
1
- import { Array, Match as M, Option, Record as Record_, Schema as S, } from 'effect';
1
+ import { Array, Match as M, Option, Record as Record_ } from 'effect';
2
2
  import { Blockquote, CodeBlock, Emphasis, HardBreak, Heading, Image, InlineCode, Island, Link, List, ListItem, Paragraph, Strikethrough, Strong, Table, TableCell, TableRow, Text, ThematicBreak, } from '../ast/index.js';
3
+ import { validateFields } from './validateFields.js';
3
4
  const UNSUPPORTED_GUIDANCE = {
4
5
  html: 'Raw HTML is not part of the markdown vocabulary. Use an island directive to render custom views.',
5
6
  textDirective: 'Inline directives are not supported. Use a leaf directive (`::Name`) on its own line, or a container directive (`:::Name`).',
@@ -8,7 +9,7 @@ const UNSUPPORTED_GUIDANCE = {
8
9
  definition: 'Reference-style link definitions are not supported. Use inline links (`[text](url)`).',
9
10
  footnoteReference: 'Footnotes are not supported.',
10
11
  footnoteDefinition: 'Footnotes are not supported.',
11
- yaml: 'Frontmatter is not supported. Keep document metadata in application code, for example a typed post registry.',
12
+ yaml: 'Frontmatter is not supported without a `frontmatter` schema in the markdown plugin options. Pass one to enable it, or keep document metadata in application code.',
12
13
  'leaf directive label': 'Leaf directive labels (`::Name[label]`) are not supported. Pass information through attributes (`::Name{label="..."}`) instead.',
13
14
  };
14
15
  const sourceLocation = (node) => {
@@ -70,25 +71,14 @@ const toTableRow = (row) => TableRow({
70
71
  * Throws on any node outside the markdown vocabulary.
71
72
  */
72
73
  export const normalizeRoot = (root, options = {}) => {
73
- const validateIslandAttributes = (directive, attributesSchema, attributes) => {
74
- const allowedAttributeNames = Object.keys(attributesSchema.fields);
75
- const unknownAttributeNames = Object.keys(attributes).filter(attributeName => !allowedAttributeNames.includes(attributeName));
76
- if (Array.isArrayNonEmpty(unknownAttributeNames)) {
77
- const allowedDescription = Array.match(allowedAttributeNames, {
78
- onEmpty: () => 'It takes no attributes.',
79
- onNonEmpty: names => `Allowed attributes: ${names.join(', ')}.`,
80
- });
81
- throw new Error(`Unknown attribute "${Array.headNonEmpty(unknownAttributeNames)}" for island "${directive.name}"${sourceLocation(directive)}. ` +
82
- allowedDescription);
83
- }
84
- try {
85
- S.decodeUnknownSync(attributesSchema)(attributes);
86
- }
87
- catch (error) {
88
- throw new Error(`Invalid attributes for island "${directive.name}"${sourceLocation(directive)}. ` +
89
- `${error instanceof Error ? error.message : String(error)}`);
90
- }
91
- };
74
+ const validateIslandAttributes = (directive, attributesSchema, attributes) => validateFields({
75
+ schema: attributesSchema,
76
+ values: attributes,
77
+ memberNounPlural: 'attributes',
78
+ unknownField: (attributeName, allowedDescription) => `Unknown attribute "${attributeName}" for island "${directive.name}"${sourceLocation(directive)}. ` +
79
+ allowedDescription,
80
+ invalidValues: detail => `Invalid attributes for island "${directive.name}"${sourceLocation(directive)}. ${detail}`,
81
+ });
92
82
  const toIsland = (directive, blocks) => {
93
83
  const attributes = normalizeAttributes(directive.attributes);
94
84
  const { islands } = options;
@@ -1,3 +1,4 @@
1
- export { markdown, parseMarkdown } from './vite.js';
2
- export type { MarkdownPluginOptions } from './vite.js';
1
+ export { markdown, parseMarkdown, parseMarkdownWithFrontmatter, } from './vite.js';
2
+ export type { MarkdownPluginOptions, ParsedMarkdown } from './vite.js';
3
+ export type { FrontmatterDefinition } from './frontmatter.js';
3
4
  //# sourceMappingURL=public.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../src/vite/public.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,WAAW,CAAA;AACnD,YAAY,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAA"}
1
+ {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../src/vite/public.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,aAAa,EACb,4BAA4B,GAC7B,MAAM,WAAW,CAAA;AAClB,YAAY,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AACtE,YAAY,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAA"}
@@ -1 +1 @@
1
- export { markdown, parseMarkdown } from './vite.js';
1
+ export { markdown, parseMarkdown, parseMarkdownWithFrontmatter, } from './vite.js';
@@ -0,0 +1,25 @@
1
+ import { Option, Schema as S } from 'effect';
2
+ /**
3
+ * A struct schema describing a flat record of string values, the shape shared
4
+ * by island attribute definitions and frontmatter definitions.
5
+ */
6
+ export type FieldsSchema = S.Struct<S.Struct.Fields> & Readonly<{
7
+ DecodingServices: never;
8
+ EncodingServices: never;
9
+ }>;
10
+ /**
11
+ * Validates a parsed string record against its schema: names outside the
12
+ * schema and values the schema rejects both fail with an error the caller
13
+ * phrases, and a rejected value's message also receives the failing field's
14
+ * name when the schema issue carries one. One implementation serves island
15
+ * attributes and frontmatter fields, so unknown-name detection and
16
+ * decode-error wrapping cannot drift between the two.
17
+ */
18
+ export declare const validateFields: (config: Readonly<{
19
+ schema: FieldsSchema;
20
+ values: Readonly<Record<string, string>>;
21
+ memberNounPlural: string;
22
+ unknownField: (fieldName: string, allowedDescription: string) => string;
23
+ invalidValues: (detail: string, maybeFieldName: Option.Option<string>) => string;
24
+ }>) => void;
25
+ //# sourceMappingURL=validateFields.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validateFields.d.ts","sourceRoot":"","sources":["../../src/vite/validateFields.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,MAAM,EAEN,MAAM,IAAI,CAAC,EAGZ,MAAM,QAAQ,CAAA;AAIf;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,GAClD,QAAQ,CAAC;IAAE,gBAAgB,EAAE,KAAK,CAAC;IAAC,gBAAgB,EAAE,KAAK,CAAA;CAAE,CAAC,CAAA;AAoBhE;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GACzB,QAAQ,QAAQ,CAAC;IACf,MAAM,EAAE,YAAY,CAAA;IACpB,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACxC,gBAAgB,EAAE,MAAM,CAAA;IACxB,YAAY,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,KAAK,MAAM,CAAA;IACvE,aAAa,EAAE,CACb,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,KAClC,MAAM,CAAA;CACZ,CAAC,KACD,IA6BF,CAAA"}
@@ -0,0 +1,31 @@
1
+ import { Array, Option, Predicate, Schema as S, SchemaIssue, pipe, } from 'effect';
2
+ const maybeInvalidFieldName = (error) => pipe(error, Option.liftPredicate(S.isSchemaError), Option.map(schemaError => schemaError.issue), Option.flatMap(issue => issue instanceof SchemaIssue.Composite
3
+ ? Array.head(issue.issues)
4
+ : Option.some(issue)), Option.flatMap(issue => issue instanceof SchemaIssue.Pointer
5
+ ? Array.head(issue.path)
6
+ : Option.none()), Option.filter(Predicate.isString));
7
+ /**
8
+ * Validates a parsed string record against its schema: names outside the
9
+ * schema and values the schema rejects both fail with an error the caller
10
+ * phrases, and a rejected value's message also receives the failing field's
11
+ * name when the schema issue carries one. One implementation serves island
12
+ * attributes and frontmatter fields, so unknown-name detection and
13
+ * decode-error wrapping cannot drift between the two.
14
+ */
15
+ export const validateFields = (config) => {
16
+ const allowedFieldNames = Object.keys(config.schema.fields);
17
+ const unknownFieldNames = Object.keys(config.values).filter(fieldName => !allowedFieldNames.includes(fieldName));
18
+ if (Array.isArrayNonEmpty(unknownFieldNames)) {
19
+ const allowedDescription = Array.match(allowedFieldNames, {
20
+ onEmpty: () => `It takes no ${config.memberNounPlural}.`,
21
+ onNonEmpty: names => `Allowed ${config.memberNounPlural}: ${names.join(', ')}.`,
22
+ });
23
+ throw new Error(config.unknownField(Array.headNonEmpty(unknownFieldNames), allowedDescription));
24
+ }
25
+ try {
26
+ S.decodeUnknownSync(config.schema)(config.values);
27
+ }
28
+ catch (error) {
29
+ throw new Error(config.invalidValues(error instanceof Error ? error.message : String(error), maybeInvalidFieldName(error)));
30
+ }
31
+ };
@@ -1,19 +1,41 @@
1
+ import { Option } from 'effect';
1
2
  import type { Plugin } from 'vite';
2
3
  import { MarkdownDocument } from '../ast/index.js';
4
+ import type { FrontmatterDefinition } from './frontmatter.js';
3
5
  import type { NormalizeOptions } from './normalize.js';
4
- /** Options for {@link markdown} and {@link parseMarkdown}. */
5
- export type MarkdownPluginOptions = NormalizeOptions;
6
+ /** Options for {@link markdown}, {@link parseMarkdown}, and {@link parseMarkdownWithFrontmatter}. */
7
+ export type MarkdownPluginOptions = NormalizeOptions & Readonly<{
8
+ frontmatter?: FrontmatterDefinition | undefined;
9
+ }>;
10
+ /** A parsed document together with its frontmatter fields, when it has any. */
11
+ export type ParsedMarkdown = Readonly<{
12
+ document: MarkdownDocument;
13
+ maybeFrontmatter: Option.Option<Readonly<Record<string, string>>>;
14
+ }>;
6
15
  /**
7
16
  * Parses markdown source into a typed {@link MarkdownDocument}. Throws on any
8
17
  * construct outside the markdown vocabulary, and on malformed options. The
9
18
  * {@link markdown} plugin runs this per `.md` module; call it directly for
10
- * one-off compilation in scripts.
19
+ * one-off compilation in scripts. Frontmatter, when enabled via the
20
+ * `frontmatter` option, is validated and dropped; use
21
+ * {@link parseMarkdownWithFrontmatter} to read it.
11
22
  */
12
23
  export declare const parseMarkdown: (source: string, options?: MarkdownPluginOptions) => MarkdownDocument;
24
+ /**
25
+ * Like {@link parseMarkdown}, but also returns the document's frontmatter
26
+ * fields when the `frontmatter` option is set and the source carries a
27
+ * frontmatter block. The fields arrive as the raw strings the block declares,
28
+ * already validated against the schema, so decoding them with the same schema
29
+ * cannot fail.
30
+ */
31
+ export declare const parseMarkdownWithFrontmatter: (source: string, options?: MarkdownPluginOptions) => ParsedMarkdown;
13
32
  /**
14
33
  * Vite plugin that compiles imported `.md` files at build time into typed
15
34
  * document modules. Decode the default export with `decodeDocument` and
16
- * render it with `Markdown.view`.
35
+ * render it with `Markdown.view`. Every module also carries a `frontmatter`
36
+ * named export: with a `frontmatter` schema configured, it holds the
37
+ * document's validated frontmatter fields, and it is `undefined` when the
38
+ * document has no frontmatter block.
17
39
  */
18
40
  export declare const markdown: (options?: MarkdownPluginOptions) => Plugin;
19
41
  //# sourceMappingURL=vite.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../../src/vite/vite.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAElC,OAAO,EAAE,gBAAgB,EAAkB,MAAM,iBAAiB,CAAA;AAElE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AAYtD,8DAA8D;AAC9D,MAAM,MAAM,qBAAqB,GAAG,gBAAgB,CAAA;AAwBpD;;;;;GAKG;AACH,eAAO,MAAM,aAAa,GACxB,QAAQ,MAAM,EACd,UAAS,qBAA0B,KAClC,gBACwE,CAAA;AAE3E;;;;GAIG;AACH,eAAO,MAAM,QAAQ,GAAI,UAAS,qBAA0B,KAAG,MAgB9D,CAAA"}
1
+ {"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../../src/vite/vite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,MAAM,EAAqB,MAAM,QAAQ,CAAA;AAOzD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAElC,OAAO,EAAE,gBAAgB,EAAkB,MAAM,iBAAiB,CAAA;AAClE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAA;AAM7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AActD,qGAAqG;AACrG,MAAM,MAAM,qBAAqB,GAAG,gBAAgB,GAClD,QAAQ,CAAC;IACP,WAAW,CAAC,EAAE,qBAAqB,GAAG,SAAS,CAAA;CAChD,CAAC,CAAA;AA0BJ,+EAA+E;AAC/E,MAAM,MAAM,cAAc,GAAG,QAAQ,CAAC;IACpC,QAAQ,EAAE,gBAAgB,CAAA;IAC1B,gBAAgB,EAAE,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAA;CAClE,CAAC,CAAA;AAwDF;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,GACxB,QAAQ,MAAM,EACd,UAAS,qBAA0B,KAClC,gBAEU,CAAA;AAEb;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,GACvC,QAAQ,MAAM,EACd,UAAS,qBAA0B,KAClC,cACwE,CAAA;AAE3E;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ,GAAI,UAAS,qBAA0B,KAAG,MA2B9D,CAAA"}
package/dist/vite/vite.js CHANGED
@@ -1,44 +1,90 @@
1
- import { Schema as S } from 'effect';
1
+ import { Array, Option, Schema as S, pipe } from 'effect';
2
2
  import remarkDirective from 'remark-directive';
3
3
  import remarkFrontmatter from 'remark-frontmatter';
4
4
  import remarkGfm from 'remark-gfm';
5
5
  import remarkParse from 'remark-parse';
6
6
  import { unified } from 'unified';
7
7
  import { encodeDocument } from '../ast/index.js';
8
+ import { parseFrontmatterFields, validateFrontmatterFields, } from './frontmatter.js';
8
9
  import { normalizeRoot } from './normalize.js';
9
10
  // NOTE: remark-frontmatter is included so that YAML frontmatter parses as one
10
- // `yaml` node and fails the build with guidance. Without it, remark reads
11
- // `---` fences as a thematic break plus a setext heading and renders garbage.
11
+ // `yaml` node. With a `frontmatter` schema configured it becomes the document's
12
+ // typed frontmatter; without one it fails the build with guidance. Without the
13
+ // remark plugin, remark reads `---` fences as a thematic break plus a setext
14
+ // heading and renders garbage.
12
15
  const processor = unified()
13
16
  .use(remarkParse)
14
17
  .use(remarkFrontmatter)
15
18
  .use(remarkGfm)
16
19
  .use(remarkDirective)
17
20
  .freeze();
21
+ const isSchemaStruct = (value) => S.isSchema(value) && 'fields' in value;
18
22
  const validateMarkdownPluginOptions = (options) => {
19
- const islands = options.islands;
20
- if (islands === undefined) {
21
- return options;
22
- }
23
- for (const [islandName, attributesSchema] of Object.entries(islands)) {
24
- if (!S.isSchema(attributesSchema) || !('fields' in attributesSchema)) {
25
- throw new Error(`Island "${islandName}" in markdown plugin options must map to a Schema struct describing its attributes.`);
23
+ const { islands, frontmatter } = options;
24
+ if (islands !== undefined) {
25
+ for (const [islandName, attributesSchema] of Object.entries(islands)) {
26
+ if (!isSchemaStruct(attributesSchema)) {
27
+ throw new Error(`Island "${islandName}" in markdown plugin options must map to a Schema struct describing its attributes.`);
28
+ }
26
29
  }
27
30
  }
31
+ if (frontmatter !== undefined && !isSchemaStruct(frontmatter)) {
32
+ throw new Error('The `frontmatter` markdown plugin option must be a Schema struct describing the frontmatter fields.');
33
+ }
28
34
  return options;
29
35
  };
30
- const parseWithValidatedOptions = (source, options) => normalizeRoot(processor.parse(source), options);
36
+ const isYamlNode = (node) => node.type === 'yaml';
37
+ const extractFrontmatter = (root, frontmatter) => {
38
+ if (frontmatter === undefined) {
39
+ return { contentRoot: root, maybeFrontmatter: Option.none() };
40
+ }
41
+ return pipe(root.children, Array.head, Option.filter(isYamlNode), Option.match({
42
+ onNone: () => ({
43
+ contentRoot: root,
44
+ maybeFrontmatter: Option.none(),
45
+ }),
46
+ onSome: (yamlNode) => {
47
+ const parsedFields = parseFrontmatterFields(yamlNode.value, yamlNode.position);
48
+ validateFrontmatterFields(frontmatter, parsedFields, yamlNode.position);
49
+ return {
50
+ contentRoot: { ...root, children: root.children.slice(1) },
51
+ maybeFrontmatter: Option.some(parsedFields.fields),
52
+ };
53
+ },
54
+ }));
55
+ };
56
+ const parseWithValidatedOptions = (source, options) => {
57
+ const { contentRoot, maybeFrontmatter } = extractFrontmatter(processor.parse(source), options.frontmatter);
58
+ return {
59
+ document: normalizeRoot(contentRoot, options),
60
+ maybeFrontmatter,
61
+ };
62
+ };
31
63
  /**
32
64
  * Parses markdown source into a typed {@link MarkdownDocument}. Throws on any
33
65
  * construct outside the markdown vocabulary, and on malformed options. The
34
66
  * {@link markdown} plugin runs this per `.md` module; call it directly for
35
- * one-off compilation in scripts.
67
+ * one-off compilation in scripts. Frontmatter, when enabled via the
68
+ * `frontmatter` option, is validated and dropped; use
69
+ * {@link parseMarkdownWithFrontmatter} to read it.
70
+ */
71
+ export const parseMarkdown = (source, options = {}) => parseWithValidatedOptions(source, validateMarkdownPluginOptions(options))
72
+ .document;
73
+ /**
74
+ * Like {@link parseMarkdown}, but also returns the document's frontmatter
75
+ * fields when the `frontmatter` option is set and the source carries a
76
+ * frontmatter block. The fields arrive as the raw strings the block declares,
77
+ * already validated against the schema, so decoding them with the same schema
78
+ * cannot fail.
36
79
  */
37
- export const parseMarkdown = (source, options = {}) => parseWithValidatedOptions(source, validateMarkdownPluginOptions(options));
80
+ export const parseMarkdownWithFrontmatter = (source, options = {}) => parseWithValidatedOptions(source, validateMarkdownPluginOptions(options));
38
81
  /**
39
82
  * Vite plugin that compiles imported `.md` files at build time into typed
40
83
  * document modules. Decode the default export with `decodeDocument` and
41
- * render it with `Markdown.view`.
84
+ * render it with `Markdown.view`. Every module also carries a `frontmatter`
85
+ * named export: with a `frontmatter` schema configured, it holds the
86
+ * document's validated frontmatter fields, and it is `undefined` when the
87
+ * document has no frontmatter block.
42
88
  */
43
89
  export const markdown = (options = {}) => {
44
90
  const validatedOptions = validateMarkdownPluginOptions(options);
@@ -48,9 +94,17 @@ export const markdown = (options = {}) => {
48
94
  if (!id.endsWith('.md')) {
49
95
  return undefined;
50
96
  }
51
- const document = parseWithValidatedOptions(source, validatedOptions);
97
+ const { document, maybeFrontmatter } = parseWithValidatedOptions(source, validatedOptions);
98
+ const documentExport = `export default ${JSON.stringify(encodeDocument(document))}`;
99
+ // NOTE: `frontmatter` is emitted for every module, `undefined` when the
100
+ // document has no frontmatter block, so the export always exists and the
101
+ // `*.md` ambient declaration cannot promise an export a module lacks.
102
+ const frontmatterExport = Option.match(maybeFrontmatter, {
103
+ onNone: () => 'undefined',
104
+ onSome: fields => JSON.stringify(fields),
105
+ });
52
106
  return {
53
- code: `export default ${JSON.stringify(encodeDocument(document))}`,
107
+ code: `${documentExport}\nexport const frontmatter = ${frontmatterExport}`,
54
108
  map: null,
55
109
  };
56
110
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foldkit/markdown",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Write markdown files, get Foldkit views with live islands.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -25,7 +25,7 @@
25
25
  "content.d.ts"
26
26
  ],
27
27
  "peerDependencies": {
28
- "effect": "4.0.0-beta.102",
28
+ "effect": "4.0.0-beta.105",
29
29
  "foldkit": "^0",
30
30
  "vite": "^7.0.0 || ^8.0.0"
31
31
  },
@@ -39,14 +39,14 @@
39
39
  "devDependencies": {
40
40
  "@types/mdast": "^4.0.4",
41
41
  "@types/unist": "^3.0.3",
42
- "effect": "4.0.0-beta.102",
42
+ "effect": "4.0.0-beta.105",
43
43
  "happy-dom": "^20.10.4",
44
44
  "mdast-util-directive": "^3.1.0",
45
45
  "rimraf": "^6.1.3",
46
46
  "typescript": "^6.0.3",
47
47
  "vite": "^8.0.16",
48
48
  "vitest": "^4.1.9",
49
- "foldkit": "0.134.0"
49
+ "foldkit": "0.141.1"
50
50
  },
51
51
  "keywords": [
52
52
  "foldkit",