sdocs-dev 1.13.0 → 1.14.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 +1 -1
- package/bin/sdocs-dev.js +1 -0
- package/lib/agent-block.js +4 -3
- package/lib/bridge-commands.js +19 -3
- package/lib/cells-transclude.js +0 -22
- package/lib/cells-verify.js +48 -22
- package/lib/code-langs.js +85 -0
- package/lib/commands.js +11 -3
- package/lib/file-wrap.js +39 -0
- package/lib/help-text.js +96 -5
- package/lib/io.js +27 -3
- package/package.json +2 -2
- package/shared/sdocs-cells.js +21 -5
package/bin/sdocs-bridge.js
CHANGED
|
@@ -26,7 +26,7 @@ const fs = require('fs');
|
|
|
26
26
|
const path = require('path');
|
|
27
27
|
const crypto = require('crypto');
|
|
28
28
|
const FormBlock = require('../shared/sdocs-form-block.js');
|
|
29
|
-
const { isWrappedFile, wrapForDisplay } = require('../lib/
|
|
29
|
+
const { isWrappedFile, wrapForDisplay } = require('../lib/file-wrap');
|
|
30
30
|
|
|
31
31
|
// ── Constants ─────────────────────────────────────────────
|
|
32
32
|
|
package/bin/sdocs-dev.js
CHANGED
|
@@ -54,6 +54,7 @@ function buildRouter() {
|
|
|
54
54
|
if ((opts.file || '').toLowerCase() === 'verify') return cellsVerify.cellsVerifyCommand(opts);
|
|
55
55
|
console.log(helpText.CELLS_HELP); process.exit(0);
|
|
56
56
|
} });
|
|
57
|
+
r.register('code', { handler: () => { console.log(helpText.CODE_HELP); process.exit(0); } });
|
|
57
58
|
r.register('comments', { handler: () => { console.log(helpText.COMMENTS_HELP); process.exit(0); } });
|
|
58
59
|
|
|
59
60
|
// Setup / refresh / auto-update.
|
package/lib/agent-block.js
CHANGED
|
@@ -18,8 +18,8 @@ const fs = require('fs');
|
|
|
18
18
|
const path = require('path');
|
|
19
19
|
const { SETUP_CACHE } = require('./constants');
|
|
20
20
|
|
|
21
|
-
const AGENT_BLOCK_VERSION =
|
|
22
|
-
const AGENT_BLOCK_REASON = '
|
|
21
|
+
const AGENT_BLOCK_VERSION = 12;
|
|
22
|
+
const AGENT_BLOCK_REASON = 'Adds `sdoc code`: opening a source file (sdoc app.rb) or a ```lang fenced block as a syntax-highlighted listing - a light code viewer for reading code with the user away from the IDE - with source comments given a prominent lane, a fullscreen view that folds by method/class, a comment mode for the user to annotate a line or method, and agent annotations (sdoc app.py 22:"...") that pin markdown callouts to lines so an agent can walk the user through code without editing it. One bullet added after the cells line; the rest of the block is unchanged from v11.';
|
|
23
23
|
|
|
24
24
|
const AGENT_BLOCK_BODY = `## SmallDocs
|
|
25
25
|
|
|
@@ -48,7 +48,8 @@ Each command below prints its reference when run with no arguments - run it befo
|
|
|
48
48
|
- \`sdoc charts\` - rendering inline charts (\`\`\`chart blocks)
|
|
49
49
|
- \`sdoc diagrams\` - rendering inline Mermaid diagrams (\`\`\`mermaid blocks; has full-screen mode for zoom). Reach for this when drawing system or architectural diagrams (sequence, flow, component layout) - a diagram often communicates the shape of something faster than the equivalent prose.
|
|
50
50
|
- \`sdoc slides\` - inline slide decks (\`\`\`slide / ~~~slide blocks; has full-screen presentation mode). Slides can be standalone exported as \`.pdf\` or \`.pptx\`. \`sdoc present file.md\` - open file directly in fullscreen presentation mode.
|
|
51
|
-
- \`sdoc cells\` - rendering spreadsheets (\`\`\`cells blocks): CSV rows where plain values and =formulas (SUM, AVERAGE, IF, ROUND...) sit in the same grid and compute live. The reader can sort, select ranges for quick stats, edit a scratch copy fullscreen, and download the sheet as Excel (.xlsx) with the formulas still working. Reach for this when handing the user numbers they will want to check or play with - totals, budgets, projections. \`sdoc report.csv\` opens a CSV file directly as a sheet.
|
|
51
|
+
- \`sdoc cells\` - rendering spreadsheets (\`\`\`cells blocks): CSV rows where plain values and =formulas (SUM, AVERAGE, IF, ROUND...) sit in the same grid and compute live. The reader can sort, select ranges for quick stats, edit a scratch copy fullscreen, and download the sheet as Excel (.xlsx) with the formulas still working. Name a block (\`\`\`cells Expenses) to build a workbook of several tabs whose formulas reference each other across sheets (\`=Expenses!B4\`); run \`sdoc cells verify file.md\` to compute the whole workbook headlessly and read the values back. Reach for this when handing the user numbers they will want to check or play with - totals, budgets, projections. \`sdoc report.csv\` opens a CSV file directly as a sheet.
|
|
52
|
+
- \`sdoc code\` - opening a source file or a fenced code block as a syntax-highlighted listing: a light code viewer for reading code with the user away from the IDE. \`sdoc app.rb\` (or \`.js\`, \`.py\`, \`.go\`, \`.rs\`, \`.ts\`...) opens a file as a highlighted listing; a \`\`\`lang fenced block is highlighted inline. Comments in the source get a prominent lane so the code reads clearly top to bottom. The fullscreen view adds a line-number gutter and language-aware folding (collapse a whole method or class); a comment mode lets the user annotate a line or method with review notes, kept in the browser rather than the file. You can also pin your own explanations to lines as you open a file - \`sdoc app.py 22:"this method has the bug" 25-28:"wrong comparison here"\` - and they render as markdown callouts below those lines, a way to walk the user through code without editing the source. The file rides in the URL like any document; nothing is uploaded. Reach for it when reading or reviewing code with the user, not for prose.
|
|
52
53
|
- \`sdoc schema\` - styling Markdown (fonts, colors, spacing). The default styles are already comfortable to read; reach for this only when they aren't enough - client-facing polish or a bit of fun.
|
|
53
54
|
- \`sdoc feedback\` - rendering interactive elements (\`\`\`form blocks) to receive structured input from the user. Run \`sdoc feedback file.md\` and the user's submission lands as a JSON line on stdout. Good for eliciting complex/subtle feedback. All standard interactive HTML elements with prefilled (but editable) content of your choosing.
|
|
54
55
|
`;
|
package/lib/bridge-commands.js
CHANGED
|
@@ -22,6 +22,8 @@ const { startBridge } = require('../bin/sdocs-bridge');
|
|
|
22
22
|
const { DEFAULT_URL } = require('./constants');
|
|
23
23
|
const { openBrowser } = require('./io');
|
|
24
24
|
const { stripAndCompress } = require('./url');
|
|
25
|
+
const { wrapForDisplay } = require('./file-wrap');
|
|
26
|
+
const { isCodeFile } = require('./code-langs');
|
|
25
27
|
|
|
26
28
|
function baseUrlFor(opts) {
|
|
27
29
|
return opts.url || process.env.SDOCS_URL || DEFAULT_URL;
|
|
@@ -41,7 +43,20 @@ function bridgeSnapshot(file) {
|
|
|
41
43
|
if (!file) return null;
|
|
42
44
|
if (WRAPPED_EXT.has(path.extname(file).toLowerCase())) return null;
|
|
43
45
|
try {
|
|
44
|
-
|
|
46
|
+
const raw = fs.readFileSync(file, 'utf-8');
|
|
47
|
+
// A code file's renderable document is the file wrapped in a fence (the same
|
|
48
|
+
// transform the live bridge applies). The snapshot must embed THAT, not the
|
|
49
|
+
// raw source: otherwise the read-only fallback - which is what every page
|
|
50
|
+
// refresh renders until the socket reconnects - shows the file as markdown
|
|
51
|
+
// (docstrings as prose, indented bodies as stray code blocks) instead of the
|
|
52
|
+
// fullscreen code view with its folding and comments. Carry the filename in
|
|
53
|
+
// front matter so the snapshot is self-contained: the code view names it and
|
|
54
|
+
// auto-expands without depending on the bridge's `file=` param surviving.
|
|
55
|
+
if (isCodeFile(file)) {
|
|
56
|
+
const doc = '---\nfile: ' + path.basename(file) + '\n---\n' + wrapForDisplay(raw, file);
|
|
57
|
+
return stripAndCompress(doc);
|
|
58
|
+
}
|
|
59
|
+
return stripAndCompress(raw);
|
|
45
60
|
} catch (_) {
|
|
46
61
|
return null;
|
|
47
62
|
}
|
|
@@ -166,6 +181,7 @@ function feedbackCommand(opts) { return runBridge(opts, 'feedback', 'Feedback o
|
|
|
166
181
|
module.exports = {
|
|
167
182
|
runBridgedOpen,
|
|
168
183
|
feedbackCommand,
|
|
169
|
-
buildBridgeUrl,
|
|
170
|
-
|
|
184
|
+
buildBridgeUrl, // for tests
|
|
185
|
+
bridgeSnapshot, // for tests
|
|
186
|
+
timeoutOpts, // for tests
|
|
171
187
|
};
|
package/lib/cells-transclude.js
CHANGED
|
@@ -86,29 +86,7 @@ function wrapCsvFile(csv, filename) {
|
|
|
86
86
|
String(csv).replace(/\s+$/, '') + '\n```\n';
|
|
87
87
|
}
|
|
88
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
89
|
module.exports = {
|
|
110
90
|
transcludeCells: transcludeCells,
|
|
111
91
|
wrapCsvFile: wrapCsvFile,
|
|
112
|
-
isWrappedFile: isWrappedFile,
|
|
113
|
-
wrapForDisplay: wrapForDisplay,
|
|
114
92
|
};
|
package/lib/cells-verify.js
CHANGED
|
@@ -31,30 +31,44 @@ function scanCellsBlocks(md) {
|
|
|
31
31
|
var blocks = [];
|
|
32
32
|
var m;
|
|
33
33
|
while ((m = re.exec(md))) {
|
|
34
|
-
blocks.push({
|
|
34
|
+
blocks.push({ info: (m[2] || '').replace(/"/g, ''), body: m[3] });
|
|
35
35
|
}
|
|
36
36
|
return blocks;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
// Parse + name every block into
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
39
|
+
// Parse + name + group every block into workbooks, mirroring the renderer.
|
|
40
|
+
// Each block's fence info is split into a workbook id and a sheet name (the
|
|
41
|
+
// first "/" separates them); a baked `workbook=` / `name=` directive is the
|
|
42
|
+
// fallback. Blocks bucket by workbook id in first-seen order; within each
|
|
43
|
+
// workbook a fence/directive name wins, else auto Sheet1/Sheet2..., and both
|
|
44
|
+
// the auto counter and duplicate detection reset per workbook. recalcWorkbook
|
|
45
|
+
// later resolves references only within one workbook, so a Sheet!A1 into
|
|
46
|
+
// another workbook reads #REF! - the same isolation the browser enforces.
|
|
43
47
|
function buildWorkbook(md) {
|
|
44
48
|
var blocks = scanCellsBlocks(md);
|
|
45
|
-
var
|
|
49
|
+
var order = []; // workbook ids, first-seen order
|
|
50
|
+
var byWb = {}; // id -> [{ name, model }]
|
|
46
51
|
var parseErrors = [];
|
|
47
|
-
var autoIdx = 0;
|
|
48
52
|
for (var i = 0; i < blocks.length; i++) {
|
|
49
53
|
var model;
|
|
50
54
|
try { model = CELLS.parseCells(blocks[i].body); }
|
|
51
55
|
catch (e) { parseErrors.push((e && e.message) || 'parse error'); continue; }
|
|
52
56
|
if (model.error) { parseErrors.push(model.error); continue; }
|
|
53
57
|
if (model.unresolved || model.empty) continue;
|
|
54
|
-
var
|
|
55
|
-
|
|
58
|
+
var fence = CELLS.parseFenceInfo(blocks[i].info || '');
|
|
59
|
+
var wbId = fence.workbook || (model.workbook != null ? String(model.workbook) : '');
|
|
60
|
+
var explicitName = fence.name || (model.name && String(model.name).trim()) || '';
|
|
61
|
+
if (!byWb[wbId]) { byWb[wbId] = []; order.push(wbId); }
|
|
62
|
+
byWb[wbId].push({ name: explicitName, model: model });
|
|
56
63
|
}
|
|
57
|
-
|
|
64
|
+
var workbooks = order.map(function (id) {
|
|
65
|
+
var autoIdx = 0;
|
|
66
|
+
var sheets = byWb[id].map(function (s) {
|
|
67
|
+
return { name: s.name || ('Sheet' + (++autoIdx)), model: s.model };
|
|
68
|
+
});
|
|
69
|
+
return { id: id, sheets: sheets };
|
|
70
|
+
});
|
|
71
|
+
return { workbooks: workbooks, parseErrors: parseErrors };
|
|
58
72
|
}
|
|
59
73
|
|
|
60
74
|
// Render one sheet's computed grid to a 2D array of display strings: a formula
|
|
@@ -111,17 +125,24 @@ async function cellsVerifyCommand(opts) {
|
|
|
111
125
|
}
|
|
112
126
|
|
|
113
127
|
var wb = buildWorkbook(content);
|
|
114
|
-
var fxGrids = FX.recalcWorkbook(wb.sheets.map(function (s) {
|
|
115
|
-
return { name: s.name, model: s.model };
|
|
116
|
-
}));
|
|
117
128
|
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
};
|
|
129
|
+
// Recalc each workbook in isolation, then flatten to a per-sheet view that
|
|
130
|
+
// remembers which workbook each sheet came from. --sheet filters it after.
|
|
131
|
+
var rendered = [];
|
|
132
|
+
wb.workbooks.forEach(function (book) {
|
|
133
|
+
var fxGrids = FX.recalcWorkbook(book.sheets.map(function (s) {
|
|
134
|
+
return { name: s.name, model: s.model };
|
|
135
|
+
}));
|
|
136
|
+
book.sheets.forEach(function (s, i) {
|
|
137
|
+
var errs = errorsFor(s.name, s.model, fxGrids[i] || []);
|
|
138
|
+
if (book.id) errs.forEach(function (e) { e.workbook = book.id; });
|
|
139
|
+
rendered.push({
|
|
140
|
+
workbook: book.id,
|
|
141
|
+
name: s.name,
|
|
142
|
+
values: valuesFor(s.model, fxGrids[i] || []),
|
|
143
|
+
errors: errs,
|
|
144
|
+
});
|
|
145
|
+
});
|
|
125
146
|
});
|
|
126
147
|
|
|
127
148
|
if (opts.sheetName) {
|
|
@@ -141,14 +162,19 @@ async function cellsVerifyCommand(opts) {
|
|
|
141
162
|
if (opts.jsonFlag) {
|
|
142
163
|
process.stdout.write(JSON.stringify({
|
|
143
164
|
ok: ok,
|
|
144
|
-
sheets: rendered.map(function (r) {
|
|
165
|
+
sheets: rendered.map(function (r) {
|
|
166
|
+
var o = { name: r.name, values: r.values };
|
|
167
|
+
if (r.workbook) o.workbook = r.workbook;
|
|
168
|
+
return o;
|
|
169
|
+
}),
|
|
145
170
|
errors: allErrors,
|
|
146
171
|
parseErrors: wb.parseErrors,
|
|
147
172
|
}, null, 2) + '\n');
|
|
148
173
|
} else {
|
|
149
174
|
if (!rendered.length) console.error('sdoc: no cells tabs found in ' + file);
|
|
150
175
|
var chunks = rendered.map(function (r) {
|
|
151
|
-
|
|
176
|
+
var banner = r.workbook ? ('# workbook: ' + r.workbook + ' / sheet: ' + r.name) : ('# sheet: ' + r.name);
|
|
177
|
+
return banner + '\n' + CELLS.serializeCsv(r.values);
|
|
152
178
|
});
|
|
153
179
|
if (chunks.length) process.stdout.write(chunks.join('\n') + '\n');
|
|
154
180
|
wb.parseErrors.forEach(function (e) { console.error('sdoc: cells parse error - ' + e); });
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// code-langs.js - map a source file's extension to a fenced code block.
|
|
2
|
+
//
|
|
3
|
+
// `sdoc app.rb` should open as a syntax-highlighted Ruby document, the same way
|
|
4
|
+
// `sdoc chart.mmd` opens as a diagram. The CLI can't highlight anything itself;
|
|
5
|
+
// it just wraps the file in a ```<lang> fence and the browser's highlight.js
|
|
6
|
+
// does the colouring. So all this module decides is: which fence label?
|
|
7
|
+
//
|
|
8
|
+
// The label is the highlight.js language name (or one of its aliases), so the
|
|
9
|
+
// fence the CLI writes is the same one a user would type by hand.
|
|
10
|
+
|
|
11
|
+
var path = require('path');
|
|
12
|
+
|
|
13
|
+
// Extension (no dot, lowercase) -> fence label. Markdown and plain text are
|
|
14
|
+
// intentionally absent: those open as documents, not as code listings.
|
|
15
|
+
var LANG_BY_EXT = {
|
|
16
|
+
js: 'javascript', mjs: 'javascript', cjs: 'javascript', jsx: 'javascript',
|
|
17
|
+
ts: 'typescript', tsx: 'typescript',
|
|
18
|
+
py: 'python', pyw: 'python',
|
|
19
|
+
rb: 'ruby', rake: 'ruby',
|
|
20
|
+
go: 'go',
|
|
21
|
+
rs: 'rust',
|
|
22
|
+
java: 'java',
|
|
23
|
+
kt: 'kotlin', kts: 'kotlin',
|
|
24
|
+
swift: 'swift',
|
|
25
|
+
c: 'c', h: 'c',
|
|
26
|
+
cpp: 'cpp', cc: 'cpp', cxx: 'cpp', hpp: 'cpp', hh: 'cpp',
|
|
27
|
+
cs: 'csharp',
|
|
28
|
+
php: 'php',
|
|
29
|
+
scala: 'scala',
|
|
30
|
+
ex: 'elixir', exs: 'elixir',
|
|
31
|
+
erl: 'erlang',
|
|
32
|
+
clj: 'clojure', cljs: 'clojure',
|
|
33
|
+
hs: 'haskell',
|
|
34
|
+
lua: 'lua',
|
|
35
|
+
pl: 'perl', pm: 'perl',
|
|
36
|
+
r: 'r',
|
|
37
|
+
dart: 'dart',
|
|
38
|
+
sh: 'bash', bash: 'bash', zsh: 'bash',
|
|
39
|
+
ps1: 'powershell',
|
|
40
|
+
sql: 'sql',
|
|
41
|
+
yml: 'yaml', yaml: 'yaml',
|
|
42
|
+
toml: 'toml',
|
|
43
|
+
ini: 'ini', cfg: 'ini', conf: 'ini',
|
|
44
|
+
json: 'json',
|
|
45
|
+
xml: 'xml', svg: 'xml',
|
|
46
|
+
html: 'xml', htm: 'xml',
|
|
47
|
+
css: 'css',
|
|
48
|
+
scss: 'scss', sass: 'scss',
|
|
49
|
+
less: 'less',
|
|
50
|
+
dockerfile: 'dockerfile',
|
|
51
|
+
diff: 'diff', patch: 'diff',
|
|
52
|
+
graphql: 'graphql', gql: 'graphql',
|
|
53
|
+
proto: 'protobuf'
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function extOf(filePath) {
|
|
57
|
+
var name = path.basename(String(filePath || '')).toLowerCase();
|
|
58
|
+
// Dotfiles named exactly like a known type, e.g. "Dockerfile".
|
|
59
|
+
if (name === 'dockerfile') return 'dockerfile';
|
|
60
|
+
var ext = path.extname(name);
|
|
61
|
+
return ext ? ext.slice(1) : '';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The highlight.js language for a path, or '' if we don't wrap it as code.
|
|
65
|
+
function langForFile(filePath) {
|
|
66
|
+
return LANG_BY_EXT[extOf(filePath)] || '';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isCodeFile(filePath) {
|
|
70
|
+
return !!langForFile(filePath);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// File contents -> a fenced code document. Trailing whitespace is trimmed so a
|
|
74
|
+
// file's final newline doesn't render as an empty last line in the block.
|
|
75
|
+
function wrapCodeFile(raw, filePath) {
|
|
76
|
+
var lang = langForFile(filePath);
|
|
77
|
+
return '```' + lang + '\n' + String(raw).replace(/\s+$/, '') + '\n```\n';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = {
|
|
81
|
+
LANG_BY_EXT: LANG_BY_EXT,
|
|
82
|
+
langForFile: langForFile,
|
|
83
|
+
isCodeFile: isCodeFile,
|
|
84
|
+
wrapCodeFile: wrapCodeFile
|
|
85
|
+
};
|
package/lib/commands.js
CHANGED
|
@@ -36,11 +36,19 @@ async function prepareUrl(opts) {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
// Inject `file:` into front matter (basename only — safe to share).
|
|
39
|
-
// Respects user-set file: if already present.
|
|
39
|
+
// Respects user-set file: if already present. Agent annotations
|
|
40
|
+
// (`sdoc app.py 22:"..."`) ride here too, in `annotations:`, so they travel
|
|
41
|
+
// with the link and through `sdoc share` (front matter rides in the body,
|
|
42
|
+
// unlike `local` which share strips).
|
|
40
43
|
if (content && opts.file) {
|
|
41
44
|
const parsed = SDocYaml.parseFrontMatter(content);
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
let changed = false;
|
|
46
|
+
if (!parsed.meta.file) { parsed.meta.file = path.basename(opts.file); changed = true; }
|
|
47
|
+
if (opts.annotations && opts.annotations.length) {
|
|
48
|
+
parsed.meta.annotations = opts.annotations;
|
|
49
|
+
changed = true;
|
|
50
|
+
}
|
|
51
|
+
if (changed) {
|
|
44
52
|
content = SDocYaml.serializeFrontMatter(parsed.meta) + '\n' + parsed.body;
|
|
45
53
|
}
|
|
46
54
|
}
|
package/lib/file-wrap.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// file-wrap.js - turn a file's contents into a renderable document.
|
|
2
|
+
//
|
|
3
|
+
// Some file types don't open as themselves: a .csv opens as a sheet, a .mmd as
|
|
4
|
+
// a diagram, a .rb as a highlighted listing. For those, the document SDocs
|
|
5
|
+
// renders is DERIVED from the file (its contents inside a fenced block) rather
|
|
6
|
+
// than the file itself. This module is the one dispatcher that decides which
|
|
7
|
+
// wrapping applies. Markdown and plain text are not wrapped - they open as-is.
|
|
8
|
+
//
|
|
9
|
+
// Both open paths go through here: readContent (the URL-snapshot path, in
|
|
10
|
+
// io.js) and the bridge (the live-sync path). The bridge also reads
|
|
11
|
+
// isWrappedFile to refuse saving a derived view back over the source file -
|
|
12
|
+
// that would overwrite the .csv / .mmd / .rb with fence markup.
|
|
13
|
+
|
|
14
|
+
const { wrapCsvFile } = require('./cells-transclude');
|
|
15
|
+
const codeLangs = require('./code-langs');
|
|
16
|
+
|
|
17
|
+
// A "wrapped" file is one whose renderable document is derived from a fence
|
|
18
|
+
// around its contents, not the raw file.
|
|
19
|
+
function isWrappedFile(filePath) {
|
|
20
|
+
var name = String(filePath || '');
|
|
21
|
+
return /\.(csv|mmd|mermaid)$/i.test(name) || codeLangs.isCodeFile(name);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// File contents -> renderable document. Wrapped types get their fence;
|
|
25
|
+
// everything else (markdown) passes through untouched.
|
|
26
|
+
function wrapForDisplay(raw, filePath) {
|
|
27
|
+
var name = String(filePath || '');
|
|
28
|
+
if (/\.(mmd|mermaid)$/i.test(name)) {
|
|
29
|
+
return '```mermaid\n' + String(raw).replace(/\s+$/, '') + '\n```\n';
|
|
30
|
+
}
|
|
31
|
+
if (/\.csv$/i.test(name)) return wrapCsvFile(raw, name);
|
|
32
|
+
if (codeLangs.isCodeFile(name)) return codeLangs.wrapCodeFile(raw, name);
|
|
33
|
+
return String(raw);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = {
|
|
37
|
+
isWrappedFile: isWrappedFile,
|
|
38
|
+
wrapForDisplay: wrapForDisplay,
|
|
39
|
+
};
|
package/lib/help-text.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Long help strings printed by `sdoc help`, `sdoc schema`, `sdoc charts`,
|
|
2
|
-
// `sdoc diagrams`, `sdoc comments`. Kept as plain data with no logic.
|
|
2
|
+
// `sdoc diagrams`, `sdoc code`, `sdoc comments`. Kept as plain data with no logic.
|
|
3
3
|
|
|
4
4
|
const HELP = `
|
|
5
5
|
SmallDocs CLI
|
|
@@ -22,6 +22,8 @@ USAGE
|
|
|
22
22
|
sdoc charts Chart types, options, and styling guide
|
|
23
23
|
sdoc diagrams Mermaid diagrams reference (\`\`\`mermaid blocks)
|
|
24
24
|
sdoc cells Inline spreadsheet reference (\`\`\`cells blocks)
|
|
25
|
+
sdoc code Syntax highlighting + code-viewer reference
|
|
26
|
+
sdoc app.rb / server.js / ... Open a source file as a highlighted listing
|
|
25
27
|
sdoc color-analysis <file> Check custom colours for readable contrast
|
|
26
28
|
(both themes). Run after styling a doc.
|
|
27
29
|
sdoc comments Comment-format reference (for agents)
|
|
@@ -1047,6 +1049,32 @@ MULTIPLE TABS (SHEETS)
|
|
|
1047
1049
|
tabs collapse into a single widget (a tab strip + one grid at a time),
|
|
1048
1050
|
placed where the first tab sat.
|
|
1049
1051
|
|
|
1052
|
+
WORKBOOK GROUPS (independent tab sets in one document)
|
|
1053
|
+
Name a workbook before the sheet to split a document into separate,
|
|
1054
|
+
independent workbooks. The part before the first "/" is the workbook, the
|
|
1055
|
+
part after is the sheet name:
|
|
1056
|
+
|
|
1057
|
+
\`\`\`cells financials/Model
|
|
1058
|
+
Line,2025,2026
|
|
1059
|
+
Revenue,=Drivers!B2,=Drivers!C2
|
|
1060
|
+
\`\`\`
|
|
1061
|
+
|
|
1062
|
+
\`\`\`cells financials/Drivers
|
|
1063
|
+
Driver,2025,2026
|
|
1064
|
+
Seats,800,2500
|
|
1065
|
+
\`\`\`
|
|
1066
|
+
|
|
1067
|
+
Blocks that share a workbook id form one workbook: their own tab strip (under
|
|
1068
|
+
cells-tabs: tabbed), their own sheet-name namespace, and their own formula
|
|
1069
|
+
scope. Two workbooks on a page render as two separate tab strips. A Sheet!A1
|
|
1070
|
+
reference into another workbook reads #REF! - the isolation is total, so two
|
|
1071
|
+
workbooks may reuse a sheet name without colliding. A block with no "/" stays
|
|
1072
|
+
in the default workbook and behaves exactly as before.
|
|
1073
|
+
|
|
1074
|
+
In the fullscreen view a "Download workbook" button exports the whole
|
|
1075
|
+
workbook as one .xlsx, every sheet and its cross-sheet formulas intact; the
|
|
1076
|
+
per-sheet download on each grid still exports just that one sheet.
|
|
1077
|
+
|
|
1050
1078
|
VERIFYING (for agents)
|
|
1051
1079
|
Check the computed values without a browser:
|
|
1052
1080
|
|
|
@@ -1060,9 +1088,11 @@ VERIFYING (for agents)
|
|
|
1060
1088
|
every tab computes cleanly and 1 when any cell errors, so an agent can gate
|
|
1061
1089
|
on it. Write formulas, run verify, read the values back, fix, repeat.
|
|
1062
1090
|
|
|
1063
|
-
The default output banners each tab with "# sheet: <name>"
|
|
1064
|
-
|
|
1065
|
-
|
|
1091
|
+
The default output banners each tab with "# sheet: <name>", or "# workbook:
|
|
1092
|
+
<id> / sheet: <name>" when the sheet sits in a named workbook (each workbook
|
|
1093
|
+
is verified in isolation). A data row could itself start with that text, so
|
|
1094
|
+
for machine parsing use --json (its per-tab values array, and each tab's
|
|
1095
|
+
optional workbook field, are unambiguous).
|
|
1066
1096
|
|
|
1067
1097
|
SORTING
|
|
1068
1098
|
Hover a column letter: an arrow appears on its right showing what a click
|
|
@@ -2756,4 +2786,65 @@ COMMON QUESTIONS
|
|
|
2756
2786
|
drop a \`.sdocsignore\` into the directory with the pattern.
|
|
2757
2787
|
`;
|
|
2758
2788
|
|
|
2759
|
-
|
|
2789
|
+
const CODE_HELP = `
|
|
2790
|
+
SDocs - Code (syntax highlighting)
|
|
2791
|
+
==================================
|
|
2792
|
+
Fenced code blocks are syntax-highlighted by language. Tag the fence with a
|
|
2793
|
+
language and the block is coloured in the browser; highlight.js is loaded from
|
|
2794
|
+
a CDN on first use, so a document with no code blocks pays nothing.
|
|
2795
|
+
|
|
2796
|
+
BASIC SYNTAX
|
|
2797
|
+
Tag the opening fence with a language name:
|
|
2798
|
+
|
|
2799
|
+
\`\`\`ruby
|
|
2800
|
+
def greet(name)
|
|
2801
|
+
# comments are styled to stand out, not fade away
|
|
2802
|
+
puts "hello, #{name}"
|
|
2803
|
+
end
|
|
2804
|
+
\`\`\`
|
|
2805
|
+
|
|
2806
|
+
An untagged fence stays plain (no guessing). Blocks claimed by other
|
|
2807
|
+
features - chart, mermaid, cells, form, math, slide - are never treated
|
|
2808
|
+
as code.
|
|
2809
|
+
|
|
2810
|
+
OPENING A SOURCE FILE
|
|
2811
|
+
Point sdoc at a source file and it opens as a highlighted listing:
|
|
2812
|
+
|
|
2813
|
+
sdoc app.rb
|
|
2814
|
+
sdoc server.js
|
|
2815
|
+
sdoc main.go
|
|
2816
|
+
|
|
2817
|
+
The file travels in the URL like any document; nothing is uploaded. The
|
|
2818
|
+
fence label is chosen from the extension (.rb -> ruby, .py -> python,
|
|
2819
|
+
.ts -> typescript, and so on). Markdown and plain text open as documents,
|
|
2820
|
+
not as code.
|
|
2821
|
+
|
|
2822
|
+
ANNOTATIONS (walk someone through the code)
|
|
2823
|
+
Pin a markdown explanation to a line as you open the file:
|
|
2824
|
+
|
|
2825
|
+
sdoc app.py 22:"this method is where the bug is"
|
|
2826
|
+
sdoc app.py 25-28:"the equality check compares the **wrong** value"
|
|
2827
|
+
|
|
2828
|
+
Each note renders as a callout below its line (a range stripes the lines it
|
|
2829
|
+
covers), with markdown inside - bold, inline code, links. They ride in the
|
|
2830
|
+
link and through \`sdoc share\`, and never touch the source file. This is for
|
|
2831
|
+
an agent explaining code to the reader; the in-browser comment mode
|
|
2832
|
+
(--comment) is the reverse - the reader's own review notes.
|
|
2833
|
+
|
|
2834
|
+
COMMENTS
|
|
2835
|
+
Comments are deliberately prominent - italic, full-contrast colour, a faint
|
|
2836
|
+
tint - rather than the usual muted grey, so the human explanation in a
|
|
2837
|
+
listing is the easiest part to read.
|
|
2838
|
+
|
|
2839
|
+
STYLING
|
|
2840
|
+
Token colours track the light/dark theme. The block background follows the
|
|
2841
|
+
same control as other code blocks (Blocks / Code in the style panel).
|
|
2842
|
+
|
|
2843
|
+
SUPPORTED LANGUAGES
|
|
2844
|
+
The common set ships in the core bundle (javascript, typescript, python,
|
|
2845
|
+
ruby, go, rust, java, c, cpp, csharp, php, bash, sql, yaml, json, xml,
|
|
2846
|
+
css, and more). Less common languages are fetched on demand the first time
|
|
2847
|
+
they appear. An unknown language label renders as plain text.
|
|
2848
|
+
`;
|
|
2849
|
+
|
|
2850
|
+
module.exports = { HELP, COMMENTS_HELP, SCHEMA, CHARTS_HELP, DIAGRAMS_HELP, CELLS_HELP, CODE_HELP, SLIDES_HELP, SLIDES_CUSTOM_SHAPES_HELP, LIBRARY_HELP };
|
package/lib/io.js
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { execFileSync } = require('child_process');
|
|
6
|
-
const { transcludeCells
|
|
6
|
+
const { transcludeCells } = require('./cells-transclude');
|
|
7
|
+
const { isWrappedFile, wrapForDisplay } = require('./file-wrap');
|
|
7
8
|
|
|
8
9
|
const SUBCOMMANDS = new Set([
|
|
9
10
|
'new', 'share', 'schema', 'defaults', 'help', 'version',
|
|
10
|
-
'charts', 'diagrams', 'cells', 'comments',
|
|
11
|
+
'charts', 'diagrams', 'cells', 'code', 'comments',
|
|
11
12
|
'setup', 'safe', 'auto-update', 'refresh', 'upgrade',
|
|
12
13
|
'bridge', 'feedback',
|
|
13
14
|
'slides', 'present',
|
|
@@ -20,6 +21,14 @@ const SUBCOMMANDS = new Set([
|
|
|
20
21
|
// front matter is the only place SDocs stores tags.
|
|
21
22
|
const TAG_ARG = /^\+[A-Za-z][\w-]{0,63}$/;
|
|
22
23
|
|
|
24
|
+
// Agent annotation arguments: `N:"text"` or `N-M:"text"` (the shell removes the
|
|
25
|
+
// quotes, so the token arrives as `22:this is the bug`). Line numbers are
|
|
26
|
+
// 1-based, matching what the reader sees in the gutter. Collected into an
|
|
27
|
+
// annotations list and written into the opened file's front matter so they
|
|
28
|
+
// travel with the link. The part before the colon must be digits (an optional
|
|
29
|
+
// range), so file paths like `app.py` never match.
|
|
30
|
+
const ANNOTATION_ARG = /^(\d+)(?:-(\d+))?:([\s\S]+)$/;
|
|
31
|
+
|
|
23
32
|
function parseArgs(argv) {
|
|
24
33
|
const args = argv || process.argv.slice(2);
|
|
25
34
|
let file = null;
|
|
@@ -46,6 +55,7 @@ function parseArgs(argv) {
|
|
|
46
55
|
let dryRunFlag = false;
|
|
47
56
|
let sheetName = null;
|
|
48
57
|
const addTags = [];
|
|
58
|
+
const annotations = [];
|
|
49
59
|
|
|
50
60
|
for (let i = 0; i < args.length; i++) {
|
|
51
61
|
const arg = args[i];
|
|
@@ -108,6 +118,20 @@ function parseArgs(argv) {
|
|
|
108
118
|
// front matter at open time.
|
|
109
119
|
if (TAG_ARG.test(arg)) { addTags.push(arg.slice(1).toLowerCase()); continue; }
|
|
110
120
|
|
|
121
|
+
// Agent annotation: `N:"text"` / `N-M:"text"`. Captured before the file /
|
|
122
|
+
// extra slots so a `22:...` token is never mistaken for a path.
|
|
123
|
+
const ann = ANNOTATION_ARG.exec(arg);
|
|
124
|
+
if (ann) {
|
|
125
|
+
const start = parseInt(ann[1], 10);
|
|
126
|
+
const end = ann[2] ? parseInt(ann[2], 10) : start;
|
|
127
|
+
// Strip one layer of surrounding quotes if a shell preserved them.
|
|
128
|
+
const text = ann[3].replace(/^"([\s\S]*)"$/, '$1').replace(/^'([\s\S]*)'$/, '$1');
|
|
129
|
+
if (start >= 1 && end >= start && text.trim()) {
|
|
130
|
+
annotations.push({ line: start, endLine: end, text });
|
|
131
|
+
}
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
111
135
|
if (!file) { file = arg; continue; }
|
|
112
136
|
// Second positional is captured as `extra` so `sdoc slides icons heart`
|
|
113
137
|
// gets {subcommand: 'slides', file: 'icons', extra: 'heart'}.
|
|
@@ -120,7 +144,7 @@ function parseArgs(argv) {
|
|
|
120
144
|
messageText, connectTimeoutS, idleTimeoutS, reconnectGraceMs,
|
|
121
145
|
keepOpenFlag, logFile,
|
|
122
146
|
tagsFlag, helpFlag, yesFlag, dryRunFlag, sheetName,
|
|
123
|
-
addTags,
|
|
147
|
+
addTags, annotations,
|
|
124
148
|
};
|
|
125
149
|
}
|
|
126
150
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sdocs-dev",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"description": "Open, share, and style markdown files from the terminal",
|
|
5
5
|
"main": "bin/sdocs-dev.js",
|
|
6
6
|
"bin": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"agents"
|
|
27
27
|
],
|
|
28
28
|
"author": "Josh Summers",
|
|
29
|
-
"license": "
|
|
29
|
+
"license": "Elastic-2.0",
|
|
30
30
|
"repository": {
|
|
31
31
|
"type": "git",
|
|
32
32
|
"url": "git+https://github.com/espressoplease/smalldocs.git"
|
package/shared/sdocs-cells.js
CHANGED
|
@@ -112,6 +112,20 @@
|
|
|
112
112
|
return meta;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
// Split a fence info string (everything after "cells ") into a workbook id
|
|
116
|
+
// and a sheet name. The first "/" separates them: "financials/Model" ->
|
|
117
|
+
// workbook "financials", name "Model". No slash -> default workbook "" and
|
|
118
|
+
// the whole string is the name. Everything after the first "/" is the name
|
|
119
|
+
// verbatim, so a sheet name may contain slashes; a workbook id may not. This
|
|
120
|
+
// is the one place both the browser renderer and the CLI split the fence, so
|
|
121
|
+
// the two never drift.
|
|
122
|
+
function parseFenceInfo(rest) {
|
|
123
|
+
var s = String(rest == null ? '' : rest).trim();
|
|
124
|
+
var slash = s.indexOf('/');
|
|
125
|
+
if (slash < 0) return { workbook: '', name: s };
|
|
126
|
+
return { workbook: s.slice(0, slash).trim(), name: s.slice(slash + 1).trim() };
|
|
127
|
+
}
|
|
128
|
+
|
|
115
129
|
// Build the grid model from a ```cells block body.
|
|
116
130
|
// Returns { rows, cols, cells, empty } where cells is row-major, every row
|
|
117
131
|
// padded to `cols` with empty cells so the grid is rectangular. May instead
|
|
@@ -130,7 +144,7 @@
|
|
|
130
144
|
// `name` is the tab name. Authored as the fence info string (```cells
|
|
131
145
|
// Sales); the renderer + CLI normalise that into this directive so the
|
|
132
146
|
// name has one home in the model.
|
|
133
|
-
var source, range, formats, name;
|
|
147
|
+
var source, range, formats, name, workbook;
|
|
134
148
|
var lines = trimmed.split('\n');
|
|
135
149
|
var idx = 0;
|
|
136
150
|
var FORMAT_RE = /^format:\s*(.*)$/i;
|
|
@@ -141,7 +155,8 @@
|
|
|
141
155
|
var meta = parseDirectives(dm[1]);
|
|
142
156
|
source = meta.source; range = meta.range;
|
|
143
157
|
if (meta.name != null) name = meta.name;
|
|
144
|
-
if (meta.
|
|
158
|
+
if (meta.workbook != null) workbook = meta.workbook;
|
|
159
|
+
if (meta.error) return { empty: false, error: meta.error, source: source, name: name, workbook: workbook };
|
|
145
160
|
idx++; continue;
|
|
146
161
|
}
|
|
147
162
|
if (fm) { formats = parseFormats(fm[1]); idx++; continue; }
|
|
@@ -151,8 +166,8 @@
|
|
|
151
166
|
|
|
152
167
|
// Unresolved reference (after directives): the CLI never baked it.
|
|
153
168
|
var ref = body.match(REFERENCE_RE);
|
|
154
|
-
if (ref) return { empty: false, unresolved: ref[1], formats: formats, name: name };
|
|
155
|
-
if (body === '') return { rows: 0, cols: 0, cells: [], empty: true, source: source, range: range, formats: formats, name: name };
|
|
169
|
+
if (ref) return { empty: false, unresolved: ref[1], formats: formats, name: name, workbook: workbook };
|
|
170
|
+
if (body === '') return { rows: 0, cols: 0, cells: [], empty: true, source: source, range: range, formats: formats, name: name, workbook: workbook };
|
|
156
171
|
|
|
157
172
|
var raw = parseCsv(body);
|
|
158
173
|
var cols = 0;
|
|
@@ -168,7 +183,7 @@
|
|
|
168
183
|
}
|
|
169
184
|
cells.push(out);
|
|
170
185
|
}
|
|
171
|
-
return { rows: raw.length, cols: cols, cells: cells, empty: false, source: source, range: range, formats: formats, name: name };
|
|
186
|
+
return { rows: raw.length, cols: cols, cells: cells, empty: false, source: source, range: range, formats: formats, name: name, workbook: workbook };
|
|
172
187
|
}
|
|
173
188
|
|
|
174
189
|
// Serialize a 2D array of raw cell strings back to CSV (RFC 4180 quoting:
|
|
@@ -378,6 +393,7 @@
|
|
|
378
393
|
exports.classify = classify;
|
|
379
394
|
exports.parseCsv = parseCsv;
|
|
380
395
|
exports.parseCells = parseCells;
|
|
396
|
+
exports.parseFenceInfo = parseFenceInfo;
|
|
381
397
|
exports.serializeCsv = serializeCsv;
|
|
382
398
|
exports.selectionStats = selectionStats;
|
|
383
399
|
exports.formatNumber = formatNumber;
|