@mulmoclaude/markdown-utils 1.3.2 → 1.3.3

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.
@@ -8,6 +8,14 @@
8
8
  // invoke the wrapper from their own `@click` handler and check the
9
9
  // return value to decide whether to fall through to plugin-specific
10
10
  // navigation.
11
+ // `nodeType`, not `instanceof Element`: this package is also consumed outside a
12
+ // browser (the Node + jsdom test harness installs `window`/`document` but not
13
+ // the DOM constructors), where the bare `Element` global is a ReferenceError.
14
+ // nodeType 1 is the DOM standard's own element discriminant, and it stops at
15
+ // Element rather than HTMLElement because an inline <svg> inside a link is a
16
+ // legitimate click target that must still resolve to its anchor.
17
+ const ELEMENT_NODE = 1;
18
+ const isElementNode = (target) => target !== null && "nodeType" in target && target.nodeType === ELEMENT_NODE;
11
19
  // Pure predicate: is `href` an absolute http(s) URL pointing at an
12
20
  // origin different from `currentOrigin`? Used by
13
21
  // `handleExternalLinkClick` below, and directly by tests.
@@ -42,8 +50,8 @@ export function handleExternalLinkClick(event) {
42
50
  return false;
43
51
  if (event.ctrlKey || event.metaKey || event.shiftKey)
44
52
  return false;
45
- const target = event.target;
46
- if (!target)
53
+ const { target } = event;
54
+ if (!isElementNode(target))
47
55
  return false;
48
56
  const anchor = target.closest("a");
49
57
  if (!anchor)
@@ -203,8 +203,17 @@ export function transformResolvableUrlsInHtml(html, transform) {
203
203
  return tag.replace(ATTR_ITER_RE, (...captures) => replaceAttrIfResolvable(captures, resolvableAttrs ?? [], srcsetAttrs ?? [], transform));
204
204
  });
205
205
  }
206
+ // Named view of `ATTR_ITER_RE`'s capture list (group 4, the full quoted
207
+ // value, is captured for clarity and skipped here). Groups that never
208
+ // participate come back `undefined`; the three the regex always fills fall
209
+ // back to `""` so a non-string can only ever blank the attribute, never
210
+ // reach the rewriter as if it were text.
211
+ function readAttrCaptures(captures) {
212
+ const [full, leading, name, eqWithSpaces, , doubleQuoted, singleQuoted, bare] = captures.map((value) => (typeof value === "string" ? value : undefined));
213
+ return { full: full ?? "", leading: leading ?? "", name: name ?? "", eqWithSpaces, doubleQuoted, singleQuoted, bare };
214
+ }
206
215
  function replaceAttrIfResolvable(captures, resolvableAttrs, srcsetAttrs, transform) {
207
- const [full, leading, name, eqWithSpaces, , doubleQuoted, singleQuoted, bare] = captures;
216
+ const { full, leading, name, eqWithSpaces, doubleQuoted, singleQuoted, bare } = readAttrCaptures(captures);
208
217
  if (!eqWithSpaces)
209
218
  return full;
210
219
  const lowerName = name.toLowerCase();
@@ -89,6 +89,9 @@ function extractBracketedAlt(raw) {
89
89
  }
90
90
  return null;
91
91
  }
92
+ // Declares the four fields it reads rather than the whole `Tokens.Image`:
93
+ // `type === "image"` leaves marked's open `Tokens.Generic` member in the
94
+ // union, and every read below already tolerates a missing value.
92
95
  function rewriteImageToken(token, basePath) {
93
96
  const href = (token.href ?? "").trim();
94
97
  if (href === "" || shouldSkip(href))
@@ -147,15 +150,24 @@ export function rewriteImgSrcAttrsInHtml(html, basePath) {
147
150
  function isSkippable(token) {
148
151
  return token.type === "code" || token.type === "codespan";
149
152
  }
153
+ // `Tokens.Generic` — the open member of marked's `Token` union — requires
154
+ // only a string `type` and a string `raw`, so proving those two is enough
155
+ // to call a value a `Token`.
156
+ function isToken(value) {
157
+ if (typeof value !== "object" || value === null)
158
+ return false;
159
+ return "type" in value && typeof value.type === "string" && "raw" in value && typeof value.raw === "string";
160
+ }
161
+ function toTokenArray(value) {
162
+ if (!Array.isArray(value) || value.length === 0)
163
+ return null;
164
+ const candidates = value;
165
+ return candidates.every(isToken) ? candidates : null;
166
+ }
150
167
  function getContainerChildren(token) {
151
- const container = token;
152
- if (Array.isArray(container.tokens) && container.tokens.length > 0) {
153
- return container.tokens;
154
- }
155
- if (Array.isArray(container.items) && container.items.length > 0) {
156
- return container.items;
157
- }
158
- return null;
168
+ const tokens = "tokens" in token ? token.tokens : undefined;
169
+ const items = "items" in token ? token.items : undefined;
170
+ return toTokenArray(tokens) ?? toTokenArray(items);
159
171
  }
160
172
  // Render a container's children back into the output, preserving any
161
173
  // structural glue the parent carries outside the children's combined
@@ -163,7 +175,7 @@ function getContainerChildren(token) {
163
175
  // Returns true if the container was rendered via its children, false
164
176
  // if the caller should fall back to emitting the parent's raw.
165
177
  function renderContainerChildren(raw, children, basePath, out) {
166
- const joined = children.map((token) => token.raw ?? "").join("");
178
+ const joined = children.map((token) => token.raw).join("");
167
179
  if (joined === "")
168
180
  return false;
169
181
  const idx = raw.indexOf(joined);
@@ -201,13 +213,11 @@ function renderToken(token, basePath, out) {
201
213
  // Block / inline HTML — rewrite raw <img> tags inside before
202
214
  // emitting. Markdown image syntax (![alt](url)) is handled by the
203
215
  // image-token branch above; this branch covers the HTML-fallback
204
- // path (#1011 Stage A). Fall back to verbatim raw if `raw` is
205
- // unexpectedly missing — defensive against future marked changes.
206
- const raw = token.raw ?? "";
207
- out.push(rewriteImgSrcAttrsInHtml(raw, basePath));
216
+ // path (#1011 Stage A).
217
+ out.push(rewriteImgSrcAttrsInHtml(token.raw, basePath));
208
218
  return;
209
219
  }
210
- const raw = token.raw ?? "";
220
+ const { raw } = token;
211
221
  const children = getContainerChildren(token);
212
222
  if (children && renderContainerChildren(raw, children, basePath, out)) {
213
223
  return;
@@ -10,6 +10,7 @@
10
10
  // lists, multi-line strings, escaping) instead of the regex
11
11
  // approximation in the legacy `src/utils/format/frontmatter.ts`.
12
12
  import { FAILSAFE_SCHEMA, dump as yamlDump, load as yamlLoad } from "js-yaml";
13
+ import { isRecord } from "@mulmoclaude/common";
13
14
  const FRONTMATTER_OPEN = /^---\r?\n/;
14
15
  // `(?:^|\r?\n)` lets the closing fence sit at the very start of
15
16
  // `afterOpen` — needed for the empty-envelope case `---\n---\n`
@@ -116,7 +117,7 @@ function safeYamlLoad(text) {
116
117
  // plain objects — anything else is a malformed header.
117
118
  if (loaded === null || loaded === undefined)
118
119
  return {};
119
- if (typeof loaded !== "object" || Array.isArray(loaded))
120
+ if (!isRecord(loaded))
120
121
  return null;
121
122
  return loaded;
122
123
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mulmoclaude/markdown-utils",
3
- "version": "1.3.2",
3
+ "version": "1.3.3",
4
4
  "description": "Browser-safe markdown / image rendering utilities shared by the MulmoClaude host and the markdown plugin",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",