@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 +58 -1
- package/content.d.ts +1 -0
- package/dist/ast/ast.d.ts +14 -5
- package/dist/ast/ast.d.ts.map +1 -1
- package/dist/ast/ast.js +29 -2
- package/dist/vite/frontmatter.d.ts +32 -0
- package/dist/vite/frontmatter.d.ts.map +1 -0
- package/dist/vite/frontmatter.js +72 -0
- package/dist/vite/normalize.d.ts.map +1 -1
- package/dist/vite/normalize.js +11 -21
- package/dist/vite/public.d.ts +3 -2
- package/dist/vite/public.d.ts.map +1 -1
- package/dist/vite/public.js +1 -1
- package/dist/vite/validateFields.d.ts +25 -0
- package/dist/vite/validateFields.d.ts.map +1 -0
- package/dist/vite/validateFields.js +31 -0
- package/dist/vite/vite.d.ts +26 -4
- package/dist/vite/vite.d.ts.map +1 -1
- package/dist/vite/vite.js +70 -16
- package/package.json +4 -4
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,
|
|
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
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: (
|
|
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?:
|
|
328
|
+
}, options?: SchemaAST.ParseOptions) => {
|
|
320
329
|
readonly blocks: readonly BlockEncoded[];
|
|
321
330
|
};
|
|
322
331
|
//# sourceMappingURL=ast.d.ts.map
|
package/dist/ast/ast.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../../src/ast/ast.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
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 =
|
|
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":"
|
|
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"}
|
package/dist/vite/normalize.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Array, Match as M, Option, Record as Record_
|
|
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
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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;
|
package/dist/vite/public.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/vite/public.js
CHANGED
|
@@ -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
|
+
};
|
package/dist/vite/vite.d.ts
CHANGED
|
@@ -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
|
|
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
|
package/dist/vite/vite.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../../src/vite/vite.ts"],"names":[],"mappings":"
|
|
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
|
|
11
|
-
//
|
|
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
|
|
20
|
-
if (islands
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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
|
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
49
|
+
"foldkit": "0.141.1"
|
|
50
50
|
},
|
|
51
51
|
"keywords": [
|
|
52
52
|
"foldkit",
|