@entropicwarrior/sdoc 0.2.15 → 0.2.16

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.
@@ -384,8 +384,11 @@ Content of Section B.
384
384
  \`\{!text!\}\` | Warning marker (orange)
385
385
  \`\{-text-\}\` | Negative marker (red)
386
386
  \`\{~text~\}\` | Highlight (yellow)
387
+ \`#1a73e8\` | Hex color swatch (auto-readable text)
387
388
  }
388
389
 
390
+ Hex color codes (\`#rgb\`, \`#rgba\`, \`#rrggbb\`, \`#rrggbbaa\`) render as inline swatches — a rounded box filled with the color and labelled with the code, with the text color chosen automatically (black or white) for legibility. They are detected in normal text, list items, table cells, and headings, but not inside \`\\\`inline code\\\`\` (which stays literal). A bare \`#\` at the start of a line is a heading; escape with \`\\#\` to keep a literal hash.
391
+
389
392
  Links: \`[Link text](https://example.com)\` or \`[Other doc](./other-file.sdoc)\`. Relative paths resolve from the document's directory.
390
393
 
391
394
  Images: \`![Alt text](path/to/image.png)\`
@@ -719,12 +719,15 @@ Content of Section B.
719
719
  - Warning marker: `{!text!}` (orange highlight)
720
720
  - Negative marker: `{-text-}` (red highlight)
721
721
  - Highlight: `{~text~}` (yellow highlight)
722
+ - Hex color swatch: `#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa` (e.g. `#9aa0a8`) renders as a filled inline box labelled with the code
722
723
  - Citation reference: `[@key]` (numbered superscript link to citation entry)
723
724
  - Multiple citation references: `[@key1, @key2]` (each individually linked)
724
725
  }
725
726
 
726
727
  Inline math requires non-whitespace immediately after the opening `$` and before the closing `$`. A plain `$` followed by a digit (e.g. `$100`) does not trigger math mode because there is no closing `$`.
727
728
 
729
+ Hex color swatches are detected only when the `#` is at the start of the inline text or preceded by a non-alphanumeric character, the digit run is exactly 3, 4, 6, or 8 hexadecimal digits, and it is not immediately followed by another letter, digit, or underscore. The background is the literal color and the text color (black or white) is chosen by perceptual brightness (YIQ); for `#rgba`/`#rrggbbaa` the alpha channel is ignored when choosing the text color. Hex inside `` `inline code` `` stays literal, and a `#` at the start of a line is parsed as a heading (escape with `\#`).
730
+
728
731
  Citation references (`[@key]`) are parsed before link syntax. `[@key]` is always a citation reference even if followed by `(url)`. Use `\[@key]` for a literal `[@key]` in text.
729
732
  }
730
733
 
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.15",
5
+ "version": "0.2.16",
6
6
  "publisher": "entropicwarrior-msenfin",
7
7
  "license": "MIT",
8
8
  "repository": {
package/src/sdoc.js CHANGED
@@ -1414,6 +1414,21 @@ function parseInline(text) {
1414
1414
  }
1415
1415
  }
1416
1416
 
1417
+ // Hex color codes (#rgb, #rgba, #rrggbb, #rrggbbaa) render as swatches.
1418
+ // Require a non-word char before the # so we don't match inside identifiers,
1419
+ // and a non-word char after the digits so partial/over-long runs are ignored.
1420
+ if (ch === "#" && (i === 0 || !/[0-9A-Za-z]/.test(text[i - 1]))) {
1421
+ const m = /^#([0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{4}|[0-9a-fA-F]{3})(?![0-9A-Za-z_])/.exec(
1422
+ text.slice(i)
1423
+ );
1424
+ if (m) {
1425
+ flush();
1426
+ nodes.push({ type: "color_swatch", value: m[0] });
1427
+ i += m[0].length;
1428
+ continue;
1429
+ }
1430
+ }
1431
+
1417
1432
  if (ch === "{") {
1418
1433
  const mc = next;
1419
1434
  let mt = null;
@@ -1728,6 +1743,46 @@ function escapeAttr(value) {
1728
1743
  return escapeHtml(value).replace(/'/g, "'");
1729
1744
  }
1730
1745
 
1746
+ // Expand a #rgb/#rgba/#rrggbb/#rrggbbaa hex string to [r, g, b] (alpha ignored).
1747
+ function hexToRgb(hex) {
1748
+ let h = hex.replace(/^#/, "");
1749
+ if (h.length === 3 || h.length === 4) {
1750
+ h = h.slice(0, 3).split("").map((c) => c + c).join("");
1751
+ } else {
1752
+ h = h.slice(0, 6);
1753
+ }
1754
+ return [
1755
+ parseInt(h.slice(0, 2), 16),
1756
+ parseInt(h.slice(2, 4), 16),
1757
+ parseInt(h.slice(4, 6), 16),
1758
+ ];
1759
+ }
1760
+
1761
+ // Pick black or white text for legibility over the given background color
1762
+ // using the perceptual YIQ brightness threshold.
1763
+ function readableTextColor(hex) {
1764
+ const [r, g, b] = hexToRgb(hex);
1765
+ const yiq = (r * 299 + g * 587 + b * 114) / 1000;
1766
+ return yiq >= 128 ? "#000000" : "#ffffff";
1767
+ }
1768
+
1769
+ // Render a hex color as a self-contained inline swatch: a rounded box filled
1770
+ // with the color, labelled with the hex code in a legible text color. Styling
1771
+ // is inline so it survives in slides, exports, and other CSS-free contexts.
1772
+ function colorSwatchHtml(value) {
1773
+ const fg = readableTextColor(value);
1774
+ // A soft neutral-grey outline on every swatch (the same for all of them) so
1775
+ // the box edge stays visible even when the fill matches the page background.
1776
+ // Semi-transparent grey reads on both light and dark backgrounds without the
1777
+ // harsh contrast of a black/white border.
1778
+ const style =
1779
+ `background-color:${value};color:${fg};` +
1780
+ "border:1px solid rgba(128,128,128,0.5);border-radius:4px;padding:0.15em 0.9em;" +
1781
+ "font-family:'JetBrains Mono','Fira Code','Source Code Pro',monospace;font-size:0.95em;" +
1782
+ "-webkit-print-color-adjust:exact;print-color-adjust:exact";
1783
+ return `<span class="sdoc-color-swatch" style="${style}">${escapeHtml(value)}</span>`;
1784
+ }
1785
+
1731
1786
  function renderInline(text) {
1732
1787
  const nodes = parseInline(text);
1733
1788
  return renderInlineNodes(nodes);
@@ -1766,6 +1821,8 @@ function renderInlineNodes(nodes) {
1766
1821
  }
1767
1822
  case "code":
1768
1823
  return `<code class="sdoc-inline-code">${escapeHtml(node.value)}</code>`;
1824
+ case "color_swatch":
1825
+ return colorSwatchHtml(node.value);
1769
1826
  case "em":
1770
1827
  return `<em>${renderInlineNodes(node.children)}</em>`;
1771
1828
  case "strong":
@@ -3773,5 +3830,7 @@ module.exports = {
3773
3830
  renderKatex,
3774
3831
  escapeHtml,
3775
3832
  escapeAttr,
3776
- sanitizeSvg
3833
+ sanitizeSvg,
3834
+ colorSwatchHtml,
3835
+ readableTextColor
3777
3836
  };
@@ -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, sanitizeSvg } = require("./sdoc");
10
+ const { parseInline, renderKatex, escapeHtml, escapeAttr, sanitizeSvg, colorSwatchHtml } = require("./sdoc");
11
11
 
12
12
  // ---------------------------------------------------------------------------
13
13
  // Inline rendering — produces clean HTML without sdoc-* classes
@@ -21,6 +21,8 @@ function renderInlineNodes(nodes) {
21
21
  return escapeHtml(node.value);
22
22
  case "code":
23
23
  return `<code>${escapeHtml(node.value)}</code>`;
24
+ case "color_swatch":
25
+ return colorSwatchHtml(node.value);
24
26
  case "em":
25
27
  return `<em>${renderInlineNodes(node.children)}</em>`;
26
28
  case "strong":