@entropicwarrior/sdoc 0.2.12 → 0.2.13
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.
- package/docs/reference/sdoc-authoring.sdoc +66 -3
- package/lexica/specification.sdoc +71 -1
- package/package.json +1 -1
- package/src/sdoc.js +149 -12
|
@@ -275,6 +275,66 @@ Content of Section B.
|
|
|
275
275
|
}
|
|
276
276
|
```
|
|
277
277
|
|
|
278
|
+
# Column Directives @column-directives
|
|
279
|
+
{
|
|
280
|
+
An optional directive row after the header controls per-column alignment and number formatting. It is consumed by the parser and not rendered as data.
|
|
281
|
+
|
|
282
|
+
# Column Alignment
|
|
283
|
+
{
|
|
284
|
+
Use \`<\` (left), \`>\` (right), or \`=\` (center):
|
|
285
|
+
|
|
286
|
+
```
|
|
287
|
+
{[table]
|
|
288
|
+
Programme | Amount | Status
|
|
289
|
+
< | > | =
|
|
290
|
+
Alpha | $1,000,000 | Active
|
|
291
|
+
Beta | $2,500,000 | Pending
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
If absent, all columns default to left. Empty cells in the directive row keep the default. Alignment applies to both header and data cells.
|
|
296
|
+
|
|
297
|
+
For headerless tables, the directive row is the first row — it is consumed but no header is rendered:
|
|
298
|
+
|
|
299
|
+
```
|
|
300
|
+
{[table headerless]
|
|
301
|
+
< | >
|
|
302
|
+
Alice | 100
|
|
303
|
+
Bob | 200
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
# Column Formatting
|
|
309
|
+
{
|
|
310
|
+
A format token in the directive row formats numeric cells and formula results in that column. Text cells (like bold labels) pass through unchanged. The format is display-only — formulas always use raw numeric values.
|
|
311
|
+
|
|
312
|
+
{[.]
|
|
313
|
+
- \`$\` — currency, no decimals: \`$1,000,000\`
|
|
314
|
+
- \`$.N\` — currency with N decimals: \`$.2\` → \`$1,234.50\`
|
|
315
|
+
- \`,\` — thousands separator: \`1,000,000\`
|
|
316
|
+
- \`,.N\` — thousands with N decimals: \`,.2\` → \`1,234,567.89\`
|
|
317
|
+
- \`.N\` — fixed N decimals: \`.2\` → \`3.14\`
|
|
318
|
+
- \`%\` — percentage (value × 100): \`0.452\` → \`45.2%\`
|
|
319
|
+
- \`%.N\` — percentage with N decimals: \`%.1\` → \`45.7%\`
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
Alignment and format combine in a single directive cell:
|
|
323
|
+
|
|
324
|
+
```
|
|
325
|
+
{[table]
|
|
326
|
+
Item | Amount
|
|
327
|
+
< | > $
|
|
328
|
+
Widget | 1000000
|
|
329
|
+
Gadget | 2500000
|
|
330
|
+
**Total** | =SUM(B1:B2)
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
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
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
278
338
|
# Table Formulas @table-formulas
|
|
279
339
|
{
|
|
280
340
|
Cells starting with \`=\` are evaluated as formulas. Hover a computed cell in preview to see the original formula.
|
|
@@ -282,9 +342,10 @@ Content of Section B.
|
|
|
282
342
|
```
|
|
283
343
|
{[table]
|
|
284
344
|
Investor | Shares | Ownership
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
Founder
|
|
345
|
+
< | > , | > %
|
|
346
|
+
Seed Fund | 500000 | 0.25
|
|
347
|
+
Founder A | 1000000 | 0.50
|
|
348
|
+
Founder B | 500000 | 0.25
|
|
288
349
|
**Total** | =SUM(B1:B3) | =SUM(C1:C3)
|
|
289
350
|
}
|
|
290
351
|
```
|
|
@@ -301,6 +362,8 @@ Content of Section B.
|
|
|
301
362
|
}
|
|
302
363
|
|
|
303
364
|
Errors display in red italic: \`#DIV/0!\` (division by zero), \`#VALUE!\` (bad reference), \`#REF!\` (invalid syntax), \`#NAME!\` (unknown function), \`#CIRCULAR!\` (circular dependency).
|
|
365
|
+
|
|
366
|
+
**Tip:** Use numeric values with a column format directive (e.g. \`> $\`) instead of pre-formatted strings like \`$1,000,000\`. Formatted strings are text — formulas cannot reference them.
|
|
304
367
|
}
|
|
305
368
|
}
|
|
306
369
|
|
|
@@ -380,6 +380,69 @@ Content of Section B.
|
|
|
380
380
|
```
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
+
# Column Directives @column-directives
|
|
384
|
+
{
|
|
385
|
+
An optional directive row immediately after the header row (or as the first row in a headerless table) controls per-column alignment and number formatting. The directive row is consumed by the parser and never rendered as data.
|
|
386
|
+
|
|
387
|
+
# Column Alignment @column-alignment
|
|
388
|
+
{
|
|
389
|
+
Alignment characters in the directive row set the text-align for all cells in that column, including the header:
|
|
390
|
+
|
|
391
|
+
{[.]
|
|
392
|
+
- `<` — left (default)
|
|
393
|
+
- `>` — right (most useful for numbers and currency)
|
|
394
|
+
- `=` — center
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
```
|
|
398
|
+
{[table]
|
|
399
|
+
Programme | Amount | Status
|
|
400
|
+
< | > | =
|
|
401
|
+
Alpha | $1,000,000 | Active
|
|
402
|
+
Beta | $2,500,000 | Pending
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
If the directive row is absent, all columns default to left alignment. Empty cells in the directive row inherit the default (left).
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
# Column Formatting @column-formatting
|
|
410
|
+
{
|
|
411
|
+
A format token in the directive row causes numeric cells and formula results in that column to be formatted for display. Text cells are unaffected. The format is display-only — formulas always operate on raw numeric values.
|
|
412
|
+
|
|
413
|
+
{[.]
|
|
414
|
+
- `$` — currency, no decimals: `1000000` → `$1,000,000`
|
|
415
|
+
- `$.N` — currency with N decimals: `$.2` → `$1,234.50`
|
|
416
|
+
- `,` — thousands separator, no decimals: `1000000` → `1,000,000`
|
|
417
|
+
- `,.N` — thousands with N decimals: `,.2` → `1,234,567.89`
|
|
418
|
+
- `.N` — fixed N decimals: `.2` → `3.14`
|
|
419
|
+
- `%` — percentage (value × 100, auto decimals): `0.452` → `45.2%`
|
|
420
|
+
- `%.N` — percentage with N decimals: `%.1` → `45.7%`
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
A directive cell may combine alignment and format, separated by whitespace:
|
|
424
|
+
|
|
425
|
+
```
|
|
426
|
+
{[table]
|
|
427
|
+
Item | Amount
|
|
428
|
+
< | > $
|
|
429
|
+
Widget | 1000000
|
|
430
|
+
Gadget | 2500000
|
|
431
|
+
**Total** | =SUM(B1:B2)
|
|
432
|
+
}
|
|
433
|
+
```
|
|
434
|
+
|
|
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
|
+
}
|
|
437
|
+
|
|
438
|
+
# Directive Row Detection @directive-detection
|
|
439
|
+
{
|
|
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.
|
|
441
|
+
|
|
442
|
+
For headerless tables, the directive row functions as a virtual header — it is consumed for its directives but no `<thead>` is rendered.
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
383
446
|
# Table Formulas @table-formulas
|
|
384
447
|
{
|
|
385
448
|
Table cells whose text begins with `=` (but not `==`) are evaluated as formulas. The result replaces the cell text in rendered output; the original formula is preserved as a tooltip.
|
|
@@ -1083,7 +1146,14 @@ Content of Section A.
|
|
|
1083
1146
|
| "left" | "center" | "right" ;
|
|
1084
1147
|
percentage = digit { digit } [ "." digit { digit } ] "%" ;
|
|
1085
1148
|
pixels = digit { digit } "px" ;
|
|
1086
|
-
table_body = table_row { table_row } ;
|
|
1149
|
+
table_body = [ directive_row ] table_row { table_row } ;
|
|
1150
|
+
directive_row = directive_cell { "|" directive_cell } ;
|
|
1151
|
+
directive_cell = [ align_char ] [ ws format_spec ] ;
|
|
1152
|
+
align_char = "<" | ">" | "=" ;
|
|
1153
|
+
format_spec = "$" [ "." digits ]
|
|
1154
|
+
| "," [ "." digits ]
|
|
1155
|
+
| "." digits
|
|
1156
|
+
| "%" [ "." digits ] ;
|
|
1087
1157
|
table_row = cell { "|" cell } ;
|
|
1088
1158
|
citations_scope = citations_open ws? citations_body "}" ;
|
|
1089
1159
|
citations_open = "{[citations]" ;
|
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.
|
|
5
|
+
"version": "0.2.13",
|
|
6
6
|
"publisher": "entropicwarrior-msenfin",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"repository": {
|
package/src/sdoc.js
CHANGED
|
@@ -63,6 +63,105 @@ function parseTableOptions(text) {
|
|
|
63
63
|
return options;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
// ── Column directive row (alignment + format) ──────────────────────────
|
|
67
|
+
|
|
68
|
+
function isDirectiveRow(cells) {
|
|
69
|
+
if (cells.length === 0) return false;
|
|
70
|
+
const pattern = /^([<>=])?\s*(\$(?:\.\d+)?|,(?:\.\d+)?|\.\d+|%(?:\.\d+)?)?$/;
|
|
71
|
+
let hasDirective = false;
|
|
72
|
+
for (const cell of cells) {
|
|
73
|
+
const trimmed = cell.trim();
|
|
74
|
+
if (trimmed === "") continue;
|
|
75
|
+
if (!pattern.test(trimmed)) return false;
|
|
76
|
+
hasDirective = true;
|
|
77
|
+
}
|
|
78
|
+
return hasDirective;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function parseFormatSpec(spec) {
|
|
82
|
+
if (!spec) return null;
|
|
83
|
+
if (spec.startsWith("$")) {
|
|
84
|
+
const decimals = spec.includes(".") ? parseInt(spec.slice(spec.indexOf(".") + 1), 10) : 0;
|
|
85
|
+
return { prefix: "$", thousands: true, decimals, percent: false };
|
|
86
|
+
}
|
|
87
|
+
if (spec.startsWith("%")) {
|
|
88
|
+
const decimals = spec.includes(".") ? parseInt(spec.slice(spec.indexOf(".") + 1), 10) : -1;
|
|
89
|
+
return { prefix: "", thousands: false, decimals, percent: true };
|
|
90
|
+
}
|
|
91
|
+
if (spec.startsWith(",")) {
|
|
92
|
+
const decimals = spec.includes(".") ? parseInt(spec.slice(spec.indexOf(".") + 1), 10) : 0;
|
|
93
|
+
return { prefix: "", thousands: true, decimals, percent: false };
|
|
94
|
+
}
|
|
95
|
+
if (spec.startsWith(".")) {
|
|
96
|
+
const decimals = parseInt(spec.slice(1), 10);
|
|
97
|
+
return { prefix: "", thousands: false, decimals, percent: false };
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseDirectiveRow(cells) {
|
|
103
|
+
const align = [];
|
|
104
|
+
const format = [];
|
|
105
|
+
let hasAlign = false;
|
|
106
|
+
let hasFormat = false;
|
|
107
|
+
const fmtPattern = /(\$(?:\.\d+)?|,(?:\.\d+)?|\.\d+|%(?:\.\d+)?)$/;
|
|
108
|
+
|
|
109
|
+
for (const cell of cells) {
|
|
110
|
+
const trimmed = cell.trim();
|
|
111
|
+
const alignMatch = trimmed.match(/^([<>=])/);
|
|
112
|
+
const fmtMatch = trimmed.match(fmtPattern);
|
|
113
|
+
|
|
114
|
+
let a = null;
|
|
115
|
+
if (alignMatch) {
|
|
116
|
+
a = alignMatch[1] === "<" ? "left" : alignMatch[1] === ">" ? "right" : "center";
|
|
117
|
+
hasAlign = true;
|
|
118
|
+
}
|
|
119
|
+
align.push(a);
|
|
120
|
+
|
|
121
|
+
let f = null;
|
|
122
|
+
if (fmtMatch) {
|
|
123
|
+
f = parseFormatSpec(fmtMatch[1]);
|
|
124
|
+
hasFormat = true;
|
|
125
|
+
}
|
|
126
|
+
format.push(f);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
align: hasAlign ? align : null,
|
|
131
|
+
format: hasFormat ? format : null,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function formatNumber(value, spec) {
|
|
136
|
+
if (spec.percent) {
|
|
137
|
+
const pct = value * 100;
|
|
138
|
+
if (spec.decimals < 0) {
|
|
139
|
+
return (Number.isInteger(pct) ? pct.toString() : pct.toFixed(2).replace(/\.?0+$/, "")) + "%";
|
|
140
|
+
}
|
|
141
|
+
return pct.toFixed(spec.decimals) + "%";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const negative = value < 0;
|
|
145
|
+
const absVal = Math.abs(value);
|
|
146
|
+
let result;
|
|
147
|
+
|
|
148
|
+
if (spec.decimals > 0) {
|
|
149
|
+
result = absVal.toFixed(spec.decimals);
|
|
150
|
+
} else {
|
|
151
|
+
result = Math.round(absVal).toString();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (spec.thousands) {
|
|
155
|
+
const parts = result.split(".");
|
|
156
|
+
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
157
|
+
result = parts.join(".");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return (negative ? "-" : "") + spec.prefix + result;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── End column directive row ────────────────────────────────────────────
|
|
164
|
+
|
|
66
165
|
class LineCursor {
|
|
67
166
|
constructor(lines) {
|
|
68
167
|
this.lines = lines;
|
|
@@ -835,18 +934,31 @@ function parseTableBody(cursor, tableStartLine, options) {
|
|
|
835
934
|
cursor.next();
|
|
836
935
|
}
|
|
837
936
|
|
|
937
|
+
// Detect column directive row (alignment / formatting)
|
|
938
|
+
// For headerless tables: check rows[0]; for normal tables: check rows[1] (after header)
|
|
939
|
+
const directiveIndex = options.headerless ? 0 : 1;
|
|
940
|
+
let columnAlign = null;
|
|
941
|
+
let columnFormat = null;
|
|
942
|
+
if (rows.length > directiveIndex && isDirectiveRow(rows[directiveIndex])) {
|
|
943
|
+
const directives = parseDirectiveRow(rows[directiveIndex]);
|
|
944
|
+
columnAlign = directives.align;
|
|
945
|
+
columnFormat = directives.format;
|
|
946
|
+
rows.splice(directiveIndex, 1);
|
|
947
|
+
}
|
|
948
|
+
|
|
838
949
|
const hasOptions = options.borderless || options.headerless || options.width || options.align;
|
|
839
950
|
|
|
951
|
+
let tableNode;
|
|
840
952
|
if (options.headerless) {
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
953
|
+
tableNode = { type: "table", headers: [], rows, lineStart: tableStartLine, lineEnd: cursor.index };
|
|
954
|
+
} else {
|
|
955
|
+
const headers = rows.length > 0 ? rows[0] : [];
|
|
956
|
+
const body = rows.slice(1);
|
|
957
|
+
tableNode = { type: "table", headers, rows: body, lineStart: tableStartLine, lineEnd: cursor.index };
|
|
844
958
|
}
|
|
845
|
-
|
|
846
|
-
const headers = rows.length > 0 ? rows[0] : [];
|
|
847
|
-
const body = rows.slice(1);
|
|
848
|
-
const tableNode = { type: "table", headers, rows: body, lineStart: tableStartLine, lineEnd: cursor.index };
|
|
849
959
|
if (hasOptions) tableNode.options = options;
|
|
960
|
+
if (columnAlign) tableNode.columnAlign = columnAlign;
|
|
961
|
+
if (columnFormat) tableNode.columnFormat = columnFormat;
|
|
850
962
|
return tableNode;
|
|
851
963
|
}
|
|
852
964
|
|
|
@@ -2122,15 +2234,23 @@ function formatFormulaResult(cell) {
|
|
|
2122
2234
|
function renderTable(table) {
|
|
2123
2235
|
const dl = dataLineAttrs(table);
|
|
2124
2236
|
const opts = table.options || {};
|
|
2237
|
+
const colAlign = table.columnAlign || [];
|
|
2238
|
+
const colFormat = table.columnFormat || [];
|
|
2125
2239
|
const classes = ["sdoc-table"];
|
|
2126
2240
|
if (opts.borderless) classes.push("sdoc-table-borderless");
|
|
2127
2241
|
if (opts.headerless) classes.push("sdoc-table-headerless");
|
|
2128
2242
|
const classAttr = classes.join(" ");
|
|
2129
2243
|
|
|
2244
|
+
function cellStyle(colIndex) {
|
|
2245
|
+
const align = colAlign[colIndex];
|
|
2246
|
+
if (!align || align === "left") return "";
|
|
2247
|
+
return ` style="text-align:${align}"`;
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2130
2250
|
let thead = "";
|
|
2131
2251
|
if (table.headers.length > 0) {
|
|
2132
2252
|
const headerCells = table.headers
|
|
2133
|
-
.map((cell) => `<th class="sdoc-table-th">${renderInline(cell)}</th>`)
|
|
2253
|
+
.map((cell, c) => `<th class="sdoc-table-th"${cellStyle(c)}>${renderInline(cell)}</th>`)
|
|
2134
2254
|
.join("");
|
|
2135
2255
|
thead = `<thead class="sdoc-table-head"><tr>${headerCells}</tr></thead>`;
|
|
2136
2256
|
}
|
|
@@ -2142,16 +2262,33 @@ function renderTable(table) {
|
|
|
2142
2262
|
.map((row, r) => {
|
|
2143
2263
|
const cells = row
|
|
2144
2264
|
.map((cell, c) => {
|
|
2265
|
+
const style = cellStyle(c);
|
|
2266
|
+
const fmt = colFormat[c] || null;
|
|
2267
|
+
|
|
2145
2268
|
if (isFormulaCell(cell)) {
|
|
2146
2269
|
const result = grid[r][c];
|
|
2147
|
-
|
|
2270
|
+
let display;
|
|
2271
|
+
if (!result.error && fmt) {
|
|
2272
|
+
display = escapeHtml(formatNumber(result.value, fmt));
|
|
2273
|
+
} else {
|
|
2274
|
+
display = escapeHtml(formatFormulaResult(result));
|
|
2275
|
+
}
|
|
2148
2276
|
const formula = escapeAttr(cell.trim());
|
|
2149
2277
|
if (result.error) {
|
|
2150
|
-
return `<td class="sdoc-table-td sdoc-formula-error" title="${formula}">${display}</td>`;
|
|
2278
|
+
return `<td class="sdoc-table-td sdoc-formula-error"${style} title="${formula}">${display}</td>`;
|
|
2151
2279
|
}
|
|
2152
|
-
return `<td class="sdoc-table-td sdoc-formula-cell" title="${formula}">${display}</td>`;
|
|
2280
|
+
return `<td class="sdoc-table-td sdoc-formula-cell"${style} title="${formula}">${display}</td>`;
|
|
2153
2281
|
}
|
|
2154
|
-
|
|
2282
|
+
|
|
2283
|
+
// Apply column format to numeric data cells
|
|
2284
|
+
if (fmt) {
|
|
2285
|
+
const parsed = parseCellValue(cell);
|
|
2286
|
+
if (!isNaN(parsed.value)) {
|
|
2287
|
+
return `<td class="sdoc-table-td"${style}>${escapeHtml(formatNumber(parsed.value, fmt))}</td>`;
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
return `<td class="sdoc-table-td"${style}>${renderInline(cell)}</td>`;
|
|
2155
2292
|
})
|
|
2156
2293
|
.join("");
|
|
2157
2294
|
return `<tr>${cells}</tr>`;
|