sdocs-dev 1.6.2 → 1.13.0
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/bin/sdocs-bridge.js +974 -0
- package/bin/sdocs-dev.js +152 -2102
- package/bin/sdocs-icon-names.js +1965 -0
- package/lib/agent-block.js +245 -0
- package/lib/agent-files.js +162 -0
- package/lib/bridge-commands.js +171 -0
- package/lib/cells-transclude.js +114 -0
- package/lib/cells-verify.js +165 -0
- package/lib/commands.js +291 -0
- package/lib/constants.js +283 -0
- package/lib/help-text.js +2759 -0
- package/lib/io.js +177 -0
- package/lib/library-autostart.js +145 -0
- package/lib/library-commands.js +307 -0
- package/lib/library-ephemeral.js +111 -0
- package/lib/library-index.js +285 -0
- package/lib/library-paths.js +20 -0
- package/lib/library-scan.js +258 -0
- package/lib/library-server.js +400 -0
- package/lib/library-store.js +141 -0
- package/lib/router.js +52 -0
- package/lib/safe.js +200 -0
- package/lib/setup.js +351 -0
- package/lib/short-link.js +105 -0
- package/lib/styles.js +91 -0
- package/lib/update-check.js +163 -0
- package/lib/url.js +111 -0
- package/package.json +5 -16
- package/shared/sdocs-cells-formula.js +485 -0
- package/shared/sdocs-cells.js +389 -0
- package/shared/sdocs-contrast.js +196 -0
- package/shared/sdocs-form-block.js +605 -0
- package/shared/sdocs-library-tags.js +41 -0
- package/{public → shared}/sdocs-styles.js +134 -5
- package/README.md +0 -149
- /package/{public → shared}/sdocs-slugify.js +0 -0
- /package/{public → shared}/sdocs-yaml.js +0 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// cells-transclude.js - bake {{path/to/file.csv}} references into ```cells
|
|
2
|
+
// blocks at CLI time.
|
|
3
|
+
//
|
|
4
|
+
// The browser can never read a local file, so a CSV reference only means
|
|
5
|
+
// something while the CLI is involved. On open (and on share), every
|
|
6
|
+
// ```cells block whose body is a bare {{...}} reference is replaced with the
|
|
7
|
+
// file's full contents plus a metadata line the renderer reads:
|
|
8
|
+
//
|
|
9
|
+
// ```cells ```cells
|
|
10
|
+
// {{data/report.csv}} -> sdoc-cells: source=report.csv
|
|
11
|
+
// ``` Region,Q1,Q2
|
|
12
|
+
// North,100,150
|
|
13
|
+
// ...
|
|
14
|
+
// ```
|
|
15
|
+
//
|
|
16
|
+
// The whole file is baked in (the user chose "whole CSV always travels"), so
|
|
17
|
+
// the resulting doc is self-contained and a share link never errors. Only the
|
|
18
|
+
// basename is recorded as `source=` - the full local path would leak the
|
|
19
|
+
// author's directory structure into a shared link. A read failure bakes an
|
|
20
|
+
// `error=` directive the renderer surfaces instead.
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
|
|
25
|
+
// A ```cells fenced block (captures leading boundary, optional fence name, and
|
|
26
|
+
// body). The name (```cells Sales) is preserved on the baked block so a tab
|
|
27
|
+
// loaded from a CSV keeps its identity. Tilde fences and inline-data blocks are
|
|
28
|
+
// left untouched.
|
|
29
|
+
const CELLS_BLOCK = /(^|\n)```cells[ \t]*([^\n]*)\n([\s\S]*?)\n```/g;
|
|
30
|
+
const REFERENCE = /^\{\{\s*([^}]+?)\s*\}\}$/;
|
|
31
|
+
// A trailing :range suffix like :B5:J32 or :B5 (a view hint; data is baked in
|
|
32
|
+
// whole regardless, so we just strip it off the path for now).
|
|
33
|
+
const RANGE_SUFFIX = /:([A-Za-z]+\d+(?::[A-Za-z]+\d+)?)$/;
|
|
34
|
+
|
|
35
|
+
function directiveValue(s) {
|
|
36
|
+
return /\s|"/.test(s) ? JSON.stringify(s) : s;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function bakeBlock(boundary, name, ref, baseDir, readFile, preLines) {
|
|
40
|
+
var range = '';
|
|
41
|
+
var filePath = ref;
|
|
42
|
+
var rm = ref.match(RANGE_SUFFIX);
|
|
43
|
+
if (rm) { range = rm[1]; filePath = ref.slice(0, ref.length - rm[0].length); }
|
|
44
|
+
|
|
45
|
+
var fence = '```cells' + (name ? ' ' + name : '');
|
|
46
|
+
var base = path.basename(filePath);
|
|
47
|
+
// Author format: lines (e.g. `format: B=$`) sit before the reference and are
|
|
48
|
+
// preserved verbatim above the baked data.
|
|
49
|
+
var head = (preLines && preLines.length ? preLines.join('\n') + '\n' : '');
|
|
50
|
+
var csv;
|
|
51
|
+
try {
|
|
52
|
+
csv = readFile(path.resolve(baseDir, filePath));
|
|
53
|
+
} catch (e) {
|
|
54
|
+
return boundary + fence + '\n' + head + 'sdoc-cells: error=' +
|
|
55
|
+
directiveValue('Could not read ' + base) + '\n```';
|
|
56
|
+
}
|
|
57
|
+
csv = String(csv).replace(/\s+$/, '');
|
|
58
|
+
var directive = 'sdoc-cells: source=' + directiveValue(base) +
|
|
59
|
+
(range ? ' range=' + range : '');
|
|
60
|
+
return boundary + fence + '\n' + head + directive + '\n' + csv + '\n```';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Replace every {{file.csv}} cells block in `content` with the baked data.
|
|
64
|
+
// `readFile` is injectable for tests; defaults to fs.readFileSync(utf-8).
|
|
65
|
+
function transcludeCells(content, baseDir, readFile) {
|
|
66
|
+
if (typeof content !== 'string' || content.indexOf('```cells') === -1) return content;
|
|
67
|
+
var read = readFile || function (p) { return fs.readFileSync(p, 'utf-8'); };
|
|
68
|
+
return content.replace(CELLS_BLOCK, function (whole, boundary, name, body) {
|
|
69
|
+
// Peel any leading author `format:` lines, then require a sole {{ref}}.
|
|
70
|
+
var lines = body.split('\n');
|
|
71
|
+
var pre = [];
|
|
72
|
+
var i = 0;
|
|
73
|
+
while (i < lines.length && /^\s*format:\s*/i.test(lines[i])) { pre.push(lines[i].trim()); i++; }
|
|
74
|
+
var rest = lines.slice(i).join('\n').trim();
|
|
75
|
+
var m = rest.match(REFERENCE);
|
|
76
|
+
if (!m) return whole; // inline data - leave alone
|
|
77
|
+
return bakeBlock(boundary, (name || '').trim(), m[1], baseDir, read, pre);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Wrap a standalone .csv file's contents in a ```cells block (so `sdoc x.csv`
|
|
82
|
+
// opens as a sheet, mirroring the .mmd -> mermaid wrapping).
|
|
83
|
+
function wrapCsvFile(csv, filename) {
|
|
84
|
+
var base = path.basename(filename);
|
|
85
|
+
return '```cells\nsdoc-cells: source=' + directiveValue(base) + '\n' +
|
|
86
|
+
String(csv).replace(/\s+$/, '') + '\n```\n';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// A "wrapped" file is one whose renderable document is DERIVED from the file
|
|
90
|
+
// (its contents inside a fenced block) rather than the file itself. The two
|
|
91
|
+
// open paths (URL snapshot via readContent, live sync via the bridge) both
|
|
92
|
+
// wrap these for display, and the bridge refuses to save the derived view
|
|
93
|
+
// back - that would overwrite the .csv / .mmd with fence markup.
|
|
94
|
+
function isWrappedFile(filePath) {
|
|
95
|
+
return /\.(csv|mmd|mermaid)$/i.test(String(filePath || ''));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// File contents -> renderable document. Wrapped types get their fence;
|
|
99
|
+
// everything else (markdown) passes through untouched.
|
|
100
|
+
function wrapForDisplay(raw, filePath) {
|
|
101
|
+
var name = String(filePath || '');
|
|
102
|
+
if (/\.(mmd|mermaid)$/i.test(name)) {
|
|
103
|
+
return '```mermaid\n' + String(raw).replace(/\s+$/, '') + '\n```\n';
|
|
104
|
+
}
|
|
105
|
+
if (/\.csv$/i.test(name)) return wrapCsvFile(raw, name);
|
|
106
|
+
return String(raw);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
transcludeCells: transcludeCells,
|
|
111
|
+
wrapCsvFile: wrapCsvFile,
|
|
112
|
+
isWrappedFile: isWrappedFile,
|
|
113
|
+
wrapForDisplay: wrapForDisplay,
|
|
114
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// cells-verify.js - headless evaluation of a document's ```cells tabs.
|
|
2
|
+
//
|
|
3
|
+
// `sdoc cells verify <file.md>` parses every ```cells block, names the tabs
|
|
4
|
+
// exactly as the browser does, recalculates them together as one workbook
|
|
5
|
+
// (so cross-tab references like Sales!A1 resolve), and prints the COMPUTED
|
|
6
|
+
// values. It is the agent's feedback loop: write formulas, run this, read the
|
|
7
|
+
// numbers back - no browser, same engine the page runs (cli/shared/), so what
|
|
8
|
+
// it prints is what ships.
|
|
9
|
+
//
|
|
10
|
+
// Output:
|
|
11
|
+
// default CSV per tab, each under a "# sheet: <name>" banner. Human view;
|
|
12
|
+
// a data row could itself start with "# sheet:", so this form is
|
|
13
|
+
// documented as not machine-round-trippable - agents use --json.
|
|
14
|
+
// --json { ok, sheets:[{name, values:[[...]]}], errors:[{sheet,cell,code}] }
|
|
15
|
+
// --sheet N only that tab (case-insensitive). Absent name -> exit 2.
|
|
16
|
+
//
|
|
17
|
+
// Exit: 0 = no cell errors in the emitted tabs; 1 = at least one cell error
|
|
18
|
+
// (#REF!/#CIRC!/#DIV0!/...); 2 = bad usage (no file, --sheet names no tab).
|
|
19
|
+
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const io = require('./io');
|
|
22
|
+
const CELLS = require('../shared/sdocs-cells.js');
|
|
23
|
+
const FX = require('../shared/sdocs-cells-formula.js');
|
|
24
|
+
|
|
25
|
+
// Scan ```cells (or ~~~cells) fenced blocks, capturing the fence info string
|
|
26
|
+
// as the tab name and the block body. Approximate vs marked (no nested-fence
|
|
27
|
+
// awareness) but matches how agents author docs. The closing fence must be the
|
|
28
|
+
// same run of fence characters (backreference) at a line start.
|
|
29
|
+
function scanCellsBlocks(md) {
|
|
30
|
+
var re = /(?:^|\n)(```+|~~~+)cells[ \t]*([^\n]*)\n([\s\S]*?)\n\1[ \t]*(?=\n|$)/g;
|
|
31
|
+
var blocks = [];
|
|
32
|
+
var m;
|
|
33
|
+
while ((m = re.exec(md))) {
|
|
34
|
+
blocks.push({ name: (m[2] || '').trim().replace(/"/g, ''), body: m[3] });
|
|
35
|
+
}
|
|
36
|
+
return blocks;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Parse + name every block into a workbook, mirroring the renderer: a fence
|
|
40
|
+
// name wins, then an explicit `name=` directive, then auto Sheet1/Sheet2... by
|
|
41
|
+
// order among the blocks that became real grids. Non-grid blocks (empty /
|
|
42
|
+
// unresolved) are skipped; a parse error is recorded so the exit code reflects it.
|
|
43
|
+
function buildWorkbook(md) {
|
|
44
|
+
var blocks = scanCellsBlocks(md);
|
|
45
|
+
var sheets = []; // { name, model }
|
|
46
|
+
var parseErrors = [];
|
|
47
|
+
var autoIdx = 0;
|
|
48
|
+
for (var i = 0; i < blocks.length; i++) {
|
|
49
|
+
var model;
|
|
50
|
+
try { model = CELLS.parseCells(blocks[i].body); }
|
|
51
|
+
catch (e) { parseErrors.push((e && e.message) || 'parse error'); continue; }
|
|
52
|
+
if (model.error) { parseErrors.push(model.error); continue; }
|
|
53
|
+
if (model.unresolved || model.empty) continue;
|
|
54
|
+
var name = blocks[i].name || (model.name && String(model.name).trim()) || ('Sheet' + (++autoIdx));
|
|
55
|
+
sheets.push({ name: name, model: model });
|
|
56
|
+
}
|
|
57
|
+
return { sheets: sheets, parseErrors: parseErrors };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Render one sheet's computed grid to a 2D array of display strings: a formula
|
|
61
|
+
// cell shows its result (number, or the error code), every other cell shows its
|
|
62
|
+
// literal text verbatim. fxGrid is this sheet's recalcWorkbook results.
|
|
63
|
+
function valuesFor(model, fxGrid) {
|
|
64
|
+
var out = [];
|
|
65
|
+
for (var r = 0; r < model.cells.length; r++) {
|
|
66
|
+
var line = model.cells[r];
|
|
67
|
+
var row = [];
|
|
68
|
+
for (var c = 0; c < line.length; c++) {
|
|
69
|
+
var cell = line[c];
|
|
70
|
+
if (FX.isFormula(cell.raw)) {
|
|
71
|
+
var fx = (fxGrid[r] && fxGrid[r][c]) || { kind: 'empty' };
|
|
72
|
+
if (fx.kind === 'number') row.push(String(fx.value));
|
|
73
|
+
else if (fx.kind === 'error') row.push(fx.code);
|
|
74
|
+
else if (fx.kind === 'text') row.push(String(fx.value));
|
|
75
|
+
else row.push('');
|
|
76
|
+
} else {
|
|
77
|
+
row.push(cell.raw);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
out.push(row);
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Collect cell errors across a sheet for the exit code + --json errors list.
|
|
86
|
+
function errorsFor(name, model, fxGrid) {
|
|
87
|
+
var errs = [];
|
|
88
|
+
for (var r = 0; r < fxGrid.length; r++) {
|
|
89
|
+
var line = fxGrid[r] || [];
|
|
90
|
+
for (var c = 0; c < line.length; c++) {
|
|
91
|
+
var fx = line[c];
|
|
92
|
+
if (fx && fx.kind === 'error') {
|
|
93
|
+
errs.push({ sheet: name, cell: CELLS.colName(c) + (r + 1), code: fx.code });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return errs;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function cellsVerifyCommand(opts) {
|
|
101
|
+
var file = opts.extra; // `sdoc cells verify <file>` -> file lands in extra
|
|
102
|
+
if (!file) {
|
|
103
|
+
console.error('sdoc: cells verify needs a file - usage: sdoc cells verify <file.md> [--json] [--sheet <name>]');
|
|
104
|
+
process.exit(2);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
var content = await io.readContent(file); // same baking the browser receives
|
|
108
|
+
if (content == null) {
|
|
109
|
+
console.error('sdoc: nothing to read from ' + file);
|
|
110
|
+
process.exit(2);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
var wb = buildWorkbook(content);
|
|
114
|
+
var fxGrids = FX.recalcWorkbook(wb.sheets.map(function (s) {
|
|
115
|
+
return { name: s.name, model: s.model };
|
|
116
|
+
}));
|
|
117
|
+
|
|
118
|
+
// Build the per-sheet view once; --sheet filters it afterwards.
|
|
119
|
+
var rendered = wb.sheets.map(function (s, i) {
|
|
120
|
+
return {
|
|
121
|
+
name: s.name,
|
|
122
|
+
values: valuesFor(s.model, fxGrids[i] || []),
|
|
123
|
+
errors: errorsFor(s.name, s.model, fxGrids[i] || []),
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (opts.sheetName) {
|
|
128
|
+
var want = String(opts.sheetName).toLowerCase();
|
|
129
|
+
var only = rendered.filter(function (r) { return r.name.toLowerCase() === want; });
|
|
130
|
+
if (!only.length) {
|
|
131
|
+
console.error('sdoc: no tab named "' + opts.sheetName + '" in ' + file);
|
|
132
|
+
process.exit(2);
|
|
133
|
+
}
|
|
134
|
+
rendered = only;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
var allErrors = [];
|
|
138
|
+
rendered.forEach(function (r) { allErrors = allErrors.concat(r.errors); });
|
|
139
|
+
var ok = allErrors.length === 0 && wb.parseErrors.length === 0;
|
|
140
|
+
|
|
141
|
+
if (opts.jsonFlag) {
|
|
142
|
+
process.stdout.write(JSON.stringify({
|
|
143
|
+
ok: ok,
|
|
144
|
+
sheets: rendered.map(function (r) { return { name: r.name, values: r.values }; }),
|
|
145
|
+
errors: allErrors,
|
|
146
|
+
parseErrors: wb.parseErrors,
|
|
147
|
+
}, null, 2) + '\n');
|
|
148
|
+
} else {
|
|
149
|
+
if (!rendered.length) console.error('sdoc: no cells tabs found in ' + file);
|
|
150
|
+
var chunks = rendered.map(function (r) {
|
|
151
|
+
return '# sheet: ' + r.name + '\n' + CELLS.serializeCsv(r.values);
|
|
152
|
+
});
|
|
153
|
+
if (chunks.length) process.stdout.write(chunks.join('\n') + '\n');
|
|
154
|
+
wb.parseErrors.forEach(function (e) { console.error('sdoc: cells parse error - ' + e); });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
process.exit(ok ? 0 : 1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = {
|
|
161
|
+
cellsVerifyCommand: cellsVerifyCommand,
|
|
162
|
+
scanCellsBlocks: scanCellsBlocks,
|
|
163
|
+
buildWorkbook: buildWorkbook,
|
|
164
|
+
valuesFor: valuesFor,
|
|
165
|
+
};
|
package/lib/commands.js
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// Verb handlers wired into the router.
|
|
2
|
+
//
|
|
3
|
+
// Each handler takes parsed opts and returns a Promise (or void). They
|
|
4
|
+
// share `prepareUrl` for the load-content / apply-defaults / build-URL
|
|
5
|
+
// flow that `open` and `share` both need.
|
|
6
|
+
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { execSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
const SDocYaml = require('../shared/sdocs-yaml.js');
|
|
11
|
+
|
|
12
|
+
const { DEFAULT_URL } = require('./constants');
|
|
13
|
+
const { readContent, openBrowser } = require('./io');
|
|
14
|
+
const { loadDefaultStyles, applyDefaultStyles, showDefaults, resetDefaults } = require('./styles');
|
|
15
|
+
const { buildUrl } = require('./url');
|
|
16
|
+
const { buildShortUrl } = require('./short-link');
|
|
17
|
+
const { refreshUpdateCache, maybeUpdateBinary } = require('./update-check');
|
|
18
|
+
const { runSetup, maybeAutoRefresh } = require('./setup');
|
|
19
|
+
|
|
20
|
+
// Shared "after the command ran" tail used by `open` and `share`.
|
|
21
|
+
async function postCommandHooks() {
|
|
22
|
+
refreshUpdateCache();
|
|
23
|
+
await maybeUpdateBinary();
|
|
24
|
+
await runSetup();
|
|
25
|
+
await maybeAutoRefresh();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Load content (file or stdin), apply ~/.sdocs/styles.yaml defaults, inject
|
|
29
|
+
// `file:` into front matter, and build either a hash URL or a short URL.
|
|
30
|
+
// Returns { url, contentPresent }.
|
|
31
|
+
async function prepareUrl(opts) {
|
|
32
|
+
let content = await readContent(opts.file);
|
|
33
|
+
const defaults = loadDefaultStyles();
|
|
34
|
+
if (content && defaults) {
|
|
35
|
+
content = applyDefaultStyles(content);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Inject `file:` into front matter (basename only — safe to share).
|
|
39
|
+
// Respects user-set file: if already present.
|
|
40
|
+
if (content && opts.file) {
|
|
41
|
+
const parsed = SDocYaml.parseFrontMatter(content);
|
|
42
|
+
if (!parsed.meta.file) {
|
|
43
|
+
parsed.meta.file = path.basename(opts.file);
|
|
44
|
+
content = SDocYaml.serializeFrontMatter(parsed.meta) + '\n' + parsed.body;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Runtime-only local metadata for the opener's view.
|
|
49
|
+
// `share` omits it so shared URLs never carry paths.
|
|
50
|
+
let local = null;
|
|
51
|
+
if (opts.file && opts.subcommand !== 'share') {
|
|
52
|
+
const abs = path.resolve(opts.file);
|
|
53
|
+
const rel = path.relative(process.cwd(), abs);
|
|
54
|
+
local = { fullPath: abs };
|
|
55
|
+
if (!rel.startsWith('..') && !path.isAbsolute(rel)) {
|
|
56
|
+
local.path = './' + rel;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let url;
|
|
61
|
+
if (opts.shortFlag) {
|
|
62
|
+
if (opts.subcommand !== 'share') {
|
|
63
|
+
console.error('sdoc: --short is only valid with the `share` subcommand');
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
if (!content) {
|
|
67
|
+
console.error('sdoc: --short needs content (a file path or piped stdin)');
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
url = await buildShortUrl(content, {
|
|
72
|
+
url: opts.url,
|
|
73
|
+
mode: opts.mode,
|
|
74
|
+
theme: opts.theme,
|
|
75
|
+
section: opts.section,
|
|
76
|
+
});
|
|
77
|
+
} catch (e) {
|
|
78
|
+
console.error('sdoc: could not create short link -', e.message);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
url = buildUrl(content, {
|
|
83
|
+
url: opts.url,
|
|
84
|
+
mode: opts.mode,
|
|
85
|
+
theme: opts.theme,
|
|
86
|
+
defaultStyles: !content ? defaults : null,
|
|
87
|
+
section: opts.section,
|
|
88
|
+
local,
|
|
89
|
+
present: opts.present,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { url, contentPresent: !!content };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Default flow: `sdoc <file>` or `sdoc` (no args, or piped stdin).
|
|
97
|
+
//
|
|
98
|
+
// The document travels in the URL hash and renders read-only-by-default in the
|
|
99
|
+
// browser; nothing connects back to disk. This is the everywhere-works path -
|
|
100
|
+
// no local socket, no browser permission prompt. The live, autosaving session
|
|
101
|
+
// (browser <-> file on disk) is opt-in via `sdoc bridge <file>`.
|
|
102
|
+
//
|
|
103
|
+
// The non-blocking, share-by-URL case is `sdoc share <file>`.
|
|
104
|
+
async function openCommand(opts) {
|
|
105
|
+
const { url } = await prepareUrl(opts);
|
|
106
|
+
openBrowser(url);
|
|
107
|
+
console.log(`SDocs → ${url.length > 80 ? url.slice(0, 77) + '...' : url}`);
|
|
108
|
+
await postCommandHooks();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function shareCommand(opts) {
|
|
112
|
+
const { url } = await prepareUrl(opts);
|
|
113
|
+
try {
|
|
114
|
+
const clip = process.platform === 'darwin' ? 'pbcopy'
|
|
115
|
+
: execSync('which xclip 2>/dev/null', { encoding: 'utf-8' }).trim() ? 'xclip -selection clipboard'
|
|
116
|
+
: 'xsel --clipboard --input';
|
|
117
|
+
execSync(clip, { input: url, stdio: ['pipe', 'ignore', 'ignore'] });
|
|
118
|
+
const name = opts.file ? path.basename(opts.file) : 'stdin';
|
|
119
|
+
const label = opts.shortFlag ? 'Short link' : 'Link';
|
|
120
|
+
console.log(`✓ ${label} for ${name} copied to clipboard`);
|
|
121
|
+
if (opts.shortFlag) console.log(` ${url}`);
|
|
122
|
+
} catch (_) {
|
|
123
|
+
process.stdout.write(url + '\n');
|
|
124
|
+
}
|
|
125
|
+
await postCommandHooks();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function defaultsCommand(opts) {
|
|
129
|
+
if (opts.resetFlag) resetDefaults();
|
|
130
|
+
else showDefaults();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// `sdoc color-analysis <file>` — grade every text-on-background pair in the
|
|
134
|
+
// document's custom palette against WCAG ratios, for both the light and dark
|
|
135
|
+
// themes. Exits 1 if anything is unreadable so an agent (or CI) notices.
|
|
136
|
+
async function colorAnalysisCommand(opts) {
|
|
137
|
+
const SDocContrast = require('../shared/sdocs-contrast.js');
|
|
138
|
+
const content = await readContent(opts.file);
|
|
139
|
+
if (!content) {
|
|
140
|
+
console.error('sdoc color-analysis: pass a markdown file (or pipe one in)');
|
|
141
|
+
console.error(' e.g. sdoc color-analysis report.md');
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
const meta = SDocYaml.parseFrontMatter(content).meta || {};
|
|
145
|
+
const styles = meta.styles || null;
|
|
146
|
+
const name = opts.file ? path.basename(opts.file) : 'stdin';
|
|
147
|
+
|
|
148
|
+
if (!styles || !SDocContrast.hasCustomColors(styles)) {
|
|
149
|
+
console.log(`sdoc color-analysis: ${name}`);
|
|
150
|
+
console.log(' No custom colours set - the built-in palette is contrast-safe in both themes.');
|
|
151
|
+
process.exit(0);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const a = SDocContrast.analyzeStyles(styles);
|
|
155
|
+
const pad = (s, n) => (s + ' '.repeat(n)).slice(0, n);
|
|
156
|
+
const minRatio = SDocContrast.MIN_CONTRAST;
|
|
157
|
+
function line(p) {
|
|
158
|
+
const tag = p.ok ? 'ok ' : 'FAIL';
|
|
159
|
+
const ratio = p.ratio == null ? ' ? ' : (p.ratio.toFixed(2) + ':1');
|
|
160
|
+
return ` ${tag} ${pad(p.label, 16)} ${pad(p.fg + ' on ' + p.bg, 22)} ${pad(ratio, 9)} ${p.ok ? '' : '(needs ' + minRatio + ':1)'}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
console.log(`sdoc color-analysis: ${name}\n`);
|
|
164
|
+
console.log('LIGHT THEME');
|
|
165
|
+
a.light.forEach(p => console.log(line(p)));
|
|
166
|
+
console.log('\nDARK THEME');
|
|
167
|
+
a.dark.forEach(p => console.log(line(p)));
|
|
168
|
+
|
|
169
|
+
console.log('');
|
|
170
|
+
if (a.fails.length === 0) {
|
|
171
|
+
console.log('All text/background pairs meet WCAG AA. ✓');
|
|
172
|
+
process.exit(0);
|
|
173
|
+
}
|
|
174
|
+
console.log(`${a.fails.length} unreadable pair${a.fails.length === 1 ? '' : 's'} (contrast below ${minRatio}:1).`);
|
|
175
|
+
console.log('Fix the flagged colours, or add a `dark:` override so the dark theme has its own readable values.');
|
|
176
|
+
console.log('Reminder: top-level colours are the LIGHT theme; dark mode is auto-derived unless you set `dark:`.');
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function newCommand(opts) {
|
|
181
|
+
const baseUrl = opts.url || process.env.SDOCS_URL || DEFAULT_URL;
|
|
182
|
+
const url = baseUrl + '/new';
|
|
183
|
+
openBrowser(url);
|
|
184
|
+
console.log(`SDocs → ${url}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// `sdoc slides` family. Dispatches on the positional after `slides`:
|
|
188
|
+
// sdoc slides -> prints SLIDES_HELP
|
|
189
|
+
// sdoc slides list -> built-in template registry
|
|
190
|
+
// sdoc slides custom-shapes -> raw-shape reference
|
|
191
|
+
// sdoc slides icons [query] -> Lucide icon name listing
|
|
192
|
+
function slidesCommand(opts) {
|
|
193
|
+
const helpText = require('./help-text');
|
|
194
|
+
const sub = opts.file;
|
|
195
|
+
if (sub === 'list') { printSlideStdlib(); return; }
|
|
196
|
+
if (sub === 'custom-shapes') { console.log(helpText.SLIDES_CUSTOM_SHAPES_HELP); return; }
|
|
197
|
+
if (sub === 'icons') { printIconList(opts.extra); return; }
|
|
198
|
+
console.log(helpText.SLIDES_HELP);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// `sdoc present <file>` opens the file straight into fullscreen slide
|
|
202
|
+
// view. Delegates to openCommand with `present: true` set so the URL
|
|
203
|
+
// gets `&present=0` and the browser auto-enters present mode on load.
|
|
204
|
+
function presentCommand(opts) {
|
|
205
|
+
return openCommand(Object.assign({}, opts, { present: true }));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function printSlideStdlib() {
|
|
209
|
+
// Require lazily so the browser-side slide stdlib (which uses window
|
|
210
|
+
// globals) is only loaded when this command actually runs.
|
|
211
|
+
const SDocSlideStdlib = require('../../public/sdocs-slide-stdlib.js');
|
|
212
|
+
const names = SDocSlideStdlib.names || Object.keys(SDocSlideStdlib.templates || {});
|
|
213
|
+
const slots = SDocSlideStdlib.slots || {};
|
|
214
|
+
console.log('Built-in slide templates');
|
|
215
|
+
console.log('========================');
|
|
216
|
+
const pad = 22;
|
|
217
|
+
for (let i = 0; i < names.length; i++) {
|
|
218
|
+
const n = names[i];
|
|
219
|
+
let label = '@extends ' + n;
|
|
220
|
+
while (label.length < pad) label += ' ';
|
|
221
|
+
const slotList = (slots[n] || []).join(', ');
|
|
222
|
+
console.log(label + ' ' + slotList);
|
|
223
|
+
}
|
|
224
|
+
console.log('');
|
|
225
|
+
console.log('`!` marks a required slot (resolver errors when omitted).');
|
|
226
|
+
console.log('Use a built-in by adding `@extends <name>` to a slide block.');
|
|
227
|
+
console.log('Define a user @template with the same name to override (you\'ll get a warning).');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function printIconList(query) {
|
|
231
|
+
let names;
|
|
232
|
+
try {
|
|
233
|
+
// The manifest sits next to this file (via cli/bin/). Require by
|
|
234
|
+
// resolved path so it works whether we're invoked from a globally
|
|
235
|
+
// installed binary or from a checkout.
|
|
236
|
+
names = require('../bin/sdocs-icon-names.js');
|
|
237
|
+
} catch (e) {
|
|
238
|
+
console.error('sdoc: icon names manifest missing (cli/bin/sdocs-icon-names.js).');
|
|
239
|
+
console.error('Run `node scripts/build-icons.js` to generate it.');
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const q = (query || '').toLowerCase().trim();
|
|
244
|
+
const matches = q ? names.filter(n => n.indexOf(q) !== -1) : names;
|
|
245
|
+
|
|
246
|
+
if (q && matches.length === 0) {
|
|
247
|
+
console.log('No Lucide icons match "' + query + '".');
|
|
248
|
+
console.log('Browse the full set at https://lucide.dev/icons/ or run `sdoc slides icons` to list everything.');
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (q) {
|
|
253
|
+
console.log('Lucide icons matching "' + query + '" (' + matches.length + ' of ' + names.length + ')');
|
|
254
|
+
} else {
|
|
255
|
+
console.log('Lucide icons available to the `icon` shape kind (' + names.length + ' total)');
|
|
256
|
+
}
|
|
257
|
+
console.log('Source: https://lucide.dev/icons/ - use `name=<icon>` in slides');
|
|
258
|
+
console.log('');
|
|
259
|
+
|
|
260
|
+
const longest = matches.reduce((m, n) => n.length > m ? n.length : m, 0);
|
|
261
|
+
const colWidth = longest + 2;
|
|
262
|
+
const cols = 4;
|
|
263
|
+
const rows = Math.ceil(matches.length / cols);
|
|
264
|
+
for (let r = 0; r < rows; r++) {
|
|
265
|
+
let line = '';
|
|
266
|
+
for (let c = 0; c < cols; c++) {
|
|
267
|
+
const idx = c * rows + r;
|
|
268
|
+
if (idx >= matches.length) break;
|
|
269
|
+
let name = matches[idx];
|
|
270
|
+
while (name.length < colWidth) name += ' ';
|
|
271
|
+
line += name;
|
|
272
|
+
}
|
|
273
|
+
console.log(line.replace(/\s+$/, ''));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (!q) {
|
|
277
|
+
console.log('');
|
|
278
|
+
console.log('Tip: filter with `sdoc slides icons <substring>` (e.g. `sdoc slides icons cloud`).');
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
module.exports = {
|
|
283
|
+
prepareUrl,
|
|
284
|
+
openCommand,
|
|
285
|
+
shareCommand,
|
|
286
|
+
defaultsCommand,
|
|
287
|
+
colorAnalysisCommand,
|
|
288
|
+
newCommand,
|
|
289
|
+
slidesCommand,
|
|
290
|
+
presentCommand,
|
|
291
|
+
};
|