@effected/markdown 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/Frontmatter.js +272 -0
  2. package/FrontmatterResolver.js +273 -0
  3. package/JsonFrontmatter.js +46 -0
  4. package/LICENSE +21 -0
  5. package/Markdown.js +251 -0
  6. package/MarkdownDiagnostic.js +85 -0
  7. package/MarkdownDocument.js +248 -0
  8. package/MarkdownEdit.js +52 -0
  9. package/MarkdownFormat.js +380 -0
  10. package/MarkdownNode.js +641 -0
  11. package/MarkdownVisitor.js +88 -0
  12. package/Mdast.js +384 -0
  13. package/README.md +255 -0
  14. package/TomlFrontmatter.js +44 -0
  15. package/YamlFrontmatter.js +45 -0
  16. package/index.d.ts +2190 -0
  17. package/index.js +15 -0
  18. package/internal/blockParser.js +419 -0
  19. package/internal/blockRegistry.js +90 -0
  20. package/internal/blockTypes.js +27 -0
  21. package/internal/blocks/atxHeading.js +54 -0
  22. package/internal/blocks/blockquote.js +40 -0
  23. package/internal/blocks/code.js +90 -0
  24. package/internal/blocks/document.js +17 -0
  25. package/internal/blocks/fencedCode.js +26 -0
  26. package/internal/blocks/footnoteDefinition.js +84 -0
  27. package/internal/blocks/frontmatter.js +56 -0
  28. package/internal/blocks/htmlBlock.js +72 -0
  29. package/internal/blocks/indentedCode.js +16 -0
  30. package/internal/blocks/linkReferenceDefinition.js +72 -0
  31. package/internal/blocks/list.js +159 -0
  32. package/internal/blocks/paragraph.js +27 -0
  33. package/internal/blocks/setextHeading.js +24 -0
  34. package/internal/blocks/table.js +378 -0
  35. package/internal/blocks/taskListItem.js +36 -0
  36. package/internal/blocks/thematicBreak.js +35 -0
  37. package/internal/carriers.js +42 -0
  38. package/internal/entities.js +26 -0
  39. package/internal/entityMap.js +7 -0
  40. package/internal/htmlTags.js +18 -0
  41. package/internal/inlineNode.js +54 -0
  42. package/internal/inlineParser.js +403 -0
  43. package/internal/inlineRegistry.js +68 -0
  44. package/internal/inlines/autolink.js +36 -0
  45. package/internal/inlines/autolinkLiteral.js +374 -0
  46. package/internal/inlines/codeSpan.js +32 -0
  47. package/internal/inlines/emphasis.js +79 -0
  48. package/internal/inlines/entity.js +21 -0
  49. package/internal/inlines/escape.js +34 -0
  50. package/internal/inlines/footnoteReference.js +63 -0
  51. package/internal/inlines/lineBreak.js +25 -0
  52. package/internal/inlines/link.js +212 -0
  53. package/internal/inlines/rawHtml.js +27 -0
  54. package/internal/inlines/strikethrough.js +81 -0
  55. package/internal/inlines/text.js +22 -0
  56. package/internal/lineIndex.js +76 -0
  57. package/internal/patterns.js +26 -0
  58. package/internal/preprocess.js +58 -0
  59. package/internal/rawInline.js +57 -0
  60. package/internal/references.js +160 -0
  61. package/internal/segments.js +36 -0
  62. package/internal/stringify.js +519 -0
  63. package/internal/unescape.js +18 -0
  64. package/package.json +61 -0
  65. package/tsdoc-metadata.json +11 -0
@@ -0,0 +1,88 @@
1
+ import { MarkdownDiagnostic } from "./MarkdownDiagnostic.js";
2
+ import { Data, Stream } from "effect";
3
+
4
+ //#region src/MarkdownVisitor.ts
5
+ /**
6
+ * Constructors and matchers for the `MarkdownVisitorEvent` union (e.g.
7
+ * `MarkdownVisitorEvent.Enter({ node, path, depth })`,
8
+ * `MarkdownVisitorEvent.$is("Exit")`).
9
+ *
10
+ * @public
11
+ */
12
+ const MarkdownVisitorEvent = Data.taggedEnum();
13
+ /**
14
+ * SAX-style markdown tree visitor statics. Not instantiable.
15
+ *
16
+ * @public
17
+ */
18
+ var MarkdownVisitor = class {
19
+ constructor() {}
20
+ /**
21
+ * Create a lazy `Stream` of `MarkdownVisitorEvent` walking `root` in
22
+ * document pre-order: `Enter` and `Exit` for every node including the
23
+ * root and every leaf. Events are produced on demand, so combining with
24
+ * `Stream.take` allows efficient partial scans without walking the rest
25
+ * of the tree.
26
+ *
27
+ * @remarks
28
+ * Infallible at the type level. A tree produced by `Markdown.parse` can
29
+ * never trip the walk's depth guard — the parser refuses deeper input —
30
+ * but a decoded foreign tree (`Mdast.fromMdast`) can; the trip surfaces
31
+ * as a terminal `MarkdownVisitorEvent` `Error` event carrying a
32
+ * `NestingDepthExceeded` {@link MarkdownDiagnostic}, mirroring the typed
33
+ * failure `Markdown.stringify` produces for the same tree.
34
+ */
35
+ static visit(root) {
36
+ return Stream.fromIterable(walkIterable(root));
37
+ }
38
+ };
39
+ /** Lazy per-subscription iterable so each run of the stream walks afresh. */
40
+ function walkIterable(root) {
41
+ return { [Symbol.iterator]() {
42
+ return walk(root)[Symbol.iterator]();
43
+ } };
44
+ }
45
+ function* walk(root) {
46
+ yield* walkNode(root, [], 0);
47
+ }
48
+ function* walkNode(node, path, depth) {
49
+ if (depth > 256) {
50
+ yield MarkdownVisitorEvent.Error({
51
+ diagnostic: MarkdownDiagnostic.make({
52
+ code: "NestingDepthExceeded",
53
+ message: `NestingDepthExceeded: limit ${256}, actual ${depth}`,
54
+ offset: node.position.start.offset,
55
+ length: Math.max(0, node.position.end.offset - node.position.start.offset),
56
+ line: Math.max(0, node.position.start.line - 1),
57
+ character: Math.max(0, node.position.start.column - 1)
58
+ }),
59
+ path,
60
+ depth
61
+ });
62
+ return;
63
+ }
64
+ yield MarkdownVisitorEvent.Enter({
65
+ node,
66
+ path,
67
+ depth
68
+ });
69
+ if ("children" in node) {
70
+ const children = node.children;
71
+ for (let index = 0; index < children.length; index++) {
72
+ const child = children[index];
73
+ const events = walkNode(child, [...path, index], depth + 1);
74
+ for (const event of events) {
75
+ yield event;
76
+ if (event._tag === "Error") return;
77
+ }
78
+ }
79
+ }
80
+ yield MarkdownVisitorEvent.Exit({
81
+ node,
82
+ path,
83
+ depth
84
+ });
85
+ }
86
+
87
+ //#endregion
88
+ export { MarkdownVisitor, MarkdownVisitorEvent };
package/Mdast.js ADDED
@@ -0,0 +1,384 @@
1
+ import { Root } from "./MarkdownNode.js";
2
+ import { unescapeString } from "./internal/unescape.js";
3
+ import { Effect, Result, Schema } from "effect";
4
+
5
+ //#region src/Mdast.ts
6
+ /**
7
+ * The remark-ecosystem interop boundary: projection between this package's
8
+ * node classes and plain mdast JSON.
9
+ *
10
+ * @remarks
11
+ * `Mdast.toMdast` projects a parsed {@link Root} to plain spec-valid mdast
12
+ * objects — fidelity extras stripped, optional fields spelled the way
13
+ * `mdast-util-from-markdown` spells them (explicit `null`/`false` where the
14
+ * reference utility emits them), so the output deep-equals what the remark
15
+ * ecosystem produces and consumes. `Mdast.fromMdast` admits foreign plain
16
+ * mdast back into the package's Schema node classes, synthesizing zero-width
17
+ * sentinel positions where unist leaves them optional.
18
+ *
19
+ * The emission target is `mdast-util-from-markdown@2.0.3` (the vendored
20
+ * interop corpus pin): `list.ordered`/`start`/`spread` and
21
+ * `listItem.spread`/`checked` always explicit (`null` for unknown `start`/
22
+ * `checked`), `code.lang`/`meta` and every `title` explicit `null` when
23
+ * absent, `image.alt`/`imageReference.alt` always a string. Under unist's
24
+ * null-equals-absent convention for optional fields this stays spec-valid
25
+ * mdast; it is also byte-identical to the reference utility's output, which
26
+ * is what interop means operationally. GFM shapes follow the same convention
27
+ * from `mdast-util-gfm`; the frontmatter capture projects to mdast's
28
+ * `yaml`/`toml` literal nodes, and a `json` capture projects to a
29
+ * `json`-typed literal node per the `mdast-util-frontmatter` custom-preset
30
+ * convention (presets name the node type after the language).
31
+ */
32
+ /**
33
+ * Indicates that foreign mdast input failed to decode into the package's
34
+ * node classes.
35
+ *
36
+ * @remarks
37
+ * `issue` carries the **structured** schema failure — at runtime a
38
+ * `SchemaIssue.Issue` tree, reachable through `_tag` and nested `issues` —
39
+ * never a stringified rendering (the `FrontmatterValidationError`
40
+ * precedent). It is typed `unknown` because v4 exposes no `Schema` for
41
+ * `Issue`; narrow it with the `SchemaIssue` module.
42
+ *
43
+ * @public
44
+ */
45
+ var MdastDecodeError = class extends Schema.TaggedErrorClass()("MdastDecodeError", {
46
+ /** The structured schema issue. Never a string. */
47
+ issue: Schema.Defect() }) {
48
+ get message() {
49
+ return "mdast input failed to decode into markdown nodes";
50
+ }
51
+ };
52
+ /** The zero-width sentinel position synthesized for foreign nodes. */
53
+ const sentinelPosition = {
54
+ start: {
55
+ line: 1,
56
+ column: 1,
57
+ offset: 0
58
+ },
59
+ end: {
60
+ line: 1,
61
+ column: 1,
62
+ offset: 0
63
+ }
64
+ };
65
+ const projectPosition = (position) => ({
66
+ start: {
67
+ line: position.start.line,
68
+ column: position.start.column,
69
+ offset: position.start.offset
70
+ },
71
+ end: {
72
+ line: position.end.line,
73
+ column: position.end.column,
74
+ offset: position.end.offset
75
+ }
76
+ });
77
+ const projectChildren = (children) => children.map((child) => projectNode(child));
78
+ const stripFinalLineEnding = (value) => value.endsWith("\r\n") ? value.slice(0, -2) : value.endsWith("\n") ? value.slice(0, -1) : value;
79
+ const projectLabel = (label) => label === void 0 ? {} : { label: unescapeString(label) };
80
+ const listSpread = (node) => {
81
+ for (let index = 0; index + 1 < node.children.length; index += 1) {
82
+ const current = node.children[index];
83
+ const next = node.children[index + 1];
84
+ if (current !== void 0 && next !== void 0 && next.position.start.line - current.position.end.line >= 2) return true;
85
+ }
86
+ return false;
87
+ };
88
+ const projectNode = (node) => {
89
+ const position = projectPosition(node.position);
90
+ switch (node.type) {
91
+ case "root": return {
92
+ type: "root",
93
+ children: projectChildren(node.children),
94
+ position
95
+ };
96
+ case "paragraph":
97
+ case "blockquote":
98
+ case "emphasis":
99
+ case "strong":
100
+ case "delete":
101
+ case "tableRow":
102
+ case "tableCell": return {
103
+ type: node.type,
104
+ children: projectChildren(node.children),
105
+ position
106
+ };
107
+ case "heading": return {
108
+ type: "heading",
109
+ depth: node.depth,
110
+ children: projectChildren(node.children),
111
+ position
112
+ };
113
+ case "text":
114
+ case "html":
115
+ case "inlineCode": return {
116
+ type: node.type,
117
+ value: node.value,
118
+ position
119
+ };
120
+ case "break":
121
+ case "thematicBreak": return {
122
+ type: node.type,
123
+ position
124
+ };
125
+ case "code": return {
126
+ type: "code",
127
+ lang: node.lang ?? null,
128
+ meta: node.meta ?? null,
129
+ value: stripFinalLineEnding(node.value),
130
+ position
131
+ };
132
+ case "link": return {
133
+ type: "link",
134
+ title: node.title ?? null,
135
+ url: node.url,
136
+ children: projectChildren(node.children),
137
+ position
138
+ };
139
+ case "image": return {
140
+ type: "image",
141
+ title: node.title ?? null,
142
+ url: node.url,
143
+ alt: node.alt ?? "",
144
+ position
145
+ };
146
+ case "linkReference": return {
147
+ type: "linkReference",
148
+ children: projectChildren(node.children),
149
+ position,
150
+ ...projectLabel(node.label),
151
+ identifier: node.identifier,
152
+ referenceType: node.referenceType
153
+ };
154
+ case "imageReference": return {
155
+ type: "imageReference",
156
+ alt: node.alt ?? "",
157
+ position,
158
+ ...projectLabel(node.label),
159
+ identifier: node.identifier,
160
+ referenceType: node.referenceType
161
+ };
162
+ case "definition": return {
163
+ type: "definition",
164
+ identifier: node.identifier,
165
+ ...projectLabel(node.label),
166
+ title: node.title ?? null,
167
+ url: node.url,
168
+ position
169
+ };
170
+ case "footnoteReference": return {
171
+ type: "footnoteReference",
172
+ identifier: node.identifier,
173
+ ...projectLabel(node.label),
174
+ position
175
+ };
176
+ case "footnoteDefinition": return {
177
+ type: "footnoteDefinition",
178
+ identifier: node.identifier,
179
+ ...projectLabel(node.label),
180
+ children: projectChildren(node.children),
181
+ position
182
+ };
183
+ case "list": return {
184
+ type: "list",
185
+ ordered: node.ordered ?? false,
186
+ start: node.start ?? null,
187
+ spread: listSpread(node),
188
+ children: projectChildren(node.children),
189
+ position
190
+ };
191
+ case "listItem": return {
192
+ type: "listItem",
193
+ spread: node.spread ?? false,
194
+ checked: node.checked ?? null,
195
+ children: projectChildren(node.children),
196
+ position
197
+ };
198
+ case "table": return {
199
+ type: "table",
200
+ align: node.align === void 0 ? null : [...node.align],
201
+ children: projectChildren(node.children),
202
+ position
203
+ };
204
+ case "frontmatter": return {
205
+ type: node.format,
206
+ value: node.value,
207
+ position
208
+ };
209
+ }
210
+ };
211
+ /** The frontmatter literal node types foreign mdast spells per format. */
212
+ const frontmatterTypes = /* @__PURE__ */ new Map([
213
+ ["yaml", "yaml"],
214
+ ["toml", "toml"],
215
+ ["json", "json"]
216
+ ]);
217
+ /**
218
+ * Fields admitted per foreign node type, beyond `type`/`position`/`children`.
219
+ * Unknown fields (`data`, custom extensions) are dropped at the boundary;
220
+ * `null` values on optional fields normalize to absence per unist's
221
+ * null-equals-absent convention.
222
+ */
223
+ const admittedFields = {
224
+ root: [],
225
+ paragraph: [],
226
+ blockquote: [],
227
+ emphasis: [],
228
+ strong: [],
229
+ delete: [],
230
+ tableRow: [],
231
+ tableCell: [],
232
+ break: [],
233
+ thematicBreak: [],
234
+ text: ["value"],
235
+ html: ["value"],
236
+ inlineCode: ["value"],
237
+ heading: ["depth"],
238
+ code: [
239
+ "value",
240
+ "lang",
241
+ "meta"
242
+ ],
243
+ link: ["url", "title"],
244
+ image: [
245
+ "url",
246
+ "title",
247
+ "alt"
248
+ ],
249
+ linkReference: [
250
+ "identifier",
251
+ "label",
252
+ "referenceType"
253
+ ],
254
+ imageReference: [
255
+ "identifier",
256
+ "label",
257
+ "referenceType",
258
+ "alt"
259
+ ],
260
+ definition: [
261
+ "identifier",
262
+ "label",
263
+ "url",
264
+ "title"
265
+ ],
266
+ footnoteReference: ["identifier", "label"],
267
+ footnoteDefinition: ["identifier", "label"],
268
+ list: [
269
+ "ordered",
270
+ "start",
271
+ "spread"
272
+ ],
273
+ listItem: ["spread", "checked"],
274
+ table: ["align"]
275
+ };
276
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
277
+ const completePoint = (value) => isRecord(value) && typeof value.line === "number" && typeof value.column === "number" && typeof value.offset === "number";
278
+ const normalizePosition = (value) => {
279
+ if (isRecord(value) && completePoint(value.start) && completePoint(value.end)) {
280
+ const start = value.start;
281
+ const end = value.end;
282
+ return {
283
+ start: {
284
+ line: start.line,
285
+ column: start.column,
286
+ offset: start.offset
287
+ },
288
+ end: {
289
+ line: end.line,
290
+ column: end.column,
291
+ offset: end.offset
292
+ }
293
+ };
294
+ }
295
+ return sentinelPosition;
296
+ };
297
+ const normalizeNode = (value) => {
298
+ if (!isRecord(value) || typeof value.type !== "string") return value;
299
+ const type = value.type;
300
+ const format = frontmatterTypes.get(type);
301
+ if (format !== void 0) return {
302
+ type: "frontmatter",
303
+ format,
304
+ value: value.value,
305
+ position: normalizePosition(value.position)
306
+ };
307
+ const admitted = admittedFields[type];
308
+ if (admitted === void 0) return value;
309
+ const normalized = { type };
310
+ for (const field of admitted) {
311
+ const raw = value[field];
312
+ if (raw !== void 0 && raw !== null) normalized[field] = raw;
313
+ }
314
+ if (type === "code" && typeof normalized.value === "string" && normalized.value !== "") {
315
+ const code = normalized.value;
316
+ normalized.value = code.endsWith("\n") ? code : `${code}\n`;
317
+ }
318
+ if (Array.isArray(value.children)) normalized.children = value.children.map(normalizeNode);
319
+ normalized.position = normalizePosition(value.position);
320
+ return normalized;
321
+ };
322
+ const decodeRoot = Schema.decodeUnknownResult(Root);
323
+ /**
324
+ * The mdast projection facade — the remark-ecosystem interop boundary.
325
+ *
326
+ * @public
327
+ */
328
+ var Mdast = class Mdast {
329
+ /**
330
+ * Project a parsed {@link Root} to plain mdast JSON.
331
+ *
332
+ * @remarks
333
+ * Total and pure: every tree the parser or {@link Mdast.fromMdast}
334
+ * produces projects without failure. Fidelity extras are stripped;
335
+ * optional mdast fields are spelled the way `mdast-util-from-markdown`
336
+ * spells them, so the output deep-equals the reference utility's trees
337
+ * (the vendored interop corpus pins this). The frontmatter capture
338
+ * projects to a `yaml`/`toml`/`json` literal node.
339
+ *
340
+ * @param root - The parsed document tree.
341
+ * @returns A plain mdast `root` object with unist positions.
342
+ */
343
+ static toMdast(root) {
344
+ return projectNode(root);
345
+ }
346
+ /**
347
+ * Decode foreign plain mdast into the package's node classes,
348
+ * synchronously, as a `Result`. The pure primitive twin of
349
+ * {@link Mdast.fromMdast}.
350
+ *
351
+ * @remarks
352
+ * unist makes positions optional and this package's classes require
353
+ * them, so missing or incomplete positions are synthesized as the
354
+ * zero-width sentinel (line 1, column 1, offset 0) — clearly synthetic
355
+ * and inert for rendering. Trees carrying sentinel positions serve
356
+ * tree-level workflows (stringify, the visitor, projection back out),
357
+ * not offset-splice editing, whose offsets must come from a real parse.
358
+ * `null` values on optional fields normalize to absence per unist's
359
+ * null-equals-absent convention; foreign `data` and other unrecognized
360
+ * fields are dropped at the boundary; `yaml`/`toml`/`json` literal nodes
361
+ * decode into the {@link Frontmatter} capture. Unknown node types fail
362
+ * typed.
363
+ *
364
+ * @param input - A plain mdast tree, typically a `root`.
365
+ * @returns A `Result` succeeding with the decoded {@link Root}, or
366
+ * failing with {@link MdastDecodeError} carrying the structured issue.
367
+ */
368
+ static fromMdastResult(input) {
369
+ return Result.mapError(decodeRoot(normalizeNode(input)), (error) => new MdastDecodeError({ issue: error.issue }));
370
+ }
371
+ /**
372
+ * Decode foreign plain mdast into the package's node classes. Defined in
373
+ * terms of {@link Mdast.fromMdastResult} — synchronous callers can use
374
+ * that variant directly.
375
+ *
376
+ * @param input - A plain mdast tree, typically a `root`.
377
+ * @returns An `Effect` that succeeds with the decoded {@link Root}, or
378
+ * fails with {@link MdastDecodeError}.
379
+ */
380
+ static fromMdast = Effect.fn("Mdast.fromMdast")((input) => Effect.fromResult(Mdast.fromMdastResult(input)));
381
+ };
382
+
383
+ //#endregion
384
+ export { Mdast, MdastDecodeError };