@entropicwarrior/sdoc 0.2.16 → 0.2.17

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.
@@ -333,6 +333,22 @@ Content of Section B.
333
333
 
334
334
  The \`Amount\` column is right-aligned and formatted as currency. The formula result and data cells all display as \`$1,000,000\` style. The \`**Total**\` label renders as bold text.
335
335
  }
336
+
337
+ # Copyable Columns
338
+ {
339
+ A \`copy\` token in the directive row gives every body cell in that column a click-to-copy icon (copying the cell's raw text). Ideal for columns of sequences, IDs, or commands.
340
+
341
+ ```
342
+ {[table]
343
+ Gene | Sequence
344
+ | copy
345
+ BRCA1 | ACGTACGTGGCA
346
+ TP53 | GGCATTACGATT
347
+ }
348
+ ```
349
+
350
+ \`copy\` can share a directive row with alignment/format directives on other columns.
351
+ }
336
352
  }
337
353
 
338
354
  # Table Formulas @table-formulas
@@ -385,10 +401,13 @@ Content of Section B.
385
401
  \`\{-text-\}\` | Negative marker (red)
386
402
  \`\{~text~\}\` | Highlight (yellow)
387
403
  \`#1a73e8\` | Hex color swatch (auto-readable text)
404
+ \`\{copy\}text\{/copy\}\` | Copyable text with click-to-copy icon
388
405
  }
389
406
 
390
407
  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
408
 
409
+ Copyable text: \`\{copy\}ACGTACGTGGCA\{/copy\}\` renders the inner text verbatim in monospace with a copy icon; clicking it copies the text. Great for DNA/protein sequences, IDs, hashes, and commands. Escape with \`\\\{copy\}\` for a literal. Copy icons show in HTML output (preview, exported HTML, web viewer) and are hidden when printed/exported to PDF.
410
+
392
411
  Links: \`[Link text](https://example.com)\` or \`[Other doc](./other-file.sdoc)\`. Relative paths resolve from the document's directory.
393
412
 
394
413
  Images: \`![Alt text](path/to/image.png)\`
@@ -435,9 +435,25 @@ Content of Section B.
435
435
  All numeric values and formula results in the `Amount` column display as currency (`$3,500,000`). The `**Total**` label renders normally as bold text.
436
436
  }
437
437
 
438
+ # Copyable Columns @column-copy
439
+ {
440
+ A `copy` token (case-insensitive) in the directive row marks a column copyable: every body cell in that column is rendered with a click-to-copy icon whose target is the cell's raw text. Useful for sequences, identifiers, hashes, and commands.
441
+
442
+ ```
443
+ {[table]
444
+ Gene | Sequence
445
+ | copy
446
+ BRCA1 | ACGTACGTGGCA
447
+ TP53 | GGCATTACGATT
448
+ }
449
+ ```
450
+
451
+ The `copy` token occupies a directive cell of its own and may coexist with alignment/format directives on other columns in the same row. The literal (unformatted) cell text is copied. Copy icons are interactive HTML only — they appear in preview, exported HTML, and the web viewer, and are hidden in printed/PDF output.
452
+ }
453
+
438
454
  # Directive Row Detection @directive-detection
439
455
  {
440
- A row is identified as a directive row when every non-empty cell contains only an alignment character (`<`, `>`, `=`), a format token, or both. It must be the first row after the header (or the first row in a headerless table), and at least one cell must contain a directive.
456
+ A row is identified as a directive row when every non-empty cell contains only an alignment character (`<`, `>`, `=`), a format token, the `copy` keyword, or a combination of alignment and format. It must be the first row after the header (or the first row in a headerless table), and at least one cell must contain a directive.
441
457
 
442
458
  For headerless tables, the directive row functions as a virtual header — it is consumed for its directives but no `<thead>` is rendered.
443
459
  }
@@ -720,6 +736,7 @@ Content of Section B.
720
736
  - Negative marker: `{-text-}` (red highlight)
721
737
  - Highlight: `{~text~}` (yellow highlight)
722
738
  - Hex color swatch: `#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa` (e.g. `#9aa0a8`) renders as a filled inline box labelled with the code
739
+ - Copyable text: `{copy}...{/copy}` renders the inner text verbatim (monospace) with a click-to-copy icon
723
740
  - Citation reference: `[@key]` (numbered superscript link to citation entry)
724
741
  - Multiple citation references: `[@key1, @key2]` (each individually linked)
725
742
  }
@@ -728,6 +745,8 @@ Content of Section B.
728
745
 
729
746
  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
747
 
748
+ Copyable text spans from `{copy}` to the next unescaped `{/copy}`. The inner text is captured verbatim — no nested inline formatting is applied — and is both displayed (monospace) and used as the copy target. An unterminated `{copy}` (no closing `{/copy}`) falls through as literal text. Escape with `\{copy}` to write a literal. As with other `{`-delimited constructs, a `{copy}` at the very start of a line is subject to block-level parsing; use it mid-line or within a cell.
749
+
731
750
  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.
732
751
  }
733
752
 
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.16",
5
+ "version": "0.2.17",
6
6
  "publisher": "entropicwarrior-msenfin",
7
7
  "license": "MIT",
8
8
  "repository": {
package/src/sdoc.js CHANGED
@@ -72,6 +72,7 @@ function isDirectiveRow(cells) {
72
72
  for (const cell of cells) {
73
73
  const trimmed = cell.trim();
74
74
  if (trimmed === "") continue;
75
+ if (trimmed.toLowerCase() === "copy") { hasDirective = true; continue; }
75
76
  if (!pattern.test(trimmed)) return false;
76
77
  hasDirective = true;
77
78
  }
@@ -102,12 +103,23 @@ function parseFormatSpec(spec) {
102
103
  function parseDirectiveRow(cells) {
103
104
  const align = [];
104
105
  const format = [];
106
+ const copy = [];
105
107
  let hasAlign = false;
106
108
  let hasFormat = false;
109
+ let hasCopy = false;
107
110
  const fmtPattern = /(\$(?:\.\d+)?|,(?:\.\d+)?|\.\d+|%(?:\.\d+)?)$/;
108
111
 
109
112
  for (const cell of cells) {
110
113
  const trimmed = cell.trim();
114
+
115
+ if (trimmed.toLowerCase() === "copy") {
116
+ align.push(null);
117
+ format.push(null);
118
+ copy.push(true);
119
+ hasCopy = true;
120
+ continue;
121
+ }
122
+
111
123
  const alignMatch = trimmed.match(/^([<>=])/);
112
124
  const fmtMatch = trimmed.match(fmtPattern);
113
125
 
@@ -124,11 +136,13 @@ function parseDirectiveRow(cells) {
124
136
  hasFormat = true;
125
137
  }
126
138
  format.push(f);
139
+ copy.push(false);
127
140
  }
128
141
 
129
142
  return {
130
143
  align: hasAlign ? align : null,
131
144
  format: hasFormat ? format : null,
145
+ copy: hasCopy ? copy : null,
132
146
  };
133
147
  }
134
148
 
@@ -940,10 +954,12 @@ function parseTableBody(cursor, tableStartLine, options) {
940
954
  const directiveIndex = options.headerless ? 0 : 1;
941
955
  let columnAlign = null;
942
956
  let columnFormat = null;
957
+ let columnCopy = null;
943
958
  if (rows.length > directiveIndex && isDirectiveRow(rows[directiveIndex])) {
944
959
  const directives = parseDirectiveRow(rows[directiveIndex]);
945
960
  columnAlign = directives.align;
946
961
  columnFormat = directives.format;
962
+ columnCopy = directives.copy;
947
963
  rows.splice(directiveIndex, 1);
948
964
  }
949
965
 
@@ -960,6 +976,7 @@ function parseTableBody(cursor, tableStartLine, options) {
960
976
  if (hasOptions) tableNode.options = options;
961
977
  if (columnAlign) tableNode.columnAlign = columnAlign;
962
978
  if (columnFormat) tableNode.columnFormat = columnFormat;
979
+ if (columnCopy) tableNode.columnCopy = columnCopy;
963
980
  return tableNode;
964
981
  }
965
982
 
@@ -1429,6 +1446,18 @@ function parseInline(text) {
1429
1446
  }
1430
1447
  }
1431
1448
 
1449
+ // Copyable inline text: {copy}literal text{/copy}. The inner text is copied
1450
+ // verbatim (no nested formatting), rendered monospace with a copy icon.
1451
+ if (text.startsWith("{copy}", i)) {
1452
+ const end = findUnescaped(text, i + 6, "{/copy}");
1453
+ if (end !== -1) {
1454
+ flush();
1455
+ nodes.push({ type: "copyable", value: text.slice(i + 6, end) });
1456
+ i = end + 7; // length of "{/copy}"
1457
+ continue;
1458
+ }
1459
+ }
1460
+
1432
1461
  if (ch === "{") {
1433
1462
  const mc = next;
1434
1463
  let mt = null;
@@ -1821,6 +1850,8 @@ function renderInlineNodes(nodes) {
1821
1850
  }
1822
1851
  case "code":
1823
1852
  return `<code class="sdoc-inline-code">${escapeHtml(node.value)}</code>`;
1853
+ case "copyable":
1854
+ return `<span class="sdoc-copyable"><code class="sdoc-inline-code">${escapeHtml(node.value)}</code><button class="sdoc-copy-btn sdoc-copy-inline" data-copy="${escapeAttr(node.value)}" title="Copy">⧉</button></span>`;
1824
1855
  case "color_swatch":
1825
1856
  return colorSwatchHtml(node.value);
1826
1857
  case "em":
@@ -2304,6 +2335,14 @@ function renderTable(table) {
2304
2335
  const opts = table.options || {};
2305
2336
  const colAlign = table.columnAlign || [];
2306
2337
  const colFormat = table.columnFormat || [];
2338
+ const colCopy = table.columnCopy || [];
2339
+
2340
+ // Copy icon appended to body cells in a `copy` directive column. data-copy
2341
+ // carries the raw cell text so the shared copy handler copies it verbatim.
2342
+ function copyBtn(cell, c) {
2343
+ if (!colCopy[c]) return "";
2344
+ return ` <button class="sdoc-copy-btn sdoc-copy-inline" data-copy="${escapeAttr(cell.trim())}" title="Copy">⧉</button>`;
2345
+ }
2307
2346
  const classes = ["sdoc-table"];
2308
2347
  if (opts.borderless) classes.push("sdoc-table-borderless");
2309
2348
  if (opts.headerless) classes.push("sdoc-table-headerless");
@@ -2343,20 +2382,20 @@ function renderTable(table) {
2343
2382
  }
2344
2383
  const formula = escapeAttr(cell.trim());
2345
2384
  if (result.error) {
2346
- return `<td class="sdoc-table-td sdoc-formula-error"${style} title="${formula}">${display}</td>`;
2385
+ return `<td class="sdoc-table-td sdoc-formula-error"${style} title="${formula}">${display}${copyBtn(cell, c)}</td>`;
2347
2386
  }
2348
- return `<td class="sdoc-table-td sdoc-formula-cell"${style} title="${formula}">${display}</td>`;
2387
+ return `<td class="sdoc-table-td sdoc-formula-cell"${style} title="${formula}">${display}${copyBtn(cell, c)}</td>`;
2349
2388
  }
2350
2389
 
2351
2390
  // Apply column format to numeric data cells
2352
2391
  if (fmt) {
2353
2392
  const parsed = parseCellValue(cell);
2354
2393
  if (!isNaN(parsed.value)) {
2355
- return `<td class="sdoc-table-td"${style}>${escapeHtml(formatNumber(parsed.value, fmt))}</td>`;
2394
+ return `<td class="sdoc-table-td"${style}>${escapeHtml(formatNumber(parsed.value, fmt))}${copyBtn(cell, c)}</td>`;
2356
2395
  }
2357
2396
  }
2358
2397
 
2359
- return `<td class="sdoc-table-td"${style}>${renderInline(cell)}</td>`;
2398
+ return `<td class="sdoc-table-td"${style}>${renderInline(cell)}${copyBtn(cell, c)}</td>`;
2360
2399
  })
2361
2400
  .join("");
2362
2401
  return `<tr>${cells}</tr>`;
@@ -3052,10 +3091,6 @@ const PRINT_STYLE = `
3052
3091
  position: relative;
3053
3092
  }
3054
3093
  .sdoc-copy-btn {
3055
- position: absolute;
3056
- top: 6px;
3057
- right: 6px;
3058
- z-index: 1;
3059
3094
  background: transparent;
3060
3095
  border: none;
3061
3096
  border-radius: 4px;
@@ -3063,15 +3098,35 @@ const PRINT_STYLE = `
3063
3098
  font-size: 0.85rem;
3064
3099
  line-height: 1;
3065
3100
  padding: 2px 6px;
3101
+ color: inherit;
3102
+ }
3103
+ .sdoc-code-wrap .sdoc-copy-btn {
3104
+ position: absolute;
3105
+ top: 6px;
3106
+ right: 6px;
3107
+ z-index: 1;
3066
3108
  opacity: 0;
3067
3109
  transition: opacity 0.15s;
3068
3110
  }
3069
3111
  .sdoc-code-wrap:hover .sdoc-copy-btn {
3070
3112
  opacity: 0.75;
3071
3113
  }
3072
- .sdoc-copy-btn:hover {
3114
+ .sdoc-code-wrap .sdoc-copy-btn:hover {
3073
3115
  opacity: 1 !important;
3074
3116
  }
3117
+ .sdoc-copyable {
3118
+ white-space: nowrap;
3119
+ }
3120
+ .sdoc-copy-inline {
3121
+ font-size: 0.8em;
3122
+ padding: 0 0.15em;
3123
+ opacity: 0.45;
3124
+ vertical-align: baseline;
3125
+ transition: opacity 0.15s;
3126
+ }
3127
+ .sdoc-copy-inline:hover {
3128
+ opacity: 1;
3129
+ }
3075
3130
  @media print {
3076
3131
  html {
3077
3132
  font-size: 80%;
@@ -3110,7 +3165,7 @@ const PRINT_STYLE = `
3110
3165
 
3111
3166
  const COLLAPSE_SCRIPT = `document.addEventListener("click",function(e){if(!e.target.classList.contains("sdoc-toggle"))return;e.stopPropagation();var s=e.target.closest(".sdoc-scope");if(s)s.classList.toggle("sdoc-collapsed")});`;
3112
3167
 
3113
- const COPY_SCRIPT = `document.addEventListener("click",function(e){if(!e.target.classList.contains("sdoc-copy-btn"))return;e.stopPropagation();e.preventDefault();var w=e.target.closest(".sdoc-code-wrap");if(!w)return;var c=w.querySelector("code");if(!c)return;var t=c.textContent;var b=e.target;if(navigator.clipboard){navigator.clipboard.writeText(t).then(function(){b.textContent="\\u2713";setTimeout(function(){b.textContent="\\u29C9"},1500)})}else{var a=document.createElement("textarea");a.value=t;a.style.position="fixed";a.style.opacity="0";document.body.appendChild(a);a.select();document.execCommand("copy");document.body.removeChild(a);b.textContent="\\u2713";setTimeout(function(){b.textContent="\\u29C9"},1500)}});`;
3168
+ const COPY_SCRIPT = `document.addEventListener("click",function(e){var b=e.target.closest(".sdoc-copy-btn");if(!b)return;e.stopPropagation();e.preventDefault();var t=b.getAttribute("data-copy");if(t===null){var w=b.closest(".sdoc-code-wrap");if(!w)return;var c=w.querySelector("code");if(!c)return;t=c.textContent}if(navigator.clipboard){navigator.clipboard.writeText(t).then(function(){b.textContent="\\u2713";setTimeout(function(){b.textContent="\\u29C9"},1500)})}else{var a=document.createElement("textarea");a.value=t;a.style.position="fixed";a.style.opacity="0";document.body.appendChild(a);a.select();document.execCommand("copy");document.body.removeChild(a);b.textContent="\\u2713";setTimeout(function(){b.textContent="\\u29C9"},1500)}});`;
3114
3169
 
3115
3170
  const MERMAID_CDN = "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js";
3116
3171
  const KATEX_CDN_CSS = "https://cdn.jsdelivr.net/npm/katex@0.16/dist/katex.min.css";
@@ -21,6 +21,9 @@ function renderInlineNodes(nodes) {
21
21
  return escapeHtml(node.value);
22
22
  case "code":
23
23
  return `<code>${escapeHtml(node.value)}</code>`;
24
+ case "copyable":
25
+ // Slides have no copy handler; render the literal text as monospace.
26
+ return `<code>${escapeHtml(node.value)}</code>`;
24
27
  case "color_swatch":
25
28
  return colorSwatchHtml(node.value);
26
29
  case "em":