@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.
@@ -0,0 +1,57 @@
1
+ import { Point, Position } from "../MarkdownNode.js";
2
+ import { preprocessLines } from "./preprocess.js";
3
+ import { LineIndex } from "./lineIndex.js";
4
+ import { parseInlines } from "./inlineParser.js";
5
+ import { trimWithSegments } from "./rawInline.js";
6
+
7
+ //#region src/internal/phrasing.ts
8
+ const EMPTY_REFMAP = /* @__PURE__ */ new Map();
9
+ const EMPTY_FOOTNOTE_LABELS = /* @__PURE__ */ new Set();
10
+ /**
11
+ * Parse a text fragment as a single paragraph's inline content.
12
+ */
13
+ const parsePhrasingText = (text, dialect) => {
14
+ const lines = preprocessLines(text);
15
+ const segments = [];
16
+ let content = "";
17
+ for (const line of lines) {
18
+ segments.push({
19
+ textOffset: content.length,
20
+ sourceOffset: line.start,
21
+ length: line.text.length
22
+ });
23
+ content += `${line.text}\n`;
24
+ }
25
+ const trimmed = trimWithSegments(content, segments, /\s/);
26
+ if (trimmed.text.length === 0) return [];
27
+ const first = trimmed.segments[0];
28
+ const startOffset = first === void 0 ? 0 : first.sourceOffset;
29
+ const lineIndex = LineIndex.fromLineStarts(text, lines.map((line) => line.start));
30
+ const sourceLength = text.length;
31
+ const positionOf = (start, end) => {
32
+ const clampedStart = Math.min(Math.max(start, 0), sourceLength);
33
+ const clampedEnd = Math.min(Math.max(end, clampedStart), sourceLength);
34
+ const startPoint = lineIndex.positionAt(clampedStart);
35
+ const endPoint = lineIndex.positionAt(clampedEnd);
36
+ return Position.make({
37
+ start: Point.make({
38
+ line: startPoint.line,
39
+ column: startPoint.column,
40
+ offset: clampedStart
41
+ }),
42
+ end: Point.make({
43
+ line: endPoint.line,
44
+ column: endPoint.column,
45
+ offset: clampedEnd
46
+ })
47
+ });
48
+ };
49
+ return parseInlines({
50
+ text: trimmed.text,
51
+ startOffset,
52
+ segments: trimmed.segments
53
+ }, EMPTY_REFMAP, positionOf, dialect, EMPTY_FOOTNOTE_LABELS);
54
+ };
55
+
56
+ //#endregion
57
+ export { parsePhrasingText };
@@ -6,6 +6,8 @@ const reCmarkSpace = /[ \t\n\r]/;
6
6
  /**
7
7
  * Trim `text` the way commonmark.js does before inline parsing, carrying the
8
8
  * segment table along so the surviving characters keep their source offsets.
9
+ * Exported for the phrasing-level parse entry point (`phrasing.ts`), which
10
+ * prepares content the same way a paragraph does.
9
11
  */
10
12
  const trimWithSegments = (text, segments, whitespace) => {
11
13
  let start = 0;
@@ -54,4 +56,4 @@ const prepareInline = (block, position, refmap, dialect = "commonmark", footnote
54
56
  };
55
57
 
56
58
  //#endregion
57
- export { prepareInline };
59
+ export { prepareInline, trimWithSegments };
@@ -72,7 +72,7 @@ const isSchemeColon = (value, index) => {
72
72
  * Escape one text value into `out`, tracking line starts. Returns whether the
73
73
  * emission ended at a line start.
74
74
  */
75
- const escapeText = (value, context, atLineStart) => {
75
+ const escapeText = (value, context, atLineStart, mdx) => {
76
76
  let out = "";
77
77
  let lineStart = atLineStart;
78
78
  for (let index = 0; index < value.length; index += 1) {
@@ -125,6 +125,10 @@ const escapeText = (value, context, atLineStart) => {
125
125
  out += `\\${char}`;
126
126
  continue;
127
127
  }
128
+ if (mdx && char === "{") {
129
+ out += "\\{";
130
+ continue;
131
+ }
128
132
  if (context.inHeading && char === "#") {
129
133
  out += "\\#";
130
134
  continue;
@@ -214,7 +218,7 @@ const serializeInlines = (children, context, state, startsLine, junctionGuard) =
214
218
  };
215
219
  switch (child.type) {
216
220
  case "text": {
217
- const escaped = escapeText(child.value, context, atLineStart);
221
+ const escaped = escapeText(child.value, context, atLineStart, state.mdx);
218
222
  out += escaped.text;
219
223
  atLineStart = escaped.atLineStart;
220
224
  break;
@@ -291,12 +295,126 @@ const serializeInlines = (children, context, state, startsLine, junctionGuard) =
291
295
  case "footnoteReference":
292
296
  out += `[^${escapeLabel(child.label ?? child.identifier)}]`;
293
297
  atLineStart = false;
298
+ break;
299
+ case "mdxJsxTextElement":
300
+ out += serializeMdxJsxText(child, context, state);
301
+ atLineStart = false;
302
+ break;
303
+ case "mdxTextExpression":
304
+ out += mdxExpression(child.value);
305
+ atLineStart = false;
294
306
  }
295
307
  index += 1;
296
308
  unguard(state);
297
309
  }
298
310
  return out;
299
311
  };
312
+ /** The tag string of every MDX node type. */
313
+ const MDX_NODE_TYPES = /* @__PURE__ */ new Set([
314
+ "mdxJsxFlowElement",
315
+ "mdxJsxTextElement",
316
+ "mdxFlowExpression",
317
+ "mdxTextExpression",
318
+ "mdxjsEsm"
319
+ ]);
320
+ /** Whether the tree carries any MDX node — iterative, deliberately unguarded. */
321
+ const treeContainsMdx = (root) => {
322
+ const stack = [root];
323
+ while (stack.length > 0) {
324
+ const node = stack.pop();
325
+ if (MDX_NODE_TYPES.has(node.type)) return true;
326
+ if (node.children !== void 0) for (const child of node.children) stack.push(child);
327
+ }
328
+ return false;
329
+ };
330
+ /**
331
+ * The oracle's `indentLines` line model (mdast-util-to-markdown
332
+ * `lib/util/indent-lines.js`): lines split on every terminator spelling
333
+ * (`\r\n`, `\n`, bare `\r`), terminators preserved verbatim between the
334
+ * mapped lines, and `blank` meaning an empty line — so a blank CRLF line
335
+ * stays bare instead of having a retained `\r` treated as content.
336
+ */
337
+ const mapMdxLines = (value, map) => {
338
+ const eol = /\r?\n|\r/g;
339
+ const result = [];
340
+ let start = 0;
341
+ let index = 0;
342
+ for (let match = eol.exec(value); match !== null; match = eol.exec(value)) {
343
+ const line = value.slice(start, match.index);
344
+ result.push(map(line, index, line === ""), match[0]);
345
+ start = match.index + match[0].length;
346
+ index += 1;
347
+ }
348
+ const last = value.slice(start);
349
+ result.push(map(last, index, last === ""));
350
+ return result.join("");
351
+ };
352
+ /**
353
+ * Serialize an MDX expression body between braces. Continuation lines take
354
+ * the oracle's two-space indent; the first line and blank lines take none.
355
+ */
356
+ const mdxExpression = (value) => `{${mapMdxLines(value, (line, index, blank) => index === 0 || blank ? line : ` ${line}`)}}`;
357
+ /**
358
+ * Serialize one JSX attribute. A `null` or absent value is a boolean
359
+ * attribute; a string value is quoted with `"` (the oracle default), the
360
+ * quote itself escaped as `&#x22;` exactly as `stringify-entities` spells
361
+ * it; an expression value goes between braces.
362
+ */
363
+ const serializeMdxAttribute = (attribute) => {
364
+ if (attribute.type === "mdxJsxExpressionAttribute") return `{${attribute.value}}`;
365
+ const value = attribute.value;
366
+ if (value === void 0 || value === null) return attribute.name;
367
+ if (typeof value === "string") return `${attribute.name}="${value.replaceAll("\"", "&#x22;")}"`;
368
+ return `${attribute.name}={${value.value}}`;
369
+ };
370
+ /** Serialize a text (phrasing) JSX element. */
371
+ const serializeMdxJsxText = (node, context, state) => {
372
+ const attributes = node.attributes.map(serializeMdxAttribute);
373
+ const selfClosing = node.name !== null && node.children.length === 0;
374
+ let out = `<${node.name ?? ""}`;
375
+ if (attributes.length > 0) out += ` ${attributes.join(" ")}`;
376
+ if (selfClosing) return `${out} />`;
377
+ out += ">";
378
+ out += serializeInlines(node.children, context, state, false);
379
+ out += `</${node.name ?? ""}>`;
380
+ return out;
381
+ };
382
+ /** Prefix every line of a rendered child block; blank lines stay bare. */
383
+ const indentBlockLines = (content, indent) => mapMdxLines(content, (line, _index, blank) => blank ? line : `${indent}${line}`);
384
+ /**
385
+ * Serialize a flow (block) JSX element: opening tag (attributes on their own
386
+ * lines when one carries a line ending), children as indented block layout,
387
+ * closing tag — the oracle's `mdxElement` flow branch.
388
+ */
389
+ const serializeMdxJsxFlow = (node, state) => {
390
+ const currentIndent = " ".repeat(state.jsxDepth);
391
+ const attributes = node.attributes.map(serializeMdxAttribute);
392
+ const selfClosing = node.name !== null && node.children.length === 0;
393
+ const attributesOnOneLine = attributes.join(" ");
394
+ const attributesOnTheirOwnLine = /[\r\n]/.test(attributesOnOneLine);
395
+ let value = `${currentIndent}<${node.name ?? ""}`;
396
+ if (attributesOnTheirOwnLine) value += `\n${attributes.map((attribute) => `${currentIndent} ${attribute}`).join("\n")}\n${currentIndent}`;
397
+ else if (attributesOnOneLine !== "") value += ` ${attributesOnOneLine}`;
398
+ if (selfClosing) value += `${attributesOnTheirOwnLine ? "" : " "}/`;
399
+ value += ">";
400
+ if (node.children.length > 0) {
401
+ state.jsxDepth += 1;
402
+ const childIndent = " ".repeat(state.jsxDepth);
403
+ const parts = node.children.map((child) => {
404
+ if (child.type === "mdxJsxFlowElement") {
405
+ guard(state, child);
406
+ const rendered = serializeMdxJsxFlow(child, state);
407
+ unguard(state);
408
+ return rendered;
409
+ }
410
+ return indentBlockLines(serializeBlocks([child], state), childIndent);
411
+ });
412
+ state.jsxDepth -= 1;
413
+ value += `\n${parts.join("\n\n")}\n`;
414
+ }
415
+ if (!selfClosing) value += `${currentIndent}</${node.name ?? ""}>`;
416
+ return value;
417
+ };
300
418
  /** Prefix every line of `content`; blank lines take the trimmed prefix. */
301
419
  const prefixLines = (content, prefix, blankPrefix) => content.split("\n").map((line) => line === "" ? blankPrefix : `${prefix}${line}`).join("\n");
302
420
  /** First line gets `marker`, continuation lines get spaces of its width. */
@@ -381,7 +499,10 @@ const effectiveListMarker = (list, flipped) => {
381
499
  };
382
500
  const serializeListItem = (item, marker, state, tight) => {
383
501
  guard(state, item);
502
+ const savedJsxDepth = state.jsxDepth;
503
+ state.jsxDepth = 0;
384
504
  const inner = serializeBlocks(item.children, state, tight);
505
+ state.jsxDepth = savedJsxDepth;
385
506
  unguard(state);
386
507
  const content = `${item.checked === void 0 ? "" : item.checked ? "[x] " : "[ ] "}${inner}`;
387
508
  if (content === "") return marker.trimEnd();
@@ -455,7 +576,10 @@ const serializeBlocks = (children, state, tight = false) => {
455
576
  previousListMarker = void 0;
456
577
  break;
457
578
  case "blockquote": {
579
+ const savedJsxDepth = state.jsxDepth;
580
+ state.jsxDepth = 0;
458
581
  const inner = serializeBlocks(child.children, state);
582
+ state.jsxDepth = savedJsxDepth;
459
583
  parts.push(prefixLines(inner, "> ", ">"));
460
584
  previousListMarker = void 0;
461
585
  break;
@@ -485,6 +609,17 @@ const serializeBlocks = (children, state, tight = false) => {
485
609
  previousListMarker = void 0;
486
610
  break;
487
611
  }
612
+ case "mdxJsxFlowElement":
613
+ parts.push(serializeMdxJsxFlow(child, state));
614
+ previousListMarker = void 0;
615
+ break;
616
+ case "mdxFlowExpression":
617
+ parts.push(mdxExpression(child.value));
618
+ previousListMarker = void 0;
619
+ break;
620
+ case "mdxjsEsm":
621
+ parts.push(child.value);
622
+ previousListMarker = void 0;
488
623
  }
489
624
  previous = child;
490
625
  nodes.push(child);
@@ -510,7 +645,12 @@ const serializeBlocks = (children, state, tight = false) => {
510
645
  * empty for an empty root and ends with exactly one newline otherwise.
511
646
  */
512
647
  const stringifyTree = (root) => {
513
- const body = serializeBlocks(root.children, { depth: 0 });
648
+ const state = {
649
+ depth: 0,
650
+ jsxDepth: 0,
651
+ mdx: treeContainsMdx(root)
652
+ };
653
+ const body = serializeBlocks(root.children, state);
514
654
  return body === "" ? "" : `${body}\n`;
515
655
  };
516
656
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/markdown",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "private": false,
5
5
  "description": "CommonMark and GFM markdown parse, edit and transform schemas for Effect",
6
6
  "keywords": [
@@ -39,9 +39,9 @@
39
39
  "./package.json": "./package.json"
40
40
  },
41
41
  "peerDependencies": {
42
- "@effected/jsonc": "^0.7.0",
42
+ "@effected/jsonc": "^0.8.0",
43
43
  "@effected/toml": "^0.5.0",
44
- "@effected/yaml": "^0.11.0",
44
+ "@effected/yaml": "^0.12.0",
45
45
  "effect": "4.0.0-rc.109"
46
46
  },
47
47
  "peerDependenciesMeta": {