@effected/markdown 0.7.0 → 0.8.1

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,22 +69,47 @@ 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) {
79
106
  const char = value[index];
80
107
  if (char === "\n") {
81
108
  const adjacent = value[index - 1] === "\n" || value[index + 1] === "\n";
82
- const atBoundary = index === 0 || index === value.length - 1;
83
109
  if (context.singleLine) {
84
110
  out += " ";
85
111
  lineStart = false;
86
- } else if (adjacent || atBoundary) {
112
+ } else if (adjacent || lineStart) {
87
113
  out += "&#10;";
88
114
  lineStart = false;
89
115
  } else {
@@ -129,7 +155,17 @@ const escapeText = (value, context, atLineStart, mdx) => {
129
155
  out += "\\{";
130
156
  continue;
131
157
  }
132
- if (context.inHeading && char === "#") {
158
+ if (char === "_" && !(isAlphanumericAt(value, index - 1) && isAlphanumericAt(value, index + 1))) {
159
+ out += "\\_";
160
+ continue;
161
+ }
162
+ if (char === "&") {
163
+ const ahead = value.slice(index) + followingText;
164
+ const entityShaped = reEntityAhead.test(ahead) || nonTextFollows && reEntityPrefix.test(ahead);
165
+ out += entityShaped ? "\\&" : "&";
166
+ continue;
167
+ }
168
+ if (context.inHeading && char === "#" && isClosingHashRun(value, index, followingText, nonTextFollows)) {
133
169
  out += "\\#";
134
170
  continue;
135
171
  }
@@ -218,7 +254,13 @@ const serializeInlines = (children, context, state, startsLine, junctionGuard) =
218
254
  };
219
255
  switch (child.type) {
220
256
  case "text": {
221
- const escaped = escapeText(child.value, context, atLineStart, state.mdx);
257
+ let followingText = "";
258
+ let next = index + 1;
259
+ while (next < children.length && children[next]?.type === "text") {
260
+ followingText += children[next].value;
261
+ next += 1;
262
+ }
263
+ const escaped = escapeText(child.value, context, atLineStart, state.mdx, followingText, next < children.length);
222
264
  out += escaped.text;
223
265
  atLineStart = escaped.atLineStart;
224
266
  break;
@@ -443,8 +485,9 @@ const setextUnderline = (heading, content) => {
443
485
  return (heading.depth === 1 ? "=" : "-").repeat(Math.max(1, lastLine.length));
444
486
  };
445
487
  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)}`;
488
+ const setext = heading.headingStyle === "setext" && heading.depth <= 2;
489
+ const content = serializeInlines(heading.children, HEADING_CONTEXT, state, setext);
490
+ if (setext && content !== "") return `${content}\n${setextUnderline(heading, content)}`;
448
491
  const hashes = "#".repeat(heading.depth);
449
492
  return content === "" ? hashes : `${hashes} ${content}`;
450
493
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/markdown",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
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"