labnotes 0.0.51__tar.gz → 0.0.53__tar.gz
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.
- {labnotes-0.0.51 → labnotes-0.0.53}/PKG-INFO +1 -1
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/create_app.py +18 -5
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/experiment_notes.py +1 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/load_experiment.py +1 -0
- labnotes-0.0.53/src/labnotes/save_note.py +9 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/static/labnotes.css +24 -0
- labnotes-0.0.53/src/labnotes/static/note-edit.js +23 -0
- labnotes-0.0.53/src/labnotes/static/note-editor.js +169 -0
- labnotes-0.0.53/src/labnotes/templates/editor_imports.html +23 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/templates/experiment.html +5 -1
- {labnotes-0.0.51 → labnotes-0.0.53}/.gitignore +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/CHANGELOG.md +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/MIGRATIONS.md +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/README.md +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/pyproject.toml +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/__init__.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/cell_progress.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/check_counts.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/cli.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/context.md +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/context_guide.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/create_experiment.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/data_html.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/default_config_text.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/default_counts.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/experiment_progress.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/experiment_runs.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/experiments.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/format_duration.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/format_value.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/init.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/is_positive_int.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/list_runs.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/load_config.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/models.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/modes.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/notebook_kernel.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/output_html.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/print_rows.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/read_config.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/read_path.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/runner/__init__.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/runner/describe_event.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/runner/execute_run.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/runner/hold_folder.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/runner/new_run_id.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/runner/new_stamped_run_id.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/runner/param_args.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/runner/param_grid.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/runner/plan_runs.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/runner/read_experiment_config.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/runner/run_experiment.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/runner/run_folder.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/runner/run_headless.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/runner/runs_parent.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/runner/shell_command.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/runner/start_experiment.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/runner/stop_then_kill.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/runner/wait_for_group.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/runner/write_run_json.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/server.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/static/experiment.js +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/stop_idle_kernels.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/summarize_config.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/tables/__init__.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/tables/experiment_rows.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/tables/file_prefix.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/tables/iso_time.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/tables/list_experiment_runs.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/tables/open_tables.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/tables/path_glob.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/tables/path_pattern.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/tables/read_experiment.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/tables/read_file.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/tables/read_run.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/tables/summarize_experiments.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/templates/index.html +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/templates/macros.html +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/tui/__init__.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/tui/dim.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/tui/format_elapsed.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/tui/run_app.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/tui/run_tui.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/src/labnotes/tui/runs_table.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/testing-conventions.toml +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/tests/integration/conftest.py +0 -0
- {labnotes-0.0.51 → labnotes-0.0.53}/uv.lock +0 -0
|
@@ -6,7 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
from starlette.applications import Starlette
|
|
7
7
|
from starlette.exceptions import HTTPException
|
|
8
8
|
from starlette.requests import Request
|
|
9
|
-
from starlette.responses import HTMLResponse, Response
|
|
9
|
+
from starlette.responses import HTMLResponse, JSONResponse, Response
|
|
10
10
|
from starlette.routing import Mount, Route
|
|
11
11
|
from starlette.staticfiles import StaticFiles
|
|
12
12
|
from starlette.templating import Jinja2Templates
|
|
@@ -21,6 +21,7 @@ from labnotes.load_experiment import load_experiment
|
|
|
21
21
|
from labnotes.models import LabnotesConfig
|
|
22
22
|
from labnotes.notebook_kernel import NotebookKernel
|
|
23
23
|
from labnotes.python_blocks import python_blocks
|
|
24
|
+
from labnotes.save_note import save_note
|
|
24
25
|
from labnotes.saved_outputs import saved_outputs
|
|
25
26
|
from labnotes.stop_idle_kernels import stop_idle_kernels
|
|
26
27
|
from labnotes.tables.open_tables import open_tables
|
|
@@ -73,7 +74,7 @@ def create_app(config: LabnotesConfig, kernel_idle_minutes: float) -> Starlette:
|
|
|
73
74
|
request, "experiment.html", {"experiment": loaded}
|
|
74
75
|
)
|
|
75
76
|
|
|
76
|
-
async def
|
|
77
|
+
async def find_note(request: Request) -> dict[str, str]:
|
|
77
78
|
if request.headers.get("content-type") != "application/json":
|
|
78
79
|
raise HTTPException(415, "Send JSON")
|
|
79
80
|
name = request.path_params["name"]
|
|
@@ -84,10 +85,21 @@ def create_app(config: LabnotesConfig, kernel_idle_minutes: float) -> Starlette:
|
|
|
84
85
|
if not found:
|
|
85
86
|
raise HTTPException(404, "No such note")
|
|
86
87
|
[note] = found
|
|
87
|
-
return note
|
|
88
|
+
return note
|
|
89
|
+
|
|
90
|
+
async def put_note(request: Request) -> Response:
|
|
91
|
+
note = await find_note(request)
|
|
92
|
+
body = await request.json()
|
|
93
|
+
base, content = body.get("base"), body.get("content")
|
|
94
|
+
if not isinstance(base, str) or not isinstance(content, str):
|
|
95
|
+
raise HTTPException(400, "Send base and content as strings")
|
|
96
|
+
current = save_note(Path(note["path"]), base, content)
|
|
97
|
+
if current is not None:
|
|
98
|
+
return JSONResponse({"content": current}, status_code=409)
|
|
99
|
+
return Response(status_code=204)
|
|
88
100
|
|
|
89
101
|
async def run_block(request: Request) -> Response:
|
|
90
|
-
blocks = python_blocks(await
|
|
102
|
+
blocks = python_blocks((await find_note(request))["content"])
|
|
91
103
|
key = (request.path_params["name"], request.path_params["note"])
|
|
92
104
|
block = (await request.json()).get("block")
|
|
93
105
|
if not isinstance(block, int) or not 0 <= block < len(blocks):
|
|
@@ -97,7 +109,7 @@ def create_app(config: LabnotesConfig, kernel_idle_minutes: float) -> Starlette:
|
|
|
97
109
|
return HTMLResponse(await kernels[key].run(blocks[block]))
|
|
98
110
|
|
|
99
111
|
async def restart_kernel(request: Request) -> Response:
|
|
100
|
-
await
|
|
112
|
+
await find_note(request)
|
|
101
113
|
kernel = kernels.get((request.path_params["name"], request.path_params["note"]))
|
|
102
114
|
if kernel is not None:
|
|
103
115
|
await kernel.restart()
|
|
@@ -107,6 +119,7 @@ def create_app(config: LabnotesConfig, kernel_idle_minutes: float) -> Starlette:
|
|
|
107
119
|
routes=[
|
|
108
120
|
Route("/", index),
|
|
109
121
|
Route("/experiments/{name}", experiment),
|
|
122
|
+
Route("/experiments/{name}/notes/{note:path}", put_note, methods=["PUT"]),
|
|
110
123
|
Route(
|
|
111
124
|
"/experiments/{name}/notes/{note:path}/run",
|
|
112
125
|
run_block,
|
|
@@ -424,3 +424,27 @@ h3 {
|
|
|
424
424
|
.cell-output pre { background: none; border: 0; padding: 0.3rem 0; }
|
|
425
425
|
.cell-output .stderr, .cell-output .error { color: var(--bad); }
|
|
426
426
|
.cell-output img, .cell-output svg { max-width: 100%; height: auto; }
|
|
427
|
+
|
|
428
|
+
/* Editing a note in place */
|
|
429
|
+
.prose.editable { cursor: text; }
|
|
430
|
+
.edit-status { margin: 0; padding: 0.45rem 1.75rem; font-family: var(--sans); font-size: 0.8rem; color: var(--ink-3); border-bottom: 1px solid var(--rule); }
|
|
431
|
+
.editor { padding: 0.5rem 1.75rem 1.5rem; max-width: 46rem; }
|
|
432
|
+
.editor .cm-editor.cm-focused { outline: none; }
|
|
433
|
+
.editor .cm-content { font-family: var(--serif); font-size: 1.06rem; line-height: 1.68; caret-color: var(--ink); }
|
|
434
|
+
.editor .cm-line { padding: 0; }
|
|
435
|
+
.editor .md-h1 { font-size: 1.6rem; font-weight: 600; }
|
|
436
|
+
.editor .md-h2 { font-size: 1.3rem; font-weight: 600; }
|
|
437
|
+
.editor .md-h3 { font-size: 1.1rem; font-weight: 600; }
|
|
438
|
+
.editor .md-h4 { font-weight: 600; }
|
|
439
|
+
.editor .md-strong { font-weight: 700; }
|
|
440
|
+
.editor .md-em { font-style: italic; }
|
|
441
|
+
.editor .md-strike { text-decoration: line-through; }
|
|
442
|
+
.editor .md-link { color: var(--accent); }
|
|
443
|
+
.editor .md-code { font-family: var(--mono); font-size: 0.86em; }
|
|
444
|
+
.editor .md-quote { color: var(--ink-2); }
|
|
445
|
+
.editor .md-mark { color: var(--ink-3); }
|
|
446
|
+
.editor .md-code-line { font-family: var(--mono); font-size: 0.8rem; line-height: 1.5; background: var(--paper); padding: 0 1rem; }
|
|
447
|
+
.editor .py-keyword { color: var(--accent); }
|
|
448
|
+
.editor .py-string { color: var(--good); }
|
|
449
|
+
.editor .py-comment { color: var(--ink-3); font-style: italic; }
|
|
450
|
+
.editor .py-number { color: var(--bad); }
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const note = document.querySelector(".note[data-save-url]");
|
|
2
|
+
|
|
3
|
+
if (note) {
|
|
4
|
+
const prose = note.querySelector(".prose");
|
|
5
|
+
const status = note.querySelector(".edit-status");
|
|
6
|
+
const editor = import(note.dataset.editor);
|
|
7
|
+
editor.then(
|
|
8
|
+
() => prose.classList.add("editable"),
|
|
9
|
+
() => {
|
|
10
|
+
status.textContent = "Editing needs the network: the editor loads from esm.sh.";
|
|
11
|
+
status.hidden = false;
|
|
12
|
+
},
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const open = async (event) => {
|
|
16
|
+
if (event.target.closest("a, button, output") || getSelection().toString()) return;
|
|
17
|
+
const { edit } = await editor;
|
|
18
|
+
if (!prose.isConnected) return;
|
|
19
|
+
prose.removeEventListener("click", open);
|
|
20
|
+
edit(note, prose, status);
|
|
21
|
+
};
|
|
22
|
+
prose.addEventListener("click", open);
|
|
23
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
|
|
2
|
+
import { pythonLanguage } from "@codemirror/lang-python";
|
|
3
|
+
import {
|
|
4
|
+
HighlightStyle,
|
|
5
|
+
Language,
|
|
6
|
+
defineLanguageFacet,
|
|
7
|
+
languageDataProp,
|
|
8
|
+
syntaxHighlighting,
|
|
9
|
+
syntaxTree,
|
|
10
|
+
} from "@codemirror/language";
|
|
11
|
+
import { EditorState, RangeSetBuilder } from "@codemirror/state";
|
|
12
|
+
import { Decoration, EditorView, ViewPlugin, keymap } from "@codemirror/view";
|
|
13
|
+
import { tags } from "@lezer/highlight";
|
|
14
|
+
import { GFM, parseCode, parser } from "@lezer/markdown";
|
|
15
|
+
|
|
16
|
+
// Built from @lezer/markdown rather than @codemirror/lang-markdown, which
|
|
17
|
+
// imports HTML, CSS and JavaScript support that notes never use.
|
|
18
|
+
const data = defineLanguageFacet();
|
|
19
|
+
const markdown = new Language(
|
|
20
|
+
data,
|
|
21
|
+
parser.configure([
|
|
22
|
+
GFM,
|
|
23
|
+
parseCode({
|
|
24
|
+
codeParser: (info) => (info.split(/\s/)[0] === "python" ? pythonLanguage.parser : null),
|
|
25
|
+
}),
|
|
26
|
+
{ props: [languageDataProp.add({ Document: data })] },
|
|
27
|
+
]),
|
|
28
|
+
[],
|
|
29
|
+
"markdown",
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const style = HighlightStyle.define([
|
|
33
|
+
{ tag: tags.heading1, class: "md-h1" },
|
|
34
|
+
{ tag: tags.heading2, class: "md-h2" },
|
|
35
|
+
{ tag: tags.heading3, class: "md-h3" },
|
|
36
|
+
{ tag: [tags.heading4, tags.heading5, tags.heading6], class: "md-h4" },
|
|
37
|
+
{ tag: tags.strong, class: "md-strong" },
|
|
38
|
+
{ tag: tags.emphasis, class: "md-em" },
|
|
39
|
+
{ tag: tags.strikethrough, class: "md-strike" },
|
|
40
|
+
{ tag: [tags.link, tags.url], class: "md-link" },
|
|
41
|
+
{ tag: tags.monospace, class: "md-code" },
|
|
42
|
+
{ tag: tags.quote, class: "md-quote" },
|
|
43
|
+
{ tag: [tags.processingInstruction, tags.contentSeparator], class: "md-mark" },
|
|
44
|
+
{ tag: [tags.keyword, tags.operatorKeyword, tags.bool, tags.null], class: "py-keyword" },
|
|
45
|
+
{ tag: [tags.string, tags.special(tags.string)], class: "py-string" },
|
|
46
|
+
{ tag: tags.comment, class: "py-comment" },
|
|
47
|
+
{ tag: tags.number, class: "py-number" },
|
|
48
|
+
{ tag: [tags.function(tags.variableName), tags.definition(tags.variableName)], class: "py-name" },
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
// Markup is hidden except on the lines the cursor is on, as in Obsidian.
|
|
52
|
+
const HIDDEN = new Set(["HeaderMark", "EmphasisMark", "CodeMark", "StrikethroughMark"]);
|
|
53
|
+
const hide = Decoration.replace({});
|
|
54
|
+
const codeLine = Decoration.line({ class: "md-code-line" });
|
|
55
|
+
|
|
56
|
+
function decorate(view) {
|
|
57
|
+
const builder = new RangeSetBuilder();
|
|
58
|
+
const { state } = view;
|
|
59
|
+
const active = new Set();
|
|
60
|
+
for (const range of state.selection.ranges) {
|
|
61
|
+
for (let n = state.doc.lineAt(range.from).number; n <= state.doc.lineAt(range.to).number; n++) active.add(n);
|
|
62
|
+
}
|
|
63
|
+
for (const { from, to } of view.visibleRanges) {
|
|
64
|
+
syntaxTree(state).iterate({
|
|
65
|
+
from,
|
|
66
|
+
to,
|
|
67
|
+
enter(node) {
|
|
68
|
+
if (node.name === "FencedCode") {
|
|
69
|
+
for (let at = node.from; at <= node.to; ) {
|
|
70
|
+
const line = state.doc.lineAt(at);
|
|
71
|
+
builder.add(line.from, line.from, codeLine);
|
|
72
|
+
at = line.to + 1;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
if (!HIDDEN.has(node.name) || active.has(state.doc.lineAt(node.from).number)) return;
|
|
77
|
+
let end = node.to;
|
|
78
|
+
if (node.name === "HeaderMark" && state.doc.sliceString(end, end + 1) === " ") end += 1;
|
|
79
|
+
builder.add(node.from, end, hide);
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return builder.finish();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const livePreview = ViewPlugin.fromClass(
|
|
87
|
+
class {
|
|
88
|
+
constructor(view) {
|
|
89
|
+
this.decorations = decorate(view);
|
|
90
|
+
}
|
|
91
|
+
update(update) {
|
|
92
|
+
if (update.docChanged || update.selectionSet || update.viewportChanged) {
|
|
93
|
+
this.decorations = decorate(update.view);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
{ decorations: (plugin) => plugin.decorations },
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
function show(status, message) {
|
|
101
|
+
status.textContent = message;
|
|
102
|
+
status.hidden = false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function edit(note, prose, status) {
|
|
106
|
+
let base = JSON.parse(note.querySelector(".note-source").textContent);
|
|
107
|
+
let stale = false;
|
|
108
|
+
let timer;
|
|
109
|
+
let queue = Promise.resolve();
|
|
110
|
+
|
|
111
|
+
const host = document.createElement("div");
|
|
112
|
+
host.className = "editor";
|
|
113
|
+
prose.replaceWith(host);
|
|
114
|
+
|
|
115
|
+
const save = () =>
|
|
116
|
+
(queue = queue
|
|
117
|
+
.then(async () => {
|
|
118
|
+
clearTimeout(timer);
|
|
119
|
+
const content = view.state.doc.toString();
|
|
120
|
+
if (stale || content === base) return;
|
|
121
|
+
const response = await fetch(note.dataset.saveUrl, {
|
|
122
|
+
method: "PUT",
|
|
123
|
+
headers: { "Content-Type": "application/json" },
|
|
124
|
+
body: JSON.stringify({ base, content }),
|
|
125
|
+
});
|
|
126
|
+
if (response.status === 409) {
|
|
127
|
+
stale = true;
|
|
128
|
+
show(status, "This note changed on disk, so your edits are not saved. Copy them, then reload.");
|
|
129
|
+
} else if (response.ok) {
|
|
130
|
+
base = content;
|
|
131
|
+
} else {
|
|
132
|
+
show(status, `Not saved: ${response.status} ${response.statusText}`);
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
.catch((error) => show(status, `Not saved: ${error.message}`)));
|
|
136
|
+
|
|
137
|
+
const close = async () => {
|
|
138
|
+
await save();
|
|
139
|
+
if (stale) return;
|
|
140
|
+
document.removeEventListener("mousedown", outside);
|
|
141
|
+
location.reload();
|
|
142
|
+
};
|
|
143
|
+
const outside = (event) => {
|
|
144
|
+
if (!note.contains(event.target)) close();
|
|
145
|
+
};
|
|
146
|
+
document.addEventListener("mousedown", outside);
|
|
147
|
+
|
|
148
|
+
const view = new EditorView({
|
|
149
|
+
parent: host,
|
|
150
|
+
state: EditorState.create({
|
|
151
|
+
doc: base,
|
|
152
|
+
extensions: [
|
|
153
|
+
history(),
|
|
154
|
+
keymap.of([{ key: "Escape", run: () => (close(), true) }, ...defaultKeymap, ...historyKeymap]),
|
|
155
|
+
markdown,
|
|
156
|
+
syntaxHighlighting(style),
|
|
157
|
+
livePreview,
|
|
158
|
+
EditorView.lineWrapping,
|
|
159
|
+
EditorView.contentAttributes.of({ "aria-label": "Note" }),
|
|
160
|
+
EditorView.updateListener.of((update) => {
|
|
161
|
+
if (!update.docChanged) return;
|
|
162
|
+
clearTimeout(timer);
|
|
163
|
+
timer = setTimeout(save, 500);
|
|
164
|
+
}),
|
|
165
|
+
],
|
|
166
|
+
}),
|
|
167
|
+
});
|
|
168
|
+
view.focus();
|
|
169
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{# Every package is fetched with its imports left bare (the * prefix) and pinned here, so each loads once. Two copies of @codemirror/state break CodeMirror. #}
|
|
2
|
+
<script type="importmap">
|
|
3
|
+
{
|
|
4
|
+
"imports": {
|
|
5
|
+
"@codemirror/autocomplete": "https://esm.sh/*@codemirror/autocomplete@6.20.4",
|
|
6
|
+
"@codemirror/commands": "https://esm.sh/*@codemirror/commands@6.11.1",
|
|
7
|
+
"@codemirror/lang-python": "https://esm.sh/*@codemirror/lang-python@6.2.1",
|
|
8
|
+
"@codemirror/language": "https://esm.sh/*@codemirror/language@6.13.1",
|
|
9
|
+
"@codemirror/state": "https://esm.sh/*@codemirror/state@6.7.6",
|
|
10
|
+
"@codemirror/streamparser": "https://esm.sh/*@codemirror/streamparser@6.0.0",
|
|
11
|
+
"@codemirror/view": "https://esm.sh/*@codemirror/view@6.43.15",
|
|
12
|
+
"@lezer/common": "https://esm.sh/*@lezer/common@1.5.3",
|
|
13
|
+
"@lezer/highlight": "https://esm.sh/*@lezer/highlight@1.2.5",
|
|
14
|
+
"@lezer/lr": "https://esm.sh/*@lezer/lr@1.4.11",
|
|
15
|
+
"@lezer/markdown": "https://esm.sh/*@lezer/markdown@1.8.1",
|
|
16
|
+
"@lezer/python": "https://esm.sh/*@lezer/python@1.1.19",
|
|
17
|
+
"@marijn/find-cluster-break": "https://esm.sh/*@marijn/find-cluster-break@1.0.4",
|
|
18
|
+
"crelt": "https://esm.sh/*crelt@1.0.7",
|
|
19
|
+
"style-mod": "https://esm.sh/*style-mod@4.1.4",
|
|
20
|
+
"w3c-keyname": "https://esm.sh/*w3c-keyname@2.2.8"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
</script>
|
|
@@ -37,11 +37,15 @@
|
|
|
37
37
|
<nav class="tabs sub">
|
|
38
38
|
{% for each in e.notes %}<a href="?tab=notebooks&note={{ each.name | urlencode }}"{% if each.name == note.name %} aria-current="page"{% endif %}>{{ each.name }}</a>{% endfor %}
|
|
39
39
|
</nav>
|
|
40
|
-
<article class="note" data-run-url="{{ url_for('run_block', name=e.name, note=note.name) }}" data-restart-url="{{ url_for('restart_kernel', name=e.name, note=note.name) }}">
|
|
40
|
+
<article class="note" data-run-url="{{ url_for('run_block', name=e.name, note=note.name) }}" data-restart-url="{{ url_for('restart_kernel', name=e.name, note=note.name) }}" data-save-url="{{ url_for('put_note', name=e.name, note=note.name) }}" data-editor="{{ asset('note-editor.js') }}">
|
|
41
41
|
{% if note.runnable %}<header class="note-name"><button type="button" class="restart" title="Stop this note's Python kernel. The next Run starts a fresh one.">Restart</button></header>{% endif %}
|
|
42
|
+
<p class="edit-status" role="status" hidden></p>
|
|
42
43
|
<div class="prose">{{ note.html | safe }}</div>
|
|
44
|
+
<script type="application/json" class="note-source">{{ note.content | tojson }}</script>
|
|
43
45
|
</article>
|
|
46
|
+
{% include "editor_imports.html" %}
|
|
44
47
|
<script src="{{ asset('run-cell.js') }}" defer></script>
|
|
48
|
+
<script type="module" src="{{ asset('note-edit.js') }}"></script>
|
|
45
49
|
{% else %}
|
|
46
50
|
<p class="empty">No notebooks yet.</p>
|
|
47
51
|
{% endif %}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|