@entropicwarrior/sdoc 0.2.9 → 0.2.11

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.
@@ -421,7 +421,7 @@ Content of Section B.
421
421
  ```
422
422
  ````
423
423
 
424
- For security, \`<script>\` and \`<foreignObject>\` tags are stripped from the SVG content before rendering. All other SVG elements pass through as-is.
424
+ For security, the following are stripped before rendering: \`<script>\` tags, \`<foreignObject>\` tags, event-handler attributes (\`onload\`, \`onclick\`, etc.), and \`javascript:\` URLs. Content outside the \`<svg>\` root element is discarded. All other SVG elements and attributes pass through as-is.
425
425
  }
426
426
 
427
427
  # Math Blocks @math-blocks
@@ -608,9 +608,9 @@ Content of Section B.
608
608
  - Optional language tag after the opening fence
609
609
  - The special language tag `math` renders the block as a display equation via KaTeX (no copy button)
610
610
  - The special language tag `mermaid` renders the block as an SVG diagram via the Mermaid library
611
- - The special language tag `svg` renders the block as an inline SVG graphic. Script and foreignObject tags are stripped for security.
611
+ - The special language tag `svg` renders the block as an inline SVG graphic. Script tags, foreignObject tags, event-handler attributes, and javascript: URLs are stripped for security.
612
612
  - Fences may use more than three backticks (e.g. four or five) to nest code blocks that themselves contain triple backticks. The closing fence must match the opening fence length.
613
- - Code blocks display a copy-to-clipboard button (visible on hover) in all rendered HTML output
613
+ - Code blocks display a copy-to-clipboard button (visible on hover) in all rendered HTML output, except for `math`, `mermaid`, and `svg` blocks which render without it
614
614
  }
615
615
 
616
616
  # Include by Link @code-includes
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@entropicwarrior/sdoc",
3
3
  "displayName": "SDOC - Docs for Human/Agent Teams",
4
4
  "description": "A plain-text documentation format with explicit brace scoping — deterministic parsing, AI-agent efficiency, and 10-50x token savings vs Markdown.",
5
- "version": "0.2.9",
5
+ "version": "0.2.11",
6
6
  "publisher": "entropicwarrior-msenfin",
7
7
  "license": "MIT",
8
8
  "repository": {
package/src/sdoc.js CHANGED
@@ -1391,11 +1391,32 @@ function escapeHtml(value) {
1391
1391
  }
1392
1392
 
1393
1393
  function sanitizeSvg(svg) {
1394
- return svg
1395
- .replace(/<script[\s>][\s\S]*?<\/script\s*>/gi, "")
1396
- .replace(/<script\s*\/>/gi, "")
1397
- .replace(/<foreignObject[\s>][\s\S]*?<\/foreignObject\s*>/gi, "")
1398
- .replace(/<foreignObject\s*\/>/gi, "");
1394
+ if (typeof svg !== "string") return "";
1395
+
1396
+ // Enforce a single <svg> root — discard anything outside it.
1397
+ const openMatch = svg.match(/<svg[\s>]/i);
1398
+ const closeMatch = svg.match(/<\/svg\s*>/i);
1399
+ if (!openMatch || !closeMatch) return "";
1400
+ const start = svg.indexOf(openMatch[0]);
1401
+ const end = svg.indexOf(closeMatch[0], start) + closeMatch[0].length;
1402
+ let s = svg.slice(start, end);
1403
+
1404
+ // Strip <script> and <foreignObject> elements (including self-closing).
1405
+ s = s.replace(/<script[\s>][\s\S]*?<\/script\s*>/gi, "");
1406
+ s = s.replace(/<script\b[^>]*\/\s*>/gi, "");
1407
+ s = s.replace(/<foreignObject[\s>][\s\S]*?<\/foreignObject\s*>/gi, "");
1408
+ s = s.replace(/<foreignObject\b[^>]*\/\s*>/gi, "");
1409
+
1410
+ // Strip event-handler attributes (onload, onclick, etc.).
1411
+ s = s.replace(/\s+on[a-z]+\s*=\s*(".*?"|'.*?'|[^\s>]+)/gi, "");
1412
+
1413
+ // Neutralize javascript: URLs in href and xlink:href.
1414
+ s = s.replace(
1415
+ /(\s+(?:xlink:)?href\s*=\s*)(["'])(\s*javascript:)/gi,
1416
+ function (_match, prefix, quote) { return prefix + quote + "#"; }
1417
+ );
1418
+
1419
+ return s;
1399
1420
  }
1400
1421
 
1401
1422
  function escapeAttr(value) {
@@ -1474,6 +1495,8 @@ function renderInlineNodes(nodes) {
1474
1495
  function renderScope(scope, depth, isTitleScope = false) {
1475
1496
  // :comment scopes are not rendered
1476
1497
  if (scope.scopeType === "comment") return "";
1498
+ // @about is document metadata, not rendered in output
1499
+ if (scope.id && scope.id.toLowerCase() === "about") return "";
1477
1500
 
1478
1501
  const level = Math.min(6, Math.max(1, depth));
1479
1502
  const children = scope.children.map((child) => renderNode(child, depth + 1)).join("\n");
@@ -7,7 +7,7 @@
7
7
  // const { nodes, meta } = extractMeta(parsed.nodes);
8
8
  // const html = renderSlides(nodes, { meta, themeCss, themeJs });
9
9
 
10
- const { parseInline, renderKatex, escapeHtml, escapeAttr } = require("./sdoc");
10
+ const { parseInline, renderKatex, escapeHtml, escapeAttr, sanitizeSvg } = require("./sdoc");
11
11
 
12
12
  // ---------------------------------------------------------------------------
13
13
  // Inline rendering — produces clean HTML without sdoc-* classes
@@ -71,6 +71,9 @@ function renderNode(node) {
71
71
  if (node.lang === "mermaid") {
72
72
  return `<pre class="mermaid">${escapeHtml(node.text)}</pre>`;
73
73
  }
74
+ if (node.lang === "svg") {
75
+ return `<div class="sdoc-svg-block">${sanitizeSvg(node.text)}</div>`;
76
+ }
74
77
  if (node.lang === "math") {
75
78
  return `<div class="sdoc-math sdoc-math-block">${renderKatex(node.text, true)}</div>`;
76
79
  }