nebula-notebook 0.1.1 → 0.2.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/README.md +90 -11
- package/dist/assets/errorwidget-C4r2j2DQ.js +5 -0
- package/dist/assets/fa-brands-400-CEJbCg16.woff +0 -0
- package/dist/assets/fa-brands-400-CSYNqBb_.ttf +0 -0
- package/dist/assets/fa-brands-400-DnkPfk3o.eot +0 -0
- package/dist/assets/fa-brands-400-UxlILjvJ.woff2 +0 -0
- package/dist/assets/fa-brands-400-cH1MgKbP.svg +3717 -0
- package/dist/assets/fa-regular-400-BhTwtT8w.eot +0 -0
- package/dist/assets/fa-regular-400-D1vz6WBx.ttf +0 -0
- package/dist/assets/fa-regular-400-DFnMcJPd.woff +0 -0
- package/dist/assets/fa-regular-400-DGzu1beS.woff2 +0 -0
- package/dist/assets/fa-regular-400-gwj8Pxq-.svg +801 -0
- package/dist/assets/fa-solid-900-B4ZZ7kfP.svg +5034 -0
- package/dist/assets/fa-solid-900-B6Axprfb.eot +0 -0
- package/dist/assets/fa-solid-900-BUswJgRo.woff2 +0 -0
- package/dist/assets/fa-solid-900-DOXgCApm.woff +0 -0
- package/dist/assets/fa-solid-900-mxuxnBEa.ttf +0 -0
- package/dist/assets/index-7-YBurka.js +716 -0
- package/dist/assets/index-BtWv4MIT.css +7 -0
- package/dist/assets/index-CFBUnxSZ.css +32 -0
- package/dist/assets/index-CsHoPQy-.js +1 -0
- package/dist/assets/index-D5w21_Z8.js +81 -0
- package/dist/assets/index-Day3QcNs.js +1 -0
- package/dist/assets/services-shim-D6p_A67v.js +33 -0
- package/dist/assets/viewlist-uomDf7I7.js +1 -0
- package/dist/assets/widgets-X7J3NxEn.css +1 -0
- package/dist/index.html +2 -2
- package/node-server/dist/cluster/client-registration.js +3 -0
- package/node-server/dist/cluster/kernel-proxy.js +24 -9
- package/node-server/dist/cluster/server-registry.d.ts +8 -0
- package/node-server/dist/cluster/server-registry.js +31 -7
- package/node-server/dist/fs/fs-service.d.ts +55 -7
- package/node-server/dist/fs/fs-service.js +489 -80
- package/node-server/dist/fs/notebook-formats/percent.d.ts +25 -0
- package/node-server/dist/fs/notebook-formats/percent.js +286 -0
- package/node-server/dist/fs/notebook-formats/qmd.d.ts +29 -0
- package/node-server/dist/fs/notebook-formats/qmd.js +307 -0
- package/node-server/dist/fs/notebook-formats/registry.d.ts +12 -0
- package/node-server/dist/fs/notebook-formats/registry.js +77 -0
- package/node-server/dist/fs/notebook-formats/types.d.ts +37 -0
- package/node-server/dist/fs/notebook-formats/types.js +13 -0
- package/node-server/dist/idle-exit.d.ts +52 -0
- package/node-server/dist/idle-exit.js +83 -0
- package/node-server/dist/index.js +129 -6
- package/node-server/dist/kernel/kernel-service.d.ts +113 -2
- package/node-server/dist/kernel/kernel-service.js +762 -60
- package/node-server/dist/notebook/headless-handler.d.ts +2 -0
- package/node-server/dist/notebook/headless-handler.js +70 -24
- package/node-server/dist/notebook/operation-router.js +7 -2
- package/node-server/dist/notebook/undoRedoManager.d.ts +4 -1
- package/node-server/dist/notebook/undoRedoManager.js +10 -2
- package/node-server/dist/output/display-data.js +2 -0
- package/node-server/dist/routes/cluster.js +2 -2
- package/node-server/dist/routes/compute.d.ts +8 -0
- package/node-server/dist/routes/compute.js +136 -0
- package/node-server/dist/routes/fs.js +2 -2
- package/node-server/dist/routes/kernel.js +111 -1
- package/node-server/dist/routes/notebook.js +29 -1
- package/node-server/dist/scheduler/allocation-service.d.ts +43 -0
- package/node-server/dist/scheduler/allocation-service.js +169 -0
- package/node-server/dist/scheduler/job-template.d.ts +30 -0
- package/node-server/dist/scheduler/job-template.js +85 -0
- package/node-server/dist/scheduler/mock-scheduler.d.ts +30 -0
- package/node-server/dist/scheduler/mock-scheduler.js +121 -0
- package/node-server/dist/scheduler/slurm-scheduler.d.ts +31 -0
- package/node-server/dist/scheduler/slurm-scheduler.js +393 -0
- package/node-server/dist/scheduler/types.d.ts +117 -0
- package/node-server/dist/scheduler/types.js +8 -0
- package/node-server/dist/scheduler/util.d.ts +7 -0
- package/node-server/dist/scheduler/util.js +20 -0
- package/node-server/dist/terminal/pty-manager.js +8 -0
- package/node-server/dist/terminal/server.js +43 -2
- package/node-server/dist/update-check.d.ts +20 -0
- package/node-server/dist/update-check.js +114 -0
- package/node-server/package.json +1 -0
- package/package.json +2 -1
- package/dist/assets/index-BvrHu37J.js +0 -648
- package/dist/assets/index-Dfj_xsKU.css +0 -32
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jupytext percent-format adapter for #-comment languages (.py, .R, .jl).
|
|
3
|
+
*
|
|
4
|
+
* The format is defined entirely in terms of `#` line comments (markers,
|
|
5
|
+
* header, commented markdown bodies), so the same parser/serializer works
|
|
6
|
+
* verbatim for any language that uses `#` comments — Python, R, Julia.
|
|
7
|
+
* The file extension only decides the default kernel (see registry).
|
|
8
|
+
*
|
|
9
|
+
* Format essentials:
|
|
10
|
+
* - optional jupytext-style header: `# ---` / `# `-prefixed YAML / `# ---`
|
|
11
|
+
* - `# %%` markers start cells; `[markdown]` / `[raw]` tag cell type;
|
|
12
|
+
* `id=<token>` carries the Nebula cell id (jupytext-legal key=value)
|
|
13
|
+
* - markdown/raw bodies are `# `-commented
|
|
14
|
+
* - outputs and execution counts are never serialized
|
|
15
|
+
*
|
|
16
|
+
* Anything on a marker line that we don't understand is preserved verbatim
|
|
17
|
+
* (cell._metadata.percent_header_rest) and re-emitted — never interpreted.
|
|
18
|
+
*/
|
|
19
|
+
import { NotebookFormatAdapter } from './types';
|
|
20
|
+
/** Content-based discrimination (the VS Code/jupytext convention): a .py
|
|
21
|
+
* file IS a percent notebook iff it has a `# %%` cell marker or a jupytext
|
|
22
|
+
* header. Marker-less files are plain scripts — Nebula must never write
|
|
23
|
+
* metadata into them as a side effect of merely opening them. */
|
|
24
|
+
export declare function isPercentNotebookText(text: string): boolean;
|
|
25
|
+
export declare const percentAdapter: NotebookFormatAdapter;
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* jupytext percent-format adapter for #-comment languages (.py, .R, .jl).
|
|
4
|
+
*
|
|
5
|
+
* The format is defined entirely in terms of `#` line comments (markers,
|
|
6
|
+
* header, commented markdown bodies), so the same parser/serializer works
|
|
7
|
+
* verbatim for any language that uses `#` comments — Python, R, Julia.
|
|
8
|
+
* The file extension only decides the default kernel (see registry).
|
|
9
|
+
*
|
|
10
|
+
* Format essentials:
|
|
11
|
+
* - optional jupytext-style header: `# ---` / `# `-prefixed YAML / `# ---`
|
|
12
|
+
* - `# %%` markers start cells; `[markdown]` / `[raw]` tag cell type;
|
|
13
|
+
* `id=<token>` carries the Nebula cell id (jupytext-legal key=value)
|
|
14
|
+
* - markdown/raw bodies are `# `-commented
|
|
15
|
+
* - outputs and execution counts are never serialized
|
|
16
|
+
*
|
|
17
|
+
* Anything on a marker line that we don't understand is preserved verbatim
|
|
18
|
+
* (cell._metadata.percent_header_rest) and re-emitted — never interpreted.
|
|
19
|
+
*/
|
|
20
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
23
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
24
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
25
|
+
}
|
|
26
|
+
Object.defineProperty(o, k2, desc);
|
|
27
|
+
}) : (function(o, m, k, k2) {
|
|
28
|
+
if (k2 === undefined) k2 = k;
|
|
29
|
+
o[k2] = m[k];
|
|
30
|
+
}));
|
|
31
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
32
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
33
|
+
}) : function(o, v) {
|
|
34
|
+
o["default"] = v;
|
|
35
|
+
});
|
|
36
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
37
|
+
var ownKeys = function(o) {
|
|
38
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
39
|
+
var ar = [];
|
|
40
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
41
|
+
return ar;
|
|
42
|
+
};
|
|
43
|
+
return ownKeys(o);
|
|
44
|
+
};
|
|
45
|
+
return function (mod) {
|
|
46
|
+
if (mod && mod.__esModule) return mod;
|
|
47
|
+
var result = {};
|
|
48
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
49
|
+
__setModuleDefault(result, mod);
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
52
|
+
})();
|
|
53
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
|
+
exports.percentAdapter = void 0;
|
|
55
|
+
exports.isPercentNotebookText = isPercentNotebookText;
|
|
56
|
+
const YAML = __importStar(require("yaml"));
|
|
57
|
+
const MARKER_RE = /^#\s*%%(.*)$/;
|
|
58
|
+
// Accept quoted (proper jupytext key="value" metadata) and bare id tokens
|
|
59
|
+
const ID_TOKEN_RE = /(^|\s)id="?([A-Za-z0-9_-]+)"?/;
|
|
60
|
+
function normalizeEol(text) {
|
|
61
|
+
return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
62
|
+
}
|
|
63
|
+
function parseMarkerRemainder(remainder) {
|
|
64
|
+
let rest = remainder;
|
|
65
|
+
let cellType = 'code';
|
|
66
|
+
if (/(^|\s)\[markdown\](\s|$)/.test(rest)) {
|
|
67
|
+
cellType = 'markdown';
|
|
68
|
+
rest = rest.replace(/(^|\s)\[markdown\](?=\s|$)/, '$1');
|
|
69
|
+
}
|
|
70
|
+
else if (/(^|\s)\[raw\](\s|$)/.test(rest)) {
|
|
71
|
+
cellType = 'raw';
|
|
72
|
+
rest = rest.replace(/(^|\s)\[raw\](?=\s|$)/, '$1');
|
|
73
|
+
}
|
|
74
|
+
let id = null;
|
|
75
|
+
const idMatch = rest.match(ID_TOKEN_RE);
|
|
76
|
+
if (idMatch) {
|
|
77
|
+
id = idMatch[2];
|
|
78
|
+
rest = rest.replace(ID_TOKEN_RE, '$1');
|
|
79
|
+
}
|
|
80
|
+
return { cellType, id, rest: rest.replace(/\s+/g, ' ').trim() };
|
|
81
|
+
}
|
|
82
|
+
/** Markdown/raw bodies are `# `-commented. A body line that would itself
|
|
83
|
+
* render as a cell marker is backslash-escaped (`\%%` ↔ `%%`) so the
|
|
84
|
+
* file-level scanner can never mistake it for a marker. Classical escape:
|
|
85
|
+
* serializer prepends one backslash to any leading `\*%%` run, parser
|
|
86
|
+
* removes one. Lossless for all inputs. */
|
|
87
|
+
function encodeCommentLine(line) {
|
|
88
|
+
const escaped = /^(\s*)(\\*)%%/.test(line)
|
|
89
|
+
? line.replace(/^(\s*)(\\*)%%/, '$1\\$2%%')
|
|
90
|
+
: line;
|
|
91
|
+
return escaped === '' ? '#' : `# ${escaped}`;
|
|
92
|
+
}
|
|
93
|
+
function decodeCommentLine(line) {
|
|
94
|
+
let body;
|
|
95
|
+
if (line === '#')
|
|
96
|
+
body = '';
|
|
97
|
+
else if (line.startsWith('# '))
|
|
98
|
+
body = line.slice(2);
|
|
99
|
+
else if (line.startsWith('#'))
|
|
100
|
+
body = line.slice(1);
|
|
101
|
+
else
|
|
102
|
+
body = line; // lenient: uncommented line inside a markdown body
|
|
103
|
+
return /^(\s*)\\(\\*)%%/.test(body) ? body.replace(/^(\s*)\\(\\*)%%/, '$1$2%%') : body;
|
|
104
|
+
}
|
|
105
|
+
function parseHeader(lines) {
|
|
106
|
+
if (lines[0] !== '# ---')
|
|
107
|
+
return { metadata: {}, kernelspecName: null, bodyStart: 0 };
|
|
108
|
+
let end = -1;
|
|
109
|
+
for (let i = 1; i < lines.length; i++) {
|
|
110
|
+
if (lines[i] === '# ---') {
|
|
111
|
+
end = i;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
if (!lines[i].startsWith('#'))
|
|
115
|
+
return { metadata: {}, kernelspecName: null, bodyStart: 0 };
|
|
116
|
+
}
|
|
117
|
+
if (end === -1)
|
|
118
|
+
return { metadata: {}, kernelspecName: null, bodyStart: 0 };
|
|
119
|
+
const yamlText = lines
|
|
120
|
+
.slice(1, end)
|
|
121
|
+
.map((l) => (l === '#' ? '' : l.startsWith('# ') ? l.slice(2) : l.slice(1)))
|
|
122
|
+
.join('\n');
|
|
123
|
+
let headerObj;
|
|
124
|
+
try {
|
|
125
|
+
const parsed = YAML.parse(yamlText);
|
|
126
|
+
headerObj = parsed && typeof parsed === 'object' ? parsed : {};
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Unparseable header: treat the whole block as content rather than lose it
|
|
130
|
+
return { metadata: {}, kernelspecName: null, bodyStart: 0 };
|
|
131
|
+
}
|
|
132
|
+
const jupyter = (headerObj.jupyter && typeof headerObj.jupyter === 'object'
|
|
133
|
+
? headerObj.jupyter
|
|
134
|
+
: {});
|
|
135
|
+
const metadata = { ...jupyter };
|
|
136
|
+
const extra = {};
|
|
137
|
+
for (const [k, v] of Object.entries(headerObj)) {
|
|
138
|
+
if (k !== 'jupyter')
|
|
139
|
+
extra[k] = v;
|
|
140
|
+
}
|
|
141
|
+
if (Object.keys(extra).length > 0)
|
|
142
|
+
metadata.__percent_header_extra = extra;
|
|
143
|
+
const kernelspec = metadata.kernelspec;
|
|
144
|
+
const kernelspecName = typeof kernelspec?.name === 'string' ? kernelspec.name : null;
|
|
145
|
+
// Skip one blank separator line after the header if present
|
|
146
|
+
let bodyStart = end + 1;
|
|
147
|
+
if (lines[bodyStart] === '')
|
|
148
|
+
bodyStart++;
|
|
149
|
+
return { metadata, kernelspecName, bodyStart };
|
|
150
|
+
}
|
|
151
|
+
function makeCell(index, info, bodyLines, trimSeparator) {
|
|
152
|
+
const cellType = info?.cellType ?? 'code';
|
|
153
|
+
// The serializer emits exactly one blank separator line between a cell body
|
|
154
|
+
// and the NEXT marker. Trim it only when a following marker caused this
|
|
155
|
+
// flush — at EOF there is no separator, so trailing blanks belong to the
|
|
156
|
+
// cell content itself.
|
|
157
|
+
const body = [...bodyLines];
|
|
158
|
+
if (trimSeparator && body[body.length - 1] === '')
|
|
159
|
+
body.pop();
|
|
160
|
+
const content = cellType === 'code'
|
|
161
|
+
? body.join('\n')
|
|
162
|
+
: body.map(decodeCommentLine).join('\n');
|
|
163
|
+
const cell = {
|
|
164
|
+
id: info?.id ?? `cell-${index}`,
|
|
165
|
+
type: cellType === 'code' ? 'code' : 'markdown',
|
|
166
|
+
content,
|
|
167
|
+
outputs: [],
|
|
168
|
+
executionCount: null,
|
|
169
|
+
isExecuting: false,
|
|
170
|
+
};
|
|
171
|
+
const meta = {};
|
|
172
|
+
if (cellType === 'raw')
|
|
173
|
+
meta.percent_cell_type = 'raw';
|
|
174
|
+
if (info && info.rest)
|
|
175
|
+
meta.percent_header_rest = info.rest;
|
|
176
|
+
if (Object.keys(meta).length > 0)
|
|
177
|
+
cell._metadata = meta;
|
|
178
|
+
return cell;
|
|
179
|
+
}
|
|
180
|
+
function parse(text) {
|
|
181
|
+
const lines = normalizeEol(text).split('\n');
|
|
182
|
+
// Drop the implicit final empty string from a trailing newline
|
|
183
|
+
if (lines.length > 0 && lines[lines.length - 1] === '')
|
|
184
|
+
lines.pop();
|
|
185
|
+
const { metadata, kernelspecName, bodyStart } = parseHeader(lines);
|
|
186
|
+
const cells = [];
|
|
187
|
+
let currentInfo = null;
|
|
188
|
+
let currentBody = [];
|
|
189
|
+
let sawMarker = false;
|
|
190
|
+
let started = false; // whether any non-blank content seen before first marker
|
|
191
|
+
const flush = (trimSeparator) => {
|
|
192
|
+
if (currentInfo === null && !started)
|
|
193
|
+
return; // nothing accumulated
|
|
194
|
+
cells.push(makeCell(cells.length, currentInfo, currentBody, trimSeparator));
|
|
195
|
+
};
|
|
196
|
+
for (let i = bodyStart; i < lines.length; i++) {
|
|
197
|
+
const line = lines[i];
|
|
198
|
+
const m = line.match(MARKER_RE);
|
|
199
|
+
if (m) {
|
|
200
|
+
if (sawMarker || started)
|
|
201
|
+
flush(true);
|
|
202
|
+
currentInfo = parseMarkerRemainder(m[1]);
|
|
203
|
+
currentBody = [];
|
|
204
|
+
sawMarker = true;
|
|
205
|
+
started = true;
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
if (!started && line.trim() !== '')
|
|
209
|
+
started = true;
|
|
210
|
+
currentBody.push(line);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (sawMarker || started)
|
|
214
|
+
flush(false);
|
|
215
|
+
return { cells, metadata, kernelspecName };
|
|
216
|
+
}
|
|
217
|
+
function serializeHeader(metadata, kernelName) {
|
|
218
|
+
const jupyter = {};
|
|
219
|
+
for (const [k, v] of Object.entries(metadata)) {
|
|
220
|
+
if (k !== '__percent_header_extra')
|
|
221
|
+
jupyter[k] = v;
|
|
222
|
+
}
|
|
223
|
+
if (kernelName) {
|
|
224
|
+
const existing = (jupyter.kernelspec && typeof jupyter.kernelspec === 'object'
|
|
225
|
+
? jupyter.kernelspec
|
|
226
|
+
: {});
|
|
227
|
+
jupyter.kernelspec = { ...existing, name: kernelName };
|
|
228
|
+
}
|
|
229
|
+
const extra = (metadata.__percent_header_extra ?? {});
|
|
230
|
+
const headerObj = { ...extra };
|
|
231
|
+
if (Object.keys(jupyter).length > 0)
|
|
232
|
+
headerObj.jupyter = jupyter;
|
|
233
|
+
if (Object.keys(headerObj).length === 0)
|
|
234
|
+
return [];
|
|
235
|
+
const yamlText = YAML.stringify(headerObj, { lineWidth: 0 }).trimEnd();
|
|
236
|
+
return [
|
|
237
|
+
'# ---',
|
|
238
|
+
...yamlText.split('\n').map((l) => (l === '' ? '#' : `# ${l}`)),
|
|
239
|
+
'# ---',
|
|
240
|
+
];
|
|
241
|
+
}
|
|
242
|
+
function serializeMarker(cell) {
|
|
243
|
+
const meta = cell._metadata ?? {};
|
|
244
|
+
const parts = [];
|
|
245
|
+
const rest = typeof meta.percent_header_rest === 'string' ? meta.percent_header_rest : '';
|
|
246
|
+
if (rest)
|
|
247
|
+
parts.push(rest);
|
|
248
|
+
if (meta.percent_cell_type === 'raw')
|
|
249
|
+
parts.push('[raw]');
|
|
250
|
+
else if (cell.type === 'markdown')
|
|
251
|
+
parts.push('[markdown]');
|
|
252
|
+
parts.push(`id="${cell.id}"`);
|
|
253
|
+
return `# %% ${parts.join(' ')}`;
|
|
254
|
+
}
|
|
255
|
+
function serialize(cells, metadata, kernelName) {
|
|
256
|
+
const out = serializeHeader(metadata, kernelName);
|
|
257
|
+
cells.forEach((cell, i) => {
|
|
258
|
+
// Exactly one blank separator before every marker except a file-leading one
|
|
259
|
+
if (out.length > 0)
|
|
260
|
+
out.push('');
|
|
261
|
+
out.push(serializeMarker(cell));
|
|
262
|
+
const contentLines = cell.content === '' ? [] : cell.content.split('\n');
|
|
263
|
+
if (cell.type === 'code')
|
|
264
|
+
out.push(...contentLines);
|
|
265
|
+
else
|
|
266
|
+
out.push(...contentLines.map(encodeCommentLine));
|
|
267
|
+
void i;
|
|
268
|
+
});
|
|
269
|
+
return out.join('\n') + '\n';
|
|
270
|
+
}
|
|
271
|
+
/** Content-based discrimination (the VS Code/jupytext convention): a .py
|
|
272
|
+
* file IS a percent notebook iff it has a `# %%` cell marker or a jupytext
|
|
273
|
+
* header. Marker-less files are plain scripts — Nebula must never write
|
|
274
|
+
* metadata into them as a side effect of merely opening them. */
|
|
275
|
+
function isPercentNotebookText(text) {
|
|
276
|
+
if (/^#\s*%%/m.test(text))
|
|
277
|
+
return true;
|
|
278
|
+
return text.startsWith('# ---\n');
|
|
279
|
+
}
|
|
280
|
+
exports.percentAdapter = {
|
|
281
|
+
name: 'percent',
|
|
282
|
+
extensions: ['.py', '.r', '.jl'],
|
|
283
|
+
capabilities: { storesOutputs: false, storesCellIds: true },
|
|
284
|
+
parse,
|
|
285
|
+
serialize,
|
|
286
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quarto adapter (.qmd) — notebook-first.
|
|
3
|
+
*
|
|
4
|
+
* A .qmd is treated as a notebook that doesn't serialize outputs:
|
|
5
|
+
* - optional YAML front matter (kept opaque; jupyter/nebula keys normalized)
|
|
6
|
+
* - ```{lang ...} fences are code cells; `#| id: x` carries the cell id;
|
|
7
|
+
* all other `#|` chunk options stay in cell content verbatim (they are
|
|
8
|
+
* valid comments) and are never interpreted
|
|
9
|
+
* - prose between fences becomes markdown cells
|
|
10
|
+
*
|
|
11
|
+
* No Quarto rendering, no chunk-option semantics, one kernel per document.
|
|
12
|
+
*
|
|
13
|
+
* Markdown cells are delimited by an HTML-comment marker mirroring the
|
|
14
|
+
* chunk-option syntax: `<!-- #| id: abc -->`. HTML comments are invisible in
|
|
15
|
+
* rendered Quarto output, so this is notebook-first without rendering cost:
|
|
16
|
+
* adjacent markdown cells stay distinct, empty markdown cells survive, and
|
|
17
|
+
* markdown ids persist (no positional fallback for Nebula-written files).
|
|
18
|
+
* Foreign .qmd files without markers still parse (one implicit markdown cell
|
|
19
|
+
* per prose block, positional ids); markers appear on first Nebula save.
|
|
20
|
+
*
|
|
21
|
+
* Known unrepresentables (inherent):
|
|
22
|
+
* - markdown content containing a bare ```{lang} fence-open line outside a
|
|
23
|
+
* wrapping plain fence is indistinguishable from a real code chunk (in
|
|
24
|
+
* Quarto itself too) and will re-parse as one
|
|
25
|
+
* - a markdown content line that is itself a `<!-- #| id: ... -->` marker
|
|
26
|
+
* will re-parse as a cell boundary
|
|
27
|
+
*/
|
|
28
|
+
import { NotebookFormatAdapter } from './types';
|
|
29
|
+
export declare const qmdAdapter: NotebookFormatAdapter;
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Quarto adapter (.qmd) — notebook-first.
|
|
4
|
+
*
|
|
5
|
+
* A .qmd is treated as a notebook that doesn't serialize outputs:
|
|
6
|
+
* - optional YAML front matter (kept opaque; jupyter/nebula keys normalized)
|
|
7
|
+
* - ```{lang ...} fences are code cells; `#| id: x` carries the cell id;
|
|
8
|
+
* all other `#|` chunk options stay in cell content verbatim (they are
|
|
9
|
+
* valid comments) and are never interpreted
|
|
10
|
+
* - prose between fences becomes markdown cells
|
|
11
|
+
*
|
|
12
|
+
* No Quarto rendering, no chunk-option semantics, one kernel per document.
|
|
13
|
+
*
|
|
14
|
+
* Markdown cells are delimited by an HTML-comment marker mirroring the
|
|
15
|
+
* chunk-option syntax: `<!-- #| id: abc -->`. HTML comments are invisible in
|
|
16
|
+
* rendered Quarto output, so this is notebook-first without rendering cost:
|
|
17
|
+
* adjacent markdown cells stay distinct, empty markdown cells survive, and
|
|
18
|
+
* markdown ids persist (no positional fallback for Nebula-written files).
|
|
19
|
+
* Foreign .qmd files without markers still parse (one implicit markdown cell
|
|
20
|
+
* per prose block, positional ids); markers appear on first Nebula save.
|
|
21
|
+
*
|
|
22
|
+
* Known unrepresentables (inherent):
|
|
23
|
+
* - markdown content containing a bare ```{lang} fence-open line outside a
|
|
24
|
+
* wrapping plain fence is indistinguishable from a real code chunk (in
|
|
25
|
+
* Quarto itself too) and will re-parse as one
|
|
26
|
+
* - a markdown content line that is itself a `<!-- #| id: ... -->` marker
|
|
27
|
+
* will re-parse as a cell boundary
|
|
28
|
+
*/
|
|
29
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
30
|
+
if (k2 === undefined) k2 = k;
|
|
31
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
32
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
33
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
34
|
+
}
|
|
35
|
+
Object.defineProperty(o, k2, desc);
|
|
36
|
+
}) : (function(o, m, k, k2) {
|
|
37
|
+
if (k2 === undefined) k2 = k;
|
|
38
|
+
o[k2] = m[k];
|
|
39
|
+
}));
|
|
40
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
41
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
42
|
+
}) : function(o, v) {
|
|
43
|
+
o["default"] = v;
|
|
44
|
+
});
|
|
45
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
46
|
+
var ownKeys = function(o) {
|
|
47
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
48
|
+
var ar = [];
|
|
49
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
50
|
+
return ar;
|
|
51
|
+
};
|
|
52
|
+
return ownKeys(o);
|
|
53
|
+
};
|
|
54
|
+
return function (mod) {
|
|
55
|
+
if (mod && mod.__esModule) return mod;
|
|
56
|
+
var result = {};
|
|
57
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
58
|
+
__setModuleDefault(result, mod);
|
|
59
|
+
return result;
|
|
60
|
+
};
|
|
61
|
+
})();
|
|
62
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
63
|
+
exports.qmdAdapter = void 0;
|
|
64
|
+
const YAML = __importStar(require("yaml"));
|
|
65
|
+
const CODE_FENCE_OPEN_RE = /^(`{3,})\{(\w+)([^}]*)\}\s*$/;
|
|
66
|
+
const PLAIN_FENCE_RE = /^(`{3,})(?!\{)/;
|
|
67
|
+
const ID_OPTION_RE = /^#\|\s*id:\s*(\S+)\s*$/;
|
|
68
|
+
const MD_MARKER_RE = /^<!--\s*#\|\s*id:\s*(\S+)\s*-->\s*$/;
|
|
69
|
+
const LANG_TO_KERNEL = {
|
|
70
|
+
python: 'python3',
|
|
71
|
+
r: 'ir',
|
|
72
|
+
};
|
|
73
|
+
function normalizeEol(text) {
|
|
74
|
+
return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
75
|
+
}
|
|
76
|
+
function parseFrontMatter(lines) {
|
|
77
|
+
if (lines[0] !== '---')
|
|
78
|
+
return { metadata: {}, kernelspecName: null, bodyStart: 0 };
|
|
79
|
+
let end = -1;
|
|
80
|
+
for (let i = 1; i < lines.length; i++) {
|
|
81
|
+
if (lines[i] === '---' || lines[i] === '...') {
|
|
82
|
+
end = i;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (end === -1)
|
|
87
|
+
return { metadata: {}, kernelspecName: null, bodyStart: 0 };
|
|
88
|
+
let fm;
|
|
89
|
+
try {
|
|
90
|
+
const parsed = YAML.parse(lines.slice(1, end).join('\n'));
|
|
91
|
+
fm = parsed && typeof parsed === 'object' ? parsed : {};
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return { metadata: {}, kernelspecName: null, bodyStart: 0 };
|
|
95
|
+
}
|
|
96
|
+
const metadata = { __qmd_front_matter: fm };
|
|
97
|
+
let kernelspecName = null;
|
|
98
|
+
const jupyter = fm.jupyter;
|
|
99
|
+
if (typeof jupyter === 'string') {
|
|
100
|
+
kernelspecName = jupyter;
|
|
101
|
+
metadata.kernelspec = { name: jupyter };
|
|
102
|
+
}
|
|
103
|
+
else if (jupyter && typeof jupyter === 'object') {
|
|
104
|
+
const kernelspec = jupyter.kernelspec;
|
|
105
|
+
if (kernelspec && typeof kernelspec === 'object') {
|
|
106
|
+
metadata.kernelspec = kernelspec;
|
|
107
|
+
const name = kernelspec.name;
|
|
108
|
+
if (typeof name === 'string')
|
|
109
|
+
kernelspecName = name;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (fm.nebula && typeof fm.nebula === 'object') {
|
|
113
|
+
metadata.nebula = fm.nebula;
|
|
114
|
+
}
|
|
115
|
+
let bodyStart = end + 1;
|
|
116
|
+
if (lines[bodyStart] === '')
|
|
117
|
+
bodyStart++;
|
|
118
|
+
return { metadata, kernelspecName, bodyStart };
|
|
119
|
+
}
|
|
120
|
+
function makeMarkdownCell(index, bodyLines, explicitId) {
|
|
121
|
+
// Trim one leading and one trailing blank line (block separators)
|
|
122
|
+
const body = [...bodyLines];
|
|
123
|
+
if (body[0] === '')
|
|
124
|
+
body.shift();
|
|
125
|
+
if (body[body.length - 1] === '')
|
|
126
|
+
body.pop();
|
|
127
|
+
// Implicit (marker-less, foreign-file) prose blocks that are empty are
|
|
128
|
+
// just whitespace between chunks; marker-delimited cells survive empty.
|
|
129
|
+
if (body.length === 0 && explicitId === null)
|
|
130
|
+
return null;
|
|
131
|
+
return {
|
|
132
|
+
id: explicitId ?? `cell-${index}`,
|
|
133
|
+
type: 'markdown',
|
|
134
|
+
content: body.join('\n'),
|
|
135
|
+
outputs: [],
|
|
136
|
+
executionCount: null,
|
|
137
|
+
isExecuting: false,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function parse(text) {
|
|
141
|
+
const lines = normalizeEol(text).split('\n');
|
|
142
|
+
if (lines.length > 0 && lines[lines.length - 1] === '')
|
|
143
|
+
lines.pop();
|
|
144
|
+
const { metadata, kernelspecName, bodyStart } = parseFrontMatter(lines);
|
|
145
|
+
const cells = [];
|
|
146
|
+
let prose = [];
|
|
147
|
+
let proseId = null;
|
|
148
|
+
let firstLanguage = null;
|
|
149
|
+
const flushProse = () => {
|
|
150
|
+
const cell = makeMarkdownCell(cells.length, prose, proseId);
|
|
151
|
+
if (cell)
|
|
152
|
+
cells.push(cell);
|
|
153
|
+
prose = [];
|
|
154
|
+
proseId = null;
|
|
155
|
+
};
|
|
156
|
+
let i = bodyStart;
|
|
157
|
+
while (i < lines.length) {
|
|
158
|
+
const line = lines[i];
|
|
159
|
+
const codeOpen = line.match(CODE_FENCE_OPEN_RE);
|
|
160
|
+
if (codeOpen) {
|
|
161
|
+
flushProse();
|
|
162
|
+
const ticks = codeOpen[1].length;
|
|
163
|
+
const language = codeOpen[2];
|
|
164
|
+
const fenceInner = `${codeOpen[2]}${codeOpen[3]}`;
|
|
165
|
+
if (!firstLanguage)
|
|
166
|
+
firstLanguage = language;
|
|
167
|
+
const body = [];
|
|
168
|
+
i++;
|
|
169
|
+
let closed = false;
|
|
170
|
+
while (i < lines.length) {
|
|
171
|
+
if (new RegExp(`^\`{${ticks},}\\s*$`).test(lines[i])) {
|
|
172
|
+
closed = true;
|
|
173
|
+
i++;
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
body.push(lines[i]);
|
|
177
|
+
i++;
|
|
178
|
+
}
|
|
179
|
+
void closed; // unterminated fence: treat rest of file as the cell body
|
|
180
|
+
// Consume a leading `#| id:` option as the cell id
|
|
181
|
+
let id = null;
|
|
182
|
+
if (body.length > 0) {
|
|
183
|
+
const idMatch = body[0].match(ID_OPTION_RE);
|
|
184
|
+
if (idMatch) {
|
|
185
|
+
id = idMatch[1];
|
|
186
|
+
body.shift();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const cell = {
|
|
190
|
+
id: id ?? `cell-${cells.length}`,
|
|
191
|
+
type: 'code',
|
|
192
|
+
content: body.join('\n'),
|
|
193
|
+
outputs: [],
|
|
194
|
+
executionCount: null,
|
|
195
|
+
isExecuting: false,
|
|
196
|
+
};
|
|
197
|
+
cell._metadata = {
|
|
198
|
+
qmd_fence_attrs: fenceInner,
|
|
199
|
+
};
|
|
200
|
+
cells.push(cell);
|
|
201
|
+
// Skip one blank separator after the closing fence
|
|
202
|
+
if (lines[i] === '')
|
|
203
|
+
i++;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const mdMarker = line.match(MD_MARKER_RE);
|
|
207
|
+
if (mdMarker) {
|
|
208
|
+
flushProse();
|
|
209
|
+
proseId = mdMarker[1];
|
|
210
|
+
i++;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const plainFence = line.match(PLAIN_FENCE_RE);
|
|
214
|
+
if (plainFence) {
|
|
215
|
+
// A plain fence inside prose: copy verbatim until its closer so that
|
|
216
|
+
// ```{lang} examples inside it are never mistaken for cell starts.
|
|
217
|
+
const ticks = plainFence[1].length;
|
|
218
|
+
prose.push(line);
|
|
219
|
+
i++;
|
|
220
|
+
while (i < lines.length) {
|
|
221
|
+
prose.push(lines[i]);
|
|
222
|
+
if (new RegExp(`^\`{${ticks},}\\s*$`).test(lines[i])) {
|
|
223
|
+
i++;
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
i++;
|
|
227
|
+
}
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
prose.push(line);
|
|
231
|
+
i++;
|
|
232
|
+
}
|
|
233
|
+
flushProse();
|
|
234
|
+
if (firstLanguage)
|
|
235
|
+
metadata.__qmd_language = firstLanguage;
|
|
236
|
+
let resolvedKernel = kernelspecName;
|
|
237
|
+
if (!resolvedKernel && firstLanguage) {
|
|
238
|
+
resolvedKernel = LANG_TO_KERNEL[firstLanguage.toLowerCase()] ?? null;
|
|
239
|
+
}
|
|
240
|
+
return { cells, metadata, kernelspecName: resolvedKernel };
|
|
241
|
+
}
|
|
242
|
+
function serializeFrontMatter(metadata, kernelName) {
|
|
243
|
+
const base = (metadata.__qmd_front_matter && typeof metadata.__qmd_front_matter === 'object'
|
|
244
|
+
? { ...metadata.__qmd_front_matter }
|
|
245
|
+
: {});
|
|
246
|
+
const kernelspec = (metadata.kernelspec && typeof metadata.kernelspec === 'object'
|
|
247
|
+
? { ...metadata.kernelspec }
|
|
248
|
+
: {});
|
|
249
|
+
if (kernelName)
|
|
250
|
+
kernelspec.name = kernelName;
|
|
251
|
+
if (Object.keys(kernelspec).length > 0) {
|
|
252
|
+
const jupyter = (base.jupyter && typeof base.jupyter === 'object'
|
|
253
|
+
? { ...base.jupyter }
|
|
254
|
+
: {});
|
|
255
|
+
jupyter.kernelspec = kernelspec;
|
|
256
|
+
base.jupyter = jupyter;
|
|
257
|
+
}
|
|
258
|
+
if (metadata.nebula && typeof metadata.nebula === 'object') {
|
|
259
|
+
base.nebula = metadata.nebula;
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
delete base.nebula;
|
|
263
|
+
}
|
|
264
|
+
if (Object.keys(base).length === 0)
|
|
265
|
+
return [];
|
|
266
|
+
const yamlText = YAML.stringify(base, { lineWidth: 0 }).trimEnd();
|
|
267
|
+
return ['---', ...yamlText.split('\n'), '---'];
|
|
268
|
+
}
|
|
269
|
+
function serialize(cells, metadata, kernelName) {
|
|
270
|
+
const language = typeof metadata.__qmd_language === 'string' ? metadata.__qmd_language : 'python';
|
|
271
|
+
const out = serializeFrontMatter(metadata, kernelName);
|
|
272
|
+
for (const cell of cells) {
|
|
273
|
+
if (out.length > 0)
|
|
274
|
+
out.push('');
|
|
275
|
+
if (cell.type === 'markdown') {
|
|
276
|
+
out.push(`<!-- #| id: ${cell.id} -->`);
|
|
277
|
+
if (cell.content !== '')
|
|
278
|
+
out.push(...cell.content.split('\n'));
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
const meta = cell._metadata ?? {};
|
|
282
|
+
const fenceInner = typeof meta.qmd_fence_attrs === 'string' ? meta.qmd_fence_attrs : language;
|
|
283
|
+
// Fence must be longer than any backtick run starting a content line,
|
|
284
|
+
// or the content would close it early.
|
|
285
|
+
let maxRun = 0;
|
|
286
|
+
for (const l of cell.content.split('\n')) {
|
|
287
|
+
const run = l.match(/^(`+)/);
|
|
288
|
+
if (run)
|
|
289
|
+
maxRun = Math.max(maxRun, run[1].length);
|
|
290
|
+
}
|
|
291
|
+
const fence = '`'.repeat(Math.max(3, maxRun + 1));
|
|
292
|
+
out.push(fence + '{' + fenceInner + '}');
|
|
293
|
+
out.push(`#| id: ${cell.id}`);
|
|
294
|
+
if (cell.content !== '')
|
|
295
|
+
out.push(...cell.content.split('\n'));
|
|
296
|
+
out.push(fence);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return out.join('\n') + '\n';
|
|
300
|
+
}
|
|
301
|
+
exports.qmdAdapter = {
|
|
302
|
+
name: 'qmd',
|
|
303
|
+
extensions: ['.qmd'],
|
|
304
|
+
capabilities: { storesOutputs: false, storesCellIds: true },
|
|
305
|
+
parse,
|
|
306
|
+
serialize,
|
|
307
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry for plain-text notebook format adapters.
|
|
3
|
+
*
|
|
4
|
+
* .ipynb is intentionally NOT registered here: dispatch sites check .ipynb
|
|
5
|
+
* first and fall through to the existing Jupyter JSON code path untouched.
|
|
6
|
+
*/
|
|
7
|
+
import { NotebookFormatAdapter } from './types';
|
|
8
|
+
export declare function getFormatAdapter(filePath: string): NotebookFormatAdapter | null;
|
|
9
|
+
export declare function defaultKernelForPath(filePath: string): string;
|
|
10
|
+
export declare function isTextNotebookPath(filePath: string): boolean;
|
|
11
|
+
/** True for any path Nebula can treat as a notebook (.ipynb or text format). */
|
|
12
|
+
export declare function isNotebookPath(filePath: string): boolean;
|