@effected/markdown 0.6.2 → 0.7.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/FrontmatterSource.js +188 -0
- package/Markdown.js +146 -0
- package/MarkdownDocument.js +4 -3
- package/MarkdownNode.js +154 -3
- package/Mdast.js +74 -1
- package/README.md +65 -0
- package/index.d.ts +443 -8
- package/index.js +3 -2
- package/internal/blockParser.js +1 -1
- package/internal/blocks/frontmatter.js +47 -1
- package/internal/phrasing.js +57 -0
- package/internal/rawInline.js +3 -1
- package/internal/stringify.js +143 -3
- package/package.json +3 -3
package/Mdast.js
CHANGED
|
@@ -206,8 +206,40 @@ const projectNode = (node) => {
|
|
|
206
206
|
value: node.value,
|
|
207
207
|
position
|
|
208
208
|
};
|
|
209
|
+
case "mdxJsxFlowElement":
|
|
210
|
+
case "mdxJsxTextElement": return {
|
|
211
|
+
type: node.type,
|
|
212
|
+
name: node.name,
|
|
213
|
+
attributes: node.attributes.map((attribute) => projectMdxAttribute(attribute)),
|
|
214
|
+
children: projectChildren(node.children),
|
|
215
|
+
position
|
|
216
|
+
};
|
|
217
|
+
case "mdxFlowExpression":
|
|
218
|
+
case "mdxTextExpression":
|
|
219
|
+
case "mdxjsEsm": return {
|
|
220
|
+
type: node.type,
|
|
221
|
+
value: node.value,
|
|
222
|
+
position
|
|
223
|
+
};
|
|
209
224
|
}
|
|
210
225
|
};
|
|
226
|
+
const projectMdxAttribute = (attribute) => {
|
|
227
|
+
if (attribute.type === "mdxJsxExpressionAttribute") return {
|
|
228
|
+
type: "mdxJsxExpressionAttribute",
|
|
229
|
+
value: attribute.value,
|
|
230
|
+
position: projectPosition(attribute.position)
|
|
231
|
+
};
|
|
232
|
+
const value = attribute.value;
|
|
233
|
+
return {
|
|
234
|
+
type: "mdxJsxAttribute",
|
|
235
|
+
name: attribute.name,
|
|
236
|
+
value: value === void 0 || value === null ? null : typeof value === "string" ? value : {
|
|
237
|
+
type: "mdxJsxAttributeValueExpression",
|
|
238
|
+
value: value.value
|
|
239
|
+
},
|
|
240
|
+
position: projectPosition(attribute.position)
|
|
241
|
+
};
|
|
242
|
+
};
|
|
211
243
|
/** The frontmatter literal node types foreign mdast spells per format. */
|
|
212
244
|
const frontmatterTypes = /* @__PURE__ */ new Map([
|
|
213
245
|
["yaml", "yaml"],
|
|
@@ -271,7 +303,10 @@ const admittedFields = {
|
|
|
271
303
|
"spread"
|
|
272
304
|
],
|
|
273
305
|
listItem: ["spread", "checked"],
|
|
274
|
-
table: ["align"]
|
|
306
|
+
table: ["align"],
|
|
307
|
+
mdxFlowExpression: ["value"],
|
|
308
|
+
mdxTextExpression: ["value"],
|
|
309
|
+
mdxjsEsm: ["value"]
|
|
275
310
|
};
|
|
276
311
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
277
312
|
const completePoint = (value) => isRecord(value) && typeof value.line === "number" && typeof value.column === "number" && typeof value.offset === "number";
|
|
@@ -304,6 +339,13 @@ const normalizeNode = (value) => {
|
|
|
304
339
|
value: value.value,
|
|
305
340
|
position: normalizePosition(value.position)
|
|
306
341
|
};
|
|
342
|
+
if (type === "mdxJsxFlowElement" || type === "mdxJsxTextElement") return {
|
|
343
|
+
type,
|
|
344
|
+
name: value.name,
|
|
345
|
+
attributes: Array.isArray(value.attributes) ? value.attributes.map(normalizeMdxAttribute) : value.attributes,
|
|
346
|
+
children: Array.isArray(value.children) ? value.children.map(normalizeNode) : value.children,
|
|
347
|
+
position: normalizePosition(value.position)
|
|
348
|
+
};
|
|
307
349
|
const admitted = admittedFields[type];
|
|
308
350
|
if (admitted === void 0) return value;
|
|
309
351
|
const normalized = { type };
|
|
@@ -319,6 +361,29 @@ const normalizeNode = (value) => {
|
|
|
319
361
|
normalized.position = normalizePosition(value.position);
|
|
320
362
|
return normalized;
|
|
321
363
|
};
|
|
364
|
+
const normalizeMdxAttribute = (value) => {
|
|
365
|
+
if (!isRecord(value) || typeof value.type !== "string") return value;
|
|
366
|
+
if (value.type === "mdxJsxExpressionAttribute") return {
|
|
367
|
+
type: "mdxJsxExpressionAttribute",
|
|
368
|
+
value: value.value,
|
|
369
|
+
position: normalizePosition(value.position)
|
|
370
|
+
};
|
|
371
|
+
if (value.type === "mdxJsxAttribute") {
|
|
372
|
+
const raw = value.value;
|
|
373
|
+
const normalized = {
|
|
374
|
+
type: "mdxJsxAttribute",
|
|
375
|
+
name: value.name,
|
|
376
|
+
position: normalizePosition(value.position)
|
|
377
|
+
};
|
|
378
|
+
if (raw !== void 0) normalized.value = isRecord(raw) && raw.type === "mdxJsxAttributeValueExpression" ? {
|
|
379
|
+
type: "mdxJsxAttributeValueExpression",
|
|
380
|
+
value: raw.value,
|
|
381
|
+
position: normalizePosition(raw.position)
|
|
382
|
+
} : raw;
|
|
383
|
+
return normalized;
|
|
384
|
+
}
|
|
385
|
+
return value;
|
|
386
|
+
};
|
|
322
387
|
const decodeRoot = Schema.decodeUnknownResult(Root);
|
|
323
388
|
/**
|
|
324
389
|
* The mdast projection facade — the remark-ecosystem interop boundary.
|
|
@@ -361,6 +426,14 @@ var Mdast = class Mdast {
|
|
|
361
426
|
* decode into the {@link Frontmatter} capture. Unknown node types fail
|
|
362
427
|
* typed.
|
|
363
428
|
*
|
|
429
|
+
* **This package's fidelity fields are among the fields dropped**, because
|
|
430
|
+
* they are not spec mdast. A `fenceChar`, `headingStyle`, `markerChar` or
|
|
431
|
+
* `delimiter` set on the tree BEFORE admission is silently discarded, and
|
|
432
|
+
* the node then serializes with the canonical default — set them on the
|
|
433
|
+
* decoded nodes this returns instead. The drop is correct (the boundary
|
|
434
|
+
* admits spec mdast and nothing else) but it is silent, which is why it is
|
|
435
|
+
* called out here.
|
|
436
|
+
*
|
|
364
437
|
* @param input - A plain mdast tree, typically a `root`.
|
|
365
438
|
* @returns A `Result` succeeding with the decoded {@link Root}, or
|
|
366
439
|
* failing with {@link MdastDecodeError} carrying the structured issue.
|
package/README.md
CHANGED
|
@@ -174,6 +174,8 @@ The three codecs — `YamlFrontmatter` (`---`), `TomlFrontmatter` (`+++`) and `J
|
|
|
174
174
|
|
|
175
175
|
Frontmatter blocks can also describe their own schema. `SchemaResolver.classify` sorts a `$schema` value into a tagged union — `ByUrl`, `ByPath`, `Inline` and `ByName` — and `SchemaResolver.fromRegistry` resolves `ByName` declarations like `skill@2.1.0` against schemas you register. URLs, paths and inline documents are carried as data and never fetched: this is a pure package and it performs no IO.
|
|
176
176
|
|
|
177
|
+
When the body should not go through the CommonMark engine at all — an MDX page, a template, a snapshot-hash contract that needs byte-exact boundaries — `FrontmatterSource.split` runs the same closed fence grammar over the raw string and nothing else. It is total: it returns the block's format, the exact bytes between the fence lines and the body remainder (`body` is always exactly `source.slice(bodyOffset)`), with absence a representable result rather than an error. `FrontmatterSource.join` serializes the parts back, reproducing the original bytes for an unmodified round-trip. Two edges normalize instead of reproducing: a closing fence that ends the document without a line terminator gains one, and fence lines with mismatched terminators (say `---\r\n … ---\n`) re-emit both with the opening fence's. The value and body bytes survive verbatim in every case, and neither is ever parsed at this level — decoding stays the codec modules' business.
|
|
178
|
+
|
|
177
179
|
## Working with the tree
|
|
178
180
|
|
|
179
181
|
`MarkdownDocument` derives its navigation accessors from the tree, so they can never disagree with it. `links` collects every URL-bearing node and passes `url` through exactly as written — bundle-relative hrefs are never normalized:
|
|
@@ -261,11 +263,74 @@ if (Result.isSuccess(parsed)) {
|
|
|
261
263
|
|
|
262
264
|
`Mdast.fromMdast` goes the other way as a checked admission boundary: it validates a foreign tree and synthesizes zero-width sentinel positions where one is absent or incomplete. Trees admitted that way serve tree-level workflows and canonical `stringify`; offset-splice editing needs the real positions only a parse produces.
|
|
263
265
|
|
|
266
|
+
## MDX trees
|
|
267
|
+
|
|
268
|
+
The MDX node vocabulary — `MdxJsxFlowElement` and `MdxJsxTextElement` (with `MdxJsxAttribute`, `MdxJsxExpressionAttribute` and `MdxJsxAttributeValueExpression` as their attribute carriers), `MdxFlowExpression`, `MdxTextExpression` and `MdxjsEsm` — is shaped exactly to the `mdast-util-mdx` contracts and constructs like every other node class. The parser does not read MDX syntax; these nodes exist so a **synthesized** tree can carry JSX and serialize to valid MDX instead of joining strings:
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
import { Markdown, MdxJsxAttribute, MdxJsxAttributeValueExpression, MdxJsxFlowElement, Root } from "@effected/markdown";
|
|
272
|
+
import { Result } from "effect";
|
|
273
|
+
|
|
274
|
+
const page = Root.make({
|
|
275
|
+
children: [
|
|
276
|
+
MdxJsxFlowElement.make({
|
|
277
|
+
name: "ApiSignature",
|
|
278
|
+
attributes: [
|
|
279
|
+
MdxJsxAttribute.make({
|
|
280
|
+
name: "code",
|
|
281
|
+
value: MdxJsxAttributeValueExpression.make({ value: JSON.stringify({ lang: "ts" }) }),
|
|
282
|
+
}),
|
|
283
|
+
],
|
|
284
|
+
children: [],
|
|
285
|
+
}),
|
|
286
|
+
],
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const mdx = Markdown.stringifyResult(page);
|
|
290
|
+
if (Result.isSuccess(mdx)) {
|
|
291
|
+
console.log(mdx.success); // <ApiSignature code={{"lang":"ts"}} />
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Serialization matches `mdxJsxToMarkdown`'s defaults — `"` quotes, spaced self-closing `<a />`, `<></>` fragments, flow children indented two spaces per JSX ancestor, `{expr}` expressions, ESM values verbatim — and a tree containing any MDX node escapes `{` in text the way MDX requires, while a tree with none serializes byte-identically to the table below. The shapes that have no MDX spelling are unconstructible rather than serialization errors: a fragment cannot carry attributes and an attribute requires a non-empty name, refused at `make` and at decode.
|
|
296
|
+
|
|
297
|
+
## The canonical form is stable
|
|
298
|
+
|
|
299
|
+
`Markdown.stringify` takes no options. It emits one canonical form, that form is pinned by byte-level tests, the engine behind it is cross-checked against commonmark.js over the full CommonMark 0.31.2 corpus, and **changing any of it is a breaking change to this package rather than a patch**. So a test may assert on these bytes, and a pipeline that needs stable rendered markdown should serialize through here instead of a third-party stringifier whose defaults are free to move between releases.
|
|
300
|
+
|
|
301
|
+
For a node carrying no fidelity field:
|
|
302
|
+
|
|
303
|
+
| Construct | Canonical form |
|
|
304
|
+
| --- | --- |
|
|
305
|
+
| Heading | ATX (`## x`), at every depth |
|
|
306
|
+
| Thematic break | `***` |
|
|
307
|
+
| Bullet list marker | `-` |
|
|
308
|
+
| Ordered list delimiter | `.`, flipping to `)` to separate an immediately adjacent sibling list |
|
|
309
|
+
| Emphasis / strong | `*` / `**` |
|
|
310
|
+
| Code block | fenced when the node carries a `lang` or a `fenceChar`, with the fence grown past any interior backtick run; otherwise indented — except where indenting would not re-parse as a code block, which forces a fence |
|
|
311
|
+
| Block separation | exactly one blank line |
|
|
312
|
+
| Document | a single trailing newline |
|
|
313
|
+
|
|
314
|
+
MDX nodes are part of the same commitment: the serialization choices listed under [MDX trees](#mdx-trees) are pinned the same way, and only trees that actually carry an MDX node pay the extra `{` escape.
|
|
315
|
+
|
|
316
|
+
**Representability wins over the table.** The canonical form never emits text that would re-parse as something else, so a row yields where the two conflict. The case that reaches a consumer is the indented code block: an indented block directly after a list is absorbed as list content, so a `Code` node with neither `lang` nor `fenceChar` emits fenced in that position and indented everywhere else. A byte-level assertion over synthesized code blocks therefore depends on the preceding sibling.
|
|
317
|
+
|
|
318
|
+
The posture that makes this a non-issue — and the one to adopt if you build trees and assert on their bytes — is to set `fenceChar` on every `Code` node carrying neither a `lang` nor one already, in a post-decode walk that reaches nested nodes. The choice then leaves the emitter entirely and no output depends on a node's neighbours.
|
|
319
|
+
|
|
320
|
+
Fidelity fields must be set on the **decoded** tree: `Mdast.fromMdast` admits spec mdast and strips everything outside it, so a `fenceChar` placed on a plain mdast tree before admission is silently dropped.
|
|
321
|
+
|
|
322
|
+
A node that carries a fidelity field overrides the matching row — `headingStyle`, `markerChar`, `fenceChar`, `delimiter` — which is how a parsed document re-serializes in its author's spelling. The table describes a synthesized node, which is what a test asserting on generated markdown actually holds.
|
|
323
|
+
|
|
324
|
+
To *normalize* an existing document to different choices, use `MarkdownFormat` with `MarkdownFormattingOptions`. That is the configurable surface; this one deliberately is not.
|
|
325
|
+
|
|
264
326
|
## Features
|
|
265
327
|
|
|
266
328
|
- `Markdown` — `parse`/`stringify` as `Effect`s with typed `MarkdownParseError`/`MarkdownStringifyError` channels, the pure `parseResult`/`stringifyResult` twins for synchronous callers, and the `MarkdownFromString` two-way codec.
|
|
329
|
+
- `Markdown.parsePhrasing`/`parsePhrasingResult` — parse a prose fragment as a single paragraph's inline content, without a full document parse and a paragraph splice: blank lines stay inline, references never form (no reference context), and positions are correct relative to the input string.
|
|
267
330
|
- `MarkdownDocument` — source, tree, diagnostics and the link-definition index, plus the derived `headings`, `sections` and `links` accessors, the `firstSection`/`sectionByHeading` finders over a `DocumentSection` (heading, depth, range, body), the `find`/`findAll` tree queries over type-narrowed selectors, and the `frontmatter` capture with `hasFrontmatterBlock` to tell an absent block from an uncaptured one.
|
|
268
331
|
- The mdast-shaped node classes — the CommonMark types plus GFM's `delete`, `table`, `tableRow`, `tableCell`, `footnoteDefinition`, `footnoteReference` and task-list `checked`, each carrying unist positions with byte offsets and this package's fidelity fields. `position` defaults to the zero-width synthetic sentinel, so a replacement fragment constructs in one line — `Text.make({ value: "shipped" })`.
|
|
332
|
+
- The MDX node vocabulary — `MdxJsxFlowElement`/`MdxJsxTextElement` with their attribute carriers, `MdxFlowExpression`/`MdxTextExpression` and `MdxjsEsm`, shaped to the `mdast-util-mdx` contracts for construction and serialization (the parser reads no MDX syntax).
|
|
333
|
+
- `FrontmatterSource` — string-level frontmatter `split`/`join` over the same closed fence grammar, byte-exact boundaries, no parsing of value or body; for bodies the CommonMark engine should not touch.
|
|
269
334
|
- `MarkdownEdit` / `MarkdownRange` (with `applyAll`) — the non-mutating text-edit vocabulary, field-identical to `@effected/jsonc`'s, `@effected/yaml`'s and `@effected/toml`'s.
|
|
270
335
|
- `MarkdownFormat` — `format`/`formatToString` compute conservative marker-normalization edits; `modify`/`modifyToString` replace a node by identity through the canonical stringifier.
|
|
271
336
|
- `MarkdownVisitor` — walk a parsed tree as a lazy `Stream` of `Enter`/`Exit` events with child-index paths and depth.
|