labnotes 0.0.52__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.
Files changed (102) hide show
  1. {labnotes-0.0.52 → labnotes-0.0.53}/PKG-INFO +1 -1
  2. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/load_experiment.py +1 -0
  3. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/static/labnotes.css +24 -0
  4. labnotes-0.0.53/src/labnotes/static/note-edit.js +23 -0
  5. labnotes-0.0.53/src/labnotes/static/note-editor.js +169 -0
  6. labnotes-0.0.53/src/labnotes/templates/editor_imports.html +23 -0
  7. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/templates/experiment.html +5 -1
  8. {labnotes-0.0.52 → labnotes-0.0.53}/.gitignore +0 -0
  9. {labnotes-0.0.52 → labnotes-0.0.53}/CHANGELOG.md +0 -0
  10. {labnotes-0.0.52 → labnotes-0.0.53}/MIGRATIONS.md +0 -0
  11. {labnotes-0.0.52 → labnotes-0.0.53}/README.md +0 -0
  12. {labnotes-0.0.52 → labnotes-0.0.53}/pyproject.toml +0 -0
  13. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/__init__.py +0 -0
  14. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/asset_url.py +0 -0
  15. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/cell_progress.py +0 -0
  16. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/check_counts.py +0 -0
  17. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/cli.py +0 -0
  18. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/context.md +0 -0
  19. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/context_guide.py +0 -0
  20. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/count_runs.py +0 -0
  21. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/create_app.py +0 -0
  22. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/create_experiment.py +0 -0
  23. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/data_html.py +0 -0
  24. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/default_config_text.py +0 -0
  25. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/default_counts.py +0 -0
  26. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/drain_watch.py +0 -0
  27. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/experiment_notes.py +0 -0
  28. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/experiment_progress.py +0 -0
  29. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/experiment_runs.py +0 -0
  30. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/experiments.py +0 -0
  31. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/format_duration.py +0 -0
  32. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/format_value.py +0 -0
  33. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/init.py +0 -0
  34. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/is_positive_int.py +0 -0
  35. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/list_experiments.py +0 -0
  36. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/list_runs.py +0 -0
  37. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/load_config.py +0 -0
  38. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/models.py +0 -0
  39. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/modes.py +0 -0
  40. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/notebook_kernel.py +0 -0
  41. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/output_html.py +0 -0
  42. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/parse_run_view.py +0 -0
  43. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/print_rows.py +0 -0
  44. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/python_blocks.py +0 -0
  45. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/read_config.py +0 -0
  46. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/read_path.py +0 -0
  47. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/render_markdown.py +0 -0
  48. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/runner/__init__.py +0 -0
  49. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/runner/describe_event.py +0 -0
  50. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/runner/execute_run.py +0 -0
  51. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/runner/hold_folder.py +0 -0
  52. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/runner/new_run_id.py +0 -0
  53. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/runner/new_stamped_run_id.py +0 -0
  54. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/runner/param_args.py +0 -0
  55. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/runner/param_grid.py +0 -0
  56. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/runner/plan_runs.py +0 -0
  57. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/runner/read_experiment_config.py +0 -0
  58. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/runner/run_experiment.py +0 -0
  59. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/runner/run_folder.py +0 -0
  60. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/runner/run_headless.py +0 -0
  61. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/runner/runs_parent.py +0 -0
  62. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/runner/shell_command.py +0 -0
  63. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/runner/start_experiment.py +0 -0
  64. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/runner/stop_then_kill.py +0 -0
  65. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/runner/wait_for_group.py +0 -0
  66. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/runner/write_run_json.py +0 -0
  67. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/save_note.py +0 -0
  68. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/saved_outputs.py +0 -0
  69. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/serve_app.py +0 -0
  70. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/server.py +0 -0
  71. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/sort_href.py +0 -0
  72. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/sql_quote.py +0 -0
  73. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/static/experiment.js +0 -0
  74. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/static/row-link.js +0 -0
  75. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/static/run-cell.js +0 -0
  76. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/static/view-toggle.js +0 -0
  77. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/stop_idle_kernels.py +0 -0
  78. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/summarize_config.py +0 -0
  79. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/tables/__init__.py +0 -0
  80. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/tables/experiment_rows.py +0 -0
  81. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/tables/file_prefix.py +0 -0
  82. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/tables/iso_time.py +0 -0
  83. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/tables/list_experiment_runs.py +0 -0
  84. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/tables/open_tables.py +0 -0
  85. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/tables/path_glob.py +0 -0
  86. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/tables/path_pattern.py +0 -0
  87. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/tables/read_experiment.py +0 -0
  88. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/tables/read_file.py +0 -0
  89. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/tables/read_run.py +0 -0
  90. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/tables/summarize_experiments.py +0 -0
  91. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/templates/base.html +0 -0
  92. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/templates/index.html +0 -0
  93. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/templates/macros.html +0 -0
  94. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/tui/__init__.py +0 -0
  95. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/tui/dim.py +0 -0
  96. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/tui/format_elapsed.py +0 -0
  97. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/tui/run_app.py +0 -0
  98. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/tui/run_tui.py +0 -0
  99. {labnotes-0.0.52 → labnotes-0.0.53}/src/labnotes/tui/runs_table.py +0 -0
  100. {labnotes-0.0.52 → labnotes-0.0.53}/testing-conventions.toml +0 -0
  101. {labnotes-0.0.52 → labnotes-0.0.53}/tests/integration/conftest.py +0 -0
  102. {labnotes-0.0.52 → labnotes-0.0.53}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labnotes
3
- Version: 0.0.52
3
+ Version: 0.0.53
4
4
  Summary: Browse experiment designs, notes, and run data from a folder.
5
5
  Project-URL: Repository, https://github.com/thekevinscott/labnotes
6
6
  Author-email: Kevin Scott <me@thekevinscott.com>
@@ -56,6 +56,7 @@ async def load_experiment(
56
56
  "notes": [
57
57
  {
58
58
  "name": note["name"],
59
+ "content": note["content"],
59
60
  "html": render_markdown(
60
61
  note["content"], outputs_for(note["name"], note["content"])
61
62
  ),
@@ -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&amp;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