@effected/markdown 0.7.0 → 0.8.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.
@@ -1,3 +1,4 @@
1
+ import { ENTITY } from "./unescape.js";
1
2
  import { GuardExceeded } from "./carriers.js";
2
3
 
3
4
  //#region src/internal/stringify.ts
@@ -21,15 +22,15 @@ const ALWAYS_ESCAPE = /* @__PURE__ */ new Set([
21
22
  "\\",
22
23
  "`",
23
24
  "*",
24
- "_",
25
25
  "[",
26
26
  "]",
27
27
  "<",
28
- ">",
29
- "&",
30
28
  "~",
31
29
  "|"
32
30
  ]);
31
+ /** A character reference at the head of the string: the engine's own grammar. */
32
+ const reEntityAhead = new RegExp(`^${ENTITY}`, "i");
33
+ const reAlphanumeric = /[\p{L}\p{N}]/u;
33
34
  /** Line-start characters that could open a block construct. */
34
35
  const LINE_START_ESCAPE = /* @__PURE__ */ new Set([
35
36
  "#",
@@ -68,11 +69,37 @@ const isSchemeColon = (value, index) => {
68
69
  while (start > 0 && /[a-zA-Z]/.test(value[start - 1])) start -= 1;
69
70
  return SCHEME_WORDS.has(value.slice(start, index).toLowerCase());
70
71
  };
72
+ /** Whether `value[index]` is a Unicode letter or number (`undefined` is not). */
73
+ const isAlphanumericAt = (value, index) => {
74
+ const char = value[index];
75
+ return char !== void 0 && reAlphanumeric.test(char);
76
+ };
77
+ /**
78
+ * Whether the `#` at `value[index]` heads a run that would read as an ATX
79
+ * closing sequence: preceded by whitespace or the value start, and followed
80
+ * by nothing but whitespace to the end of the contiguous text (this value
81
+ * plus `followingText`). A non-text sibling after that may contribute only
82
+ * blank content, so it keeps the escape (conservative).
83
+ */
84
+ const isClosingHashRun = (value, index, followingText, nonTextFollows) => {
85
+ const before = value[index - 1];
86
+ if (before !== void 0 && before !== " " && before !== " ") return false;
87
+ const rest = value.slice(index) + followingText;
88
+ let end = 0;
89
+ while (rest[end] === "#") end += 1;
90
+ return nonTextFollows || /^[ \t\n]*$/.test(rest.slice(end));
91
+ };
92
+ /** A run that a later sibling could still complete into a character reference. */
93
+ const reEntityPrefix = /^&[#a-z0-9]*$/i;
71
94
  /**
72
95
  * Escape one text value into `out`, tracking line starts. Returns whether the
73
- * emission ended at a line start.
96
+ * emission ended at a line start. `followingText` is the concatenated value
97
+ * of the contiguous text siblings after this one (the parser never splits a
98
+ * text run, but a synthesized tree may), and `nonTextFollows` says whether a
99
+ * non-text inline comes after those; together they decide the value-end
100
+ * cases (`&`, and `#` in a heading) against what will actually be emitted.
74
101
  */
75
- const escapeText = (value, context, atLineStart, mdx) => {
102
+ const escapeText = (value, context, atLineStart, mdx, followingText, nonTextFollows) => {
76
103
  let out = "";
77
104
  let lineStart = atLineStart;
78
105
  for (let index = 0; index < value.length; index += 1) {
@@ -129,7 +156,17 @@ const escapeText = (value, context, atLineStart, mdx) => {
129
156
  out += "\\{";
130
157
  continue;
131
158
  }
132
- if (context.inHeading && char === "#") {
159
+ if (char === "_" && !(isAlphanumericAt(value, index - 1) && isAlphanumericAt(value, index + 1))) {
160
+ out += "\\_";
161
+ continue;
162
+ }
163
+ if (char === "&") {
164
+ const ahead = value.slice(index) + followingText;
165
+ const entityShaped = reEntityAhead.test(ahead) || nonTextFollows && reEntityPrefix.test(ahead);
166
+ out += entityShaped ? "\\&" : "&";
167
+ continue;
168
+ }
169
+ if (context.inHeading && char === "#" && isClosingHashRun(value, index, followingText, nonTextFollows)) {
133
170
  out += "\\#";
134
171
  continue;
135
172
  }
@@ -218,7 +255,13 @@ const serializeInlines = (children, context, state, startsLine, junctionGuard) =
218
255
  };
219
256
  switch (child.type) {
220
257
  case "text": {
221
- const escaped = escapeText(child.value, context, atLineStart, state.mdx);
258
+ let followingText = "";
259
+ let next = index + 1;
260
+ while (next < children.length && children[next]?.type === "text") {
261
+ followingText += children[next].value;
262
+ next += 1;
263
+ }
264
+ const escaped = escapeText(child.value, context, atLineStart, state.mdx, followingText, next < children.length);
222
265
  out += escaped.text;
223
266
  atLineStart = escaped.atLineStart;
224
267
  break;
@@ -443,8 +486,9 @@ const setextUnderline = (heading, content) => {
443
486
  return (heading.depth === 1 ? "=" : "-").repeat(Math.max(1, lastLine.length));
444
487
  };
445
488
  const serializeHeading = (heading, state) => {
446
- const content = serializeInlines(heading.children, HEADING_CONTEXT, state, false);
447
- if (heading.headingStyle === "setext" && heading.depth <= 2 && content !== "") return `${content}\n${setextUnderline(heading, content)}`;
489
+ const setext = heading.headingStyle === "setext" && heading.depth <= 2;
490
+ const content = serializeInlines(heading.children, HEADING_CONTEXT, state, setext);
491
+ if (setext && content !== "") return `${content}\n${setextUnderline(heading, content)}`;
448
492
  const hashes = "#".repeat(heading.depth);
449
493
  return content === "" ? hashes : `${hashes} ${content}`;
450
494
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/markdown",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "private": false,
5
5
  "description": "CommonMark and GFM markdown parse, edit and transform schemas for Effect",
6
6
  "keywords": [
@@ -39,7 +39,7 @@
39
39
  "./package.json": "./package.json"
40
40
  },
41
41
  "peerDependencies": {
42
- "@effected/jsonc": "^0.8.0",
42
+ "@effected/jsonc": "^0.8.1",
43
43
  "@effected/toml": "^0.5.0",
44
44
  "@effected/yaml": "^0.12.0",
45
45
  "effect": "4.0.0-rc.109"