bots-garden/sidekickpublic⑂ Fork 0
⑂ 51fba89
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/sidekick.git
git clone ssh://git@rickub.com/bots-garden/sidekick.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

🛟 Updated.

k33g committed 2026-09-27T05:03:36+02:00 Browse files
51fba89 parent: f7a5060
The diff is large and was truncated by the engine.
modified README.md +5 -2
@@ -68,6 +68,7 @@ export SIDEKICK_TOKEN="$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=')"
6868
6969 - clicking a file opens it in a [Monaco](https://microsoft.github.io/monaco-editor/) tab; **Ctrl/Cmd+S** or **Save** writes it back;
7070 - Markdown (`.md`), AsciiDoc (`.adoc`, `.asciidoc`, `.asc`) and HTML (`.html`, `.htm`) files have a **Preview** button next to **Save** (**Ctrl/Cmd+Shift+V**): the tab shows the rendered file instead of its source, and **Source** goes back. The preview renders what the editor holds, unsaved changes included, and follows it as it changes (the agent's edits too). Images next to the document are shown, links to other files of the project open them in a tab, anchors scroll. Each tab remembers its mode;
71+- images open as pictures: PNG, JPEG, GIF, WebP, AVIF, BMP and ICO in a tab that shows the file (with its size in pixels), SVG rendered first with **Source** to edit it — the picture follows the edits. [draw.io](https://www.drawio.com) diagrams (`.drawio`, `.dio`) open rendered by the official viewer (pages, layers, zoom), **Source** shows their XML and the diagram follows the edits; `.drawio.svg` and `.drawio.png` exports are shown as the pictures they are;
7172 - **+📄** / **+📁** in the tree's header, or a right click, create a file or a folder (in the selected folder); a right click also offers **Rename** and **Delete**. With the tree focused, **F2** renames and **Delete** deletes the selected entry. Open tabs follow a rename;
7273 - after each agent tool call (and each command run in the terminal), the tree and the tabs without local changes are reloaded from disk. If a file you are editing was changed on disk meanwhile, saving asks before overwriting it.
7374
@@ -83,7 +84,7 @@ Only the working directory is reachable from the tree and the editor: `../` and
8384
8485 The tree uses the icons of VS Code's [Material Icon Theme](https://github.com/material-extensions/vscode-material-icon-theme) (MIT), with the same rules: by file name, else by extension, folders by name, with their light-theme variants.
8586
86-All the libraries the web UI uses (Tailwind, marked, highlight.js, Monaco, xterm.js, the Material icons, Asciidoctor.js) are embedded in the binary, in `web/vendor/`: the page loads nothing from the network, and works offline.
87+All the libraries the web UI uses (Tailwind, marked, highlight.js, Monaco, xterm.js, the Material icons, Asciidoctor.js, the draw.io viewer) are embedded in the binary, in `web/vendor/`: the page loads nothing from the network, and works offline.
8788
8889 `./vendor.sh` writes `web/vendor/` from the versions pinned at its top. To update a library: `./vendor.sh --outdated` lists the pinned and published versions; change the version in the script (its comments say which updates need more than that), run `./vendor.sh`, check the UI, commit `web/vendor/`.
8990
@@ -91,7 +92,9 @@ All the libraries the web UI uses (Tailwind, marked, highlight.js, Monaco, xterm
9192
9293 What the agent writes is untrusted (it may repeat HTML read in a file or a web page): the chat filters it with DOMPurify — no scripts, event handlers, `javascript:` links, styles, frames or forms; links open in a new tab. Behind that, a Content-Security-Policy only lets the page run the scripts sidekick serves and load nothing from elsewhere (so an image URL in a message cannot carry data out), and the page cannot be shown in another site's frame.
9394
94-Previewed files are untrusted too. Markdown and AsciiDoc go through the same filter (their ids are prefixed with `user-content-`, so a heading cannot take the id of one of the page's elements); AsciiDoc is converted in `secure` mode (`include::` reads nothing). An HTML file is shown in a frame sandboxed with every restriction — its scripts do not run, it has no access to sidekick — and under the page's CSP, so it loads nothing from the network: its external styles, scripts and images are missing from the preview. The images of a preview come from `/api/raw`, which only serves image files (by extension) from the working directory, with a sandboxing CSP of their own (an SVG opened directly cannot run a script).
95+Previewed files are untrusted too. Markdown and AsciiDoc go through the same filter (their ids are prefixed with `user-content-`, so a heading cannot take the id of one of the page's elements); AsciiDoc is converted in `secure` mode (`include::` reads nothing). An HTML file is shown in a frame sandboxed with every restriction — its scripts do not run, it has no access to sidekick — and under the page's CSP, so it loads nothing from the network: its external styles, scripts and images are missing from the preview. The images of a preview come from `/api/raw`, which only serves image files (by extension) from the working directory, with a sandboxing CSP of their own (an SVG opened directly cannot run a script). An SVG in the editor is shown as an `<img>`, where its scripts never run.
96+
97+The draw.io viewer runs in the page: it sanitises the HTML of labels itself, and the CSP stops what could get through — no inline script, event handler or `eval` runs, nothing loads from another site. Its resources, which would come from viewer.diagrams.net, point into the binary instead: MathJax is not embedded (a formula shows as its source), and the few shape libraries the viewer fetches on demand are missing.
9598
9699 ### Examples
97100
@@ -68,6 +68,7 @@ export SIDEKICK_TOKEN="$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=')"
68 68
69 - clicking a file opens it in a [Monaco](https://microsoft.github.io/monaco-editor/) tab; **Ctrl/Cmd+S** or **Save** writes it back;69 - clicking a file opens it in a [Monaco](https://microsoft.github.io/monaco-editor/) tab; **Ctrl/Cmd+S** or **Save** writes it back;
70 - Markdown (`.md`), AsciiDoc (`.adoc`, `.asciidoc`, `.asc`) and HTML (`.html`, `.htm`) files have a **Preview** button next to **Save** (**Ctrl/Cmd+Shift+V**): the tab shows the rendered file instead of its source, and **Source** goes back. The preview renders what the editor holds, unsaved changes included, and follows it as it changes (the agent's edits too). Images next to the document are shown, links to other files of the project open them in a tab, anchors scroll. Each tab remembers its mode;70 - Markdown (`.md`), AsciiDoc (`.adoc`, `.asciidoc`, `.asc`) and HTML (`.html`, `.htm`) files have a **Preview** button next to **Save** (**Ctrl/Cmd+Shift+V**): the tab shows the rendered file instead of its source, and **Source** goes back. The preview renders what the editor holds, unsaved changes included, and follows it as it changes (the agent's edits too). Images next to the document are shown, links to other files of the project open them in a tab, anchors scroll. Each tab remembers its mode;
71+- images open as pictures: PNG, JPEG, GIF, WebP, AVIF, BMP and ICO in a tab that shows the file (with its size in pixels), SVG rendered first with **Source** to edit it — the picture follows the edits. [draw.io](https://www.drawio.com) diagrams (`.drawio`, `.dio`) open rendered by the official viewer (pages, layers, zoom), **Source** shows their XML and the diagram follows the edits; `.drawio.svg` and `.drawio.png` exports are shown as the pictures they are;
71 - **+📄** / **+📁** in the tree's header, or a right click, create a file or a folder (in the selected folder); a right click also offers **Rename** and **Delete**. With the tree focused, **F2** renames and **Delete** deletes the selected entry. Open tabs follow a rename;72 - **+📄** / **+📁** in the tree's header, or a right click, create a file or a folder (in the selected folder); a right click also offers **Rename** and **Delete**. With the tree focused, **F2** renames and **Delete** deletes the selected entry. Open tabs follow a rename;
72 - after each agent tool call (and each command run in the terminal), the tree and the tabs without local changes are reloaded from disk. If a file you are editing was changed on disk meanwhile, saving asks before overwriting it.73 - after each agent tool call (and each command run in the terminal), the tree and the tabs without local changes are reloaded from disk. If a file you are editing was changed on disk meanwhile, saving asks before overwriting it.
73 74
@@ -83,7 +84,7 @@ Only the working directory is reachable from the tree and the editor: `../` and
83 84
84 The tree uses the icons of VS Code's [Material Icon Theme](https://github.com/material-extensions/vscode-material-icon-theme) (MIT), with the same rules: by file name, else by extension, folders by name, with their light-theme variants.85 The tree uses the icons of VS Code's [Material Icon Theme](https://github.com/material-extensions/vscode-material-icon-theme) (MIT), with the same rules: by file name, else by extension, folders by name, with their light-theme variants.
85 86
86-All the libraries the web UI uses (Tailwind, marked, highlight.js, Monaco, xterm.js, the Material icons, Asciidoctor.js) are embedded in the binary, in `web/vendor/`: the page loads nothing from the network, and works offline.87+All the libraries the web UI uses (Tailwind, marked, highlight.js, Monaco, xterm.js, the Material icons, Asciidoctor.js, the draw.io viewer) are embedded in the binary, in `web/vendor/`: the page loads nothing from the network, and works offline.
87 88
88 `./vendor.sh` writes `web/vendor/` from the versions pinned at its top. To update a library: `./vendor.sh --outdated` lists the pinned and published versions; change the version in the script (its comments say which updates need more than that), run `./vendor.sh`, check the UI, commit `web/vendor/`.89 `./vendor.sh` writes `web/vendor/` from the versions pinned at its top. To update a library: `./vendor.sh --outdated` lists the pinned and published versions; change the version in the script (its comments say which updates need more than that), run `./vendor.sh`, check the UI, commit `web/vendor/`.
89 90
@@ -91,7 +92,9 @@ All the libraries the web UI uses (Tailwind, marked, highlight.js, Monaco, xterm
91 92
92 What the agent writes is untrusted (it may repeat HTML read in a file or a web page): the chat filters it with DOMPurify — no scripts, event handlers, `javascript:` links, styles, frames or forms; links open in a new tab. Behind that, a Content-Security-Policy only lets the page run the scripts sidekick serves and load nothing from elsewhere (so an image URL in a message cannot carry data out), and the page cannot be shown in another site's frame.93 What the agent writes is untrusted (it may repeat HTML read in a file or a web page): the chat filters it with DOMPurify — no scripts, event handlers, `javascript:` links, styles, frames or forms; links open in a new tab. Behind that, a Content-Security-Policy only lets the page run the scripts sidekick serves and load nothing from elsewhere (so an image URL in a message cannot carry data out), and the page cannot be shown in another site's frame.
93 94
94-Previewed files are untrusted too. Markdown and AsciiDoc go through the same filter (their ids are prefixed with `user-content-`, so a heading cannot take the id of one of the page's elements); AsciiDoc is converted in `secure` mode (`include::` reads nothing). An HTML file is shown in a frame sandboxed with every restriction — its scripts do not run, it has no access to sidekick — and under the page's CSP, so it loads nothing from the network: its external styles, scripts and images are missing from the preview. The images of a preview come from `/api/raw`, which only serves image files (by extension) from the working directory, with a sandboxing CSP of their own (an SVG opened directly cannot run a script).95+Previewed files are untrusted too. Markdown and AsciiDoc go through the same filter (their ids are prefixed with `user-content-`, so a heading cannot take the id of one of the page's elements); AsciiDoc is converted in `secure` mode (`include::` reads nothing). An HTML file is shown in a frame sandboxed with every restriction — its scripts do not run, it has no access to sidekick — and under the page's CSP, so it loads nothing from the network: its external styles, scripts and images are missing from the preview. The images of a preview come from `/api/raw`, which only serves image files (by extension) from the working directory, with a sandboxing CSP of their own (an SVG opened directly cannot run a script). An SVG in the editor is shown as an `<img>`, where its scripts never run.
96+
97+The draw.io viewer runs in the page: it sanitises the HTML of labels itself, and the CSP stops what could get through — no inline script, event handler or `eval` runs, nothing loads from another site. Its resources, which would come from viewer.diagrams.net, point into the binary instead: MathJax is not embedded (a formula shows as its source), and the few shape libraries the viewer fetches on demand are missing.
95 98
96 ### Examples99 ### Examples
97 100
added demo/.mm/sessions/20260927-023311-88b7348e.json +16 -0
new file mode 100644
@@ -0,0 +1,16 @@
1+{
2+ "id": "20260927-023311-88b7348e",
3+ "cwd": "/Users/k33g/kDrive/Rickub/bots-garden/sidekick/demo",
4+ "createdAt": "2026-09-27T02:33:11.265396Z",
5+ "updatedAt": "2026-09-27T02:33:11.265483Z",
6+ "messages": [
7+ {
8+ "content": [
9+ {
10+ "text": "Your name is Bob.\nYou are a coding agent working in a terminal.\nYou have a \"bash\" tool to run shell commands.\nUse it to explore files, run tests, inspect the repository, etc.\nChain several commands if needed, then answer clearly in English.\n\nA request often mixes things you answer from yourself (\"say hello\") with\nthings only a command can answer (\"list the files\"). Handle every part, in\nthe order asked, and run a command for each part that needs one.\nNever state the contents of a file, the output of a command, or the state of\nthe repository unless a command in THIS answer returned it. What you did not\nread, you do not know: run the command instead of recalling it.\n\nSKILLS\nYou have a second tool, `read_skill`. Its description lists the procedures\navailable for this project — one per kind of task.\n\nAny request to DO something to a Go project is a skill, not a shell command\nyou invent. Match the request against that list, call `read_skill` FIRST,\nbefore any bash command, and then follow what it says step by step.\n\nFILE EDITING\nYou have three tools for files: `read_file`, `edit_file` and `write_file`.\nThey are how a file gets read and changed here: each change is exact,\nchecked before it is written, and comes back as a diff with line numbers.\nbash is for running things — building, testing, listing, searching.\n\n- Read before you write: call `read_file` on the file (numbered=true when\n you need line numbers). You cannot target text you have not seen; never\n rely on what you think you remember about a file.\n- To change an existing file, call `edit_file` with one or more {old, new}\n pairs. `old` is copied from the file character for character — same\n spaces, same indentation, same line breaks — and appears exactly once:\n add the surrounding lines until it is unique. Several pairs are applied\n together, against the original file. An empty `new` deletes the text.\n- Call `write_file` only to create a file, or to rewrite one entirely and\n on purpose. On an existing file it replaces everything, including what\n you did not intend to touch.\n- Read the diff the tool returns: it says exactly what changed and on which\n line. If `edit_file` refuses — text not found, ambiguous, overlapping\n edits — read the file again and fix `old`. Do not fall back to\n `write_file` to force the change through.\n- After editing code, run the narrowest check with bash: the formatter, the\n compiler, or the test covering that file.\n\nRULES\n- Keep everything the file already does, unless the user asked to remove it.\n- Touch only the files the request is about. Do not add tests, files or\n features that were not asked for.\n- Never run a git command unless the user says git, commit or push.\n- Never move, rename or delete a file unless the user asked for it.\n- Then answer in English, in a few lines.\n- If you don't know how to use a \u003ccli\u003e, run `\u003ccli\u003e --help` (or `\u003ccli\u003e help`)\n to understand the options, then run the command.\n\nBACKGROUND JOBS\nNever let a command block the answer. Anything that serves, watches or runs\nlong goes to the background, with BOTH streams redirected and its pid kept:\n\n nohup \u003ccommand\u003e \u003e /tmp/\u003cjob\u003e.log 2\u003e\u00261 \u0026 echo $! \u003e /tmp/\u003cjob\u003e.pid\n\nRedirecting only stdout still blocks until the process exits. Read the\n`bg-jobs` skill before you wait on, inspect or stop such a job — each has a\nrule you cannot guess. Stop every job you started before you finish, and say\nwhich ones you left running.\n"
11+ }
12+ ],
13+ "role": "system"
14+ }
15+ ]
16+}
\ No newline at end of file
new file mode 100644
@@ -0,0 +1,16 @@
1+{
2+ "id": "20260927-023311-88b7348e",
3+ "cwd": "/Users/k33g/kDrive/Rickub/bots-garden/sidekick/demo",
4+ "createdAt": "2026-09-27T02:33:11.265396Z",
5+ "updatedAt": "2026-09-27T02:33:11.265483Z",
6+ "messages": [
7+ {
8+ "content": [
9+ {
10+ "text": "Your name is Bob.\nYou are a coding agent working in a terminal.\nYou have a \"bash\" tool to run shell commands.\nUse it to explore files, run tests, inspect the repository, etc.\nChain several commands if needed, then answer clearly in English.\n\nA request often mixes things you answer from yourself (\"say hello\") with\nthings only a command can answer (\"list the files\"). Handle every part, in\nthe order asked, and run a command for each part that needs one.\nNever state the contents of a file, the output of a command, or the state of\nthe repository unless a command in THIS answer returned it. What you did not\nread, you do not know: run the command instead of recalling it.\n\nSKILLS\nYou have a second tool, `read_skill`. Its description lists the procedures\navailable for this project — one per kind of task.\n\nAny request to DO something to a Go project is a skill, not a shell command\nyou invent. Match the request against that list, call `read_skill` FIRST,\nbefore any bash command, and then follow what it says step by step.\n\nFILE EDITING\nYou have three tools for files: `read_file`, `edit_file` and `write_file`.\nThey are how a file gets read and changed here: each change is exact,\nchecked before it is written, and comes back as a diff with line numbers.\nbash is for running things — building, testing, listing, searching.\n\n- Read before you write: call `read_file` on the file (numbered=true when\n you need line numbers). You cannot target text you have not seen; never\n rely on what you think you remember about a file.\n- To change an existing file, call `edit_file` with one or more {old, new}\n pairs. `old` is copied from the file character for character — same\n spaces, same indentation, same line breaks — and appears exactly once:\n add the surrounding lines until it is unique. Several pairs are applied\n together, against the original file. An empty `new` deletes the text.\n- Call `write_file` only to create a file, or to rewrite one entirely and\n on purpose. On an existing file it replaces everything, including what\n you did not intend to touch.\n- Read the diff the tool returns: it says exactly what changed and on which\n line. If `edit_file` refuses — text not found, ambiguous, overlapping\n edits — read the file again and fix `old`. Do not fall back to\n `write_file` to force the change through.\n- After editing code, run the narrowest check with bash: the formatter, the\n compiler, or the test covering that file.\n\nRULES\n- Keep everything the file already does, unless the user asked to remove it.\n- Touch only the files the request is about. Do not add tests, files or\n features that were not asked for.\n- Never run a git command unless the user says git, commit or push.\n- Never move, rename or delete a file unless the user asked for it.\n- Then answer in English, in a few lines.\n- If you don't know how to use a \u003ccli\u003e, run `\u003ccli\u003e --help` (or `\u003ccli\u003e help`)\n to understand the options, then run the command.\n\nBACKGROUND JOBS\nNever let a command block the answer. Anything that serves, watches or runs\nlong goes to the background, with BOTH streams redirected and its pid kept:\n\n nohup \u003ccommand\u003e \u003e /tmp/\u003cjob\u003e.log 2\u003e\u00261 \u0026 echo $! \u003e /tmp/\u003cjob\u003e.pid\n\nRedirecting only stdout still blocks until the process exits. Read the\n`bg-jobs` skill before you wait on, inspect or stop such a job — each has a\nrule you cannot guess. Stop every job you started before you finish, and say\nwhich ones you left running.\n"
11+ }
12+ ],
13+ "role": "system"
14+ }
15+ ]
16+}
\ No newline at end of file\ No newline at end of file
added demo/20260713-header.png +0 -0
new file mode 100644
Binary files /dev/null and b/demo/20260713-header.png differ
new file mode 100644
Binary files /dev/null and b/demo/20260713-header.png differBinary files /dev/null and b/demo/20260713-header.png differ
modified demo/README.md +12 -0
@@ -1,5 +1,8 @@
11 # README
22
3+this is an image
4+![header](20260713-header.png)
5+
36 Dossier de travail.
47
58 ## Fichiers
@@ -13,3 +16,12 @@ Dossier de travail.
1316 ```bash
1417 ./start.sh
1518 ```
19+
20+```mermaid
21+flowchart LR
22+
23+A[Hard] -->|Text| B(Round)
24+B --> C{Decision}
25+C -->|One| D[Result 1]
26+C -->|Two| E[Result 2]
27+```
\ No newline at end of file
@@ -1,5 +1,8 @@
1 # README1 # README
2 2
3+this is an image
4+![header](20260713-header.png)
5+
3 Dossier de travail.6 Dossier de travail.
4 7
5 ## Fichiers8 ## Fichiers
@@ -13,3 +16,12 @@ Dossier de travail.
13 ```bash16 ```bash
14 ./start.sh17 ./start.sh
15 ```18 ```
19+
20+```mermaid
21+flowchart LR
22+
23+A[Hard] -->|Text| B(Round)
24+B --> C{Decision}
25+C -->|One| D[Result 1]
26+C -->|Two| E[Result 2]
27+```
\ No newline at end of file\ No newline at end of file
added demo/diagram-1-hooks-overview.drawio +99 -0
new file mode 100644
@@ -0,0 +1,99 @@
1+<mxfile host="65bd71144e">
2+ <diagram name="Docker Agent hooks overview" id="hooks-overview">
3+ <mxGraphModel dx="889" dy="464" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1150" math="0" shadow="0">
4+ <root>
5+ <mxCell id="0"/>
6+ <mxCell id="1" parent="0"/>
7+ <mxCell id="title" value="Docker Agent — where each hook intercepts the agent loop" style="text;html=1;align=center;fontSize=15;fontStyle=1;verticalAlign=middle;" parent="1" vertex="1">
8+ <mxGeometry x="80" y="20" width="640" height="30" as="geometry"/>
9+ </mxCell>
10+ <mxCell id="user" value="User&#xa;(prompt)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" parent="1" vertex="1">
11+ <mxGeometry x="260" y="70" width="140" height="40" as="geometry"/>
12+ </mxCell>
13+ <mxCell id="ups" value="user_prompt_submit" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1" parent="1" vertex="1">
14+ <mxGeometry x="240" y="150" width="180" height="40" as="geometry"/>
15+ </mxCell>
16+ <mxCell id="h1" value="HOOK — moderate / rewrite the INPUT" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1">
17+ <mxGeometry x="500" y="150" width="260" height="40" as="geometry"/>
18+ </mxCell>
19+ <mxCell id="blc" value="before_llm_call" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1" parent="1" vertex="1">
20+ <mxGeometry x="240" y="230" width="180" height="40" as="geometry"/>
21+ </mxCell>
22+ <mxCell id="h2" value="HOOK — scrub the OUTGOING messages" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1">
23+ <mxGeometry x="500" y="230" width="260" height="40" as="geometry"/>
24+ </mxCell>
25+ <mxCell id="llm" value="LLM model&#xa;(local, via Docker Model Runner)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontStyle=1" parent="1" vertex="1">
26+ <mxGeometry x="230" y="310" width="200" height="50" as="geometry"/>
27+ </mxCell>
28+ <mxCell id="alc" value="after_llm_call" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1" parent="1" vertex="1">
29+ <mxGeometry x="240" y="400" width="180" height="40" as="geometry"/>
30+ </mxCell>
31+ <mxCell id="h3" value="HOOK — OBSERVE the response (audit)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1">
32+ <mxGeometry x="500" y="400" width="260" height="40" as="geometry"/>
33+ </mxCell>
34+ <mxCell id="ptu" value="pre_tool_use" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1" parent="1" vertex="1">
35+ <mxGeometry x="240" y="480" width="180" height="40" as="geometry"/>
36+ </mxCell>
37+ <mxCell id="h4" value="HOOK — allow / deny an ACTION" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1">
38+ <mxGeometry x="500" y="480" width="260" height="40" as="geometry"/>
39+ </mxCell>
40+ <mxCell id="tool" value="tool execution" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontStyle=1" parent="1" vertex="1">
41+ <mxGeometry x="230" y="560" width="200" height="40" as="geometry"/>
42+ </mxCell>
43+ <mxCell id="trt" value="tool_response_transform" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1" parent="1" vertex="1">
44+ <mxGeometry x="230" y="640" width="200" height="40" as="geometry"/>
45+ </mxCell>
46+ <mxCell id="h5" value="HOOK — rewrite / redact the OUTPUT (e.g. PII)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1">
47+ <mxGeometry x="500" y="640" width="260" height="40" as="geometry"/>
48+ </mxCell>
49+ <mxCell id="back" value="result returned to the model — the loop continues" style="text;html=1;align=center;fontStyle=2;fontSize=11;" parent="1" vertex="1">
50+ <mxGeometry x="200" y="700" width="360" height="20" as="geometry"/>
51+ </mxCell>
52+ <mxCell id="contract" value="Hook contract&lt;br&gt;&lt;br&gt;docker-agent&amp;nbsp; ── event JSON ─▶ stdin&lt;br&gt;hook ◀─ decision JSON ── stdout&lt;br&gt;&lt;br&gt;{} (empty) = &quot;let it pass&quot;&lt;br&gt;otherwise: allow / deny · block · system_message ·&lt;br&gt;additional_context · updated_* …&lt;br&gt;&lt;br&gt;A hook is one command. No dependency: standard library only." style="rounded=1;whiteSpace=wrap;html=1;align=left;fillColor=#f5f5f5;strokeColor=#666666;fontFamily=Courier New;fontSize=11;verticalAlign=top;spacingLeft=8;spacingTop=6;arcSize=5;" parent="1" vertex="1">
53+ <mxGeometry x="160" y="740" width="540" height="180" as="geometry"/>
54+ </mxCell>
55+ <mxCell id="e_user_ups" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" parent="1" source="user" target="ups" edge="1">
56+ <mxGeometry relative="1" as="geometry"/>
57+ </mxCell>
58+ <mxCell id="e_ups_blc" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" parent="1" source="ups" target="blc" edge="1">
59+ <mxGeometry relative="1" as="geometry"/>
60+ </mxCell>
61+ <mxCell id="e_blc_llm" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" parent="1" source="blc" target="llm" edge="1">
62+ <mxGeometry relative="1" as="geometry"/>
63+ </mxCell>
64+ <mxCell id="e_llm_alc" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" parent="1" source="llm" target="alc" edge="1">
65+ <mxGeometry relative="1" as="geometry"/>
66+ </mxCell>
67+ <mxCell id="e_alc_ptu" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" parent="1" source="alc" target="ptu" edge="1">
68+ <mxGeometry relative="1" as="geometry">
69+ <Array as="points"/>
70+ </mxGeometry>
71+ </mxCell>
72+ <mxCell id="lbl_tool" value="model wants a tool?" style="edgeLabel;html=1;fontSize=10;fontStyle=2;" parent="e_alc_ptu" vertex="1" connectable="0">
73+ <mxGeometry x="-0.2" relative="1" as="geometry"/>
74+ </mxCell>
75+ <mxCell id="e_ptu_tool" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" parent="1" source="ptu" target="tool" edge="1">
76+ <mxGeometry relative="1" as="geometry"/>
77+ </mxCell>
78+ <mxCell id="e_tool_trt" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" parent="1" source="tool" target="trt" edge="1">
79+ <mxGeometry relative="1" as="geometry"/>
80+ </mxCell>
81+ <mxCell id="e_h1" style="edgeStyle=none;html=1;startArrow=block;endArrow=block;dashed=1;strokeColor=#d6b656;" parent="1" source="ups" target="h1" edge="1">
82+ <mxGeometry relative="1" as="geometry"/>
83+ </mxCell>
84+ <mxCell id="e_h2" style="edgeStyle=none;html=1;startArrow=block;endArrow=block;dashed=1;strokeColor=#d6b656;" parent="1" source="blc" target="h2" edge="1">
85+ <mxGeometry relative="1" as="geometry"/>
86+ </mxCell>
87+ <mxCell id="e_h3" style="edgeStyle=none;html=1;startArrow=block;endArrow=block;dashed=1;strokeColor=#d6b656;" parent="1" source="alc" target="h3" edge="1">
88+ <mxGeometry relative="1" as="geometry"/>
89+ </mxCell>
90+ <mxCell id="e_h4" style="edgeStyle=none;html=1;startArrow=block;endArrow=block;dashed=1;strokeColor=#d6b656;" parent="1" source="ptu" target="h4" edge="1">
91+ <mxGeometry relative="1" as="geometry"/>
92+ </mxCell>
93+ <mxCell id="e_h5" style="edgeStyle=none;html=1;startArrow=block;endArrow=block;dashed=1;strokeColor=#d6b656;" parent="1" source="trt" target="h5" edge="1">
94+ <mxGeometry relative="1" as="geometry"/>
95+ </mxCell>
96+ </root>
97+ </mxGraphModel>
98+ </diagram>
99+</mxfile>
\ No newline at end of file
new file mode 100644
@@ -0,0 +1,99 @@
1+<mxfile host="65bd71144e">
2+ <diagram name="Docker Agent hooks overview" id="hooks-overview">
3+ <mxGraphModel dx="889" dy="464" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1150" math="0" shadow="0">
4+ <root>
5+ <mxCell id="0"/>
6+ <mxCell id="1" parent="0"/>
7+ <mxCell id="title" value="Docker Agent — where each hook intercepts the agent loop" style="text;html=1;align=center;fontSize=15;fontStyle=1;verticalAlign=middle;" parent="1" vertex="1">
8+ <mxGeometry x="80" y="20" width="640" height="30" as="geometry"/>
9+ </mxCell>
10+ <mxCell id="user" value="User&#xa;(prompt)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" parent="1" vertex="1">
11+ <mxGeometry x="260" y="70" width="140" height="40" as="geometry"/>
12+ </mxCell>
13+ <mxCell id="ups" value="user_prompt_submit" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1" parent="1" vertex="1">
14+ <mxGeometry x="240" y="150" width="180" height="40" as="geometry"/>
15+ </mxCell>
16+ <mxCell id="h1" value="HOOK — moderate / rewrite the INPUT" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1">
17+ <mxGeometry x="500" y="150" width="260" height="40" as="geometry"/>
18+ </mxCell>
19+ <mxCell id="blc" value="before_llm_call" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1" parent="1" vertex="1">
20+ <mxGeometry x="240" y="230" width="180" height="40" as="geometry"/>
21+ </mxCell>
22+ <mxCell id="h2" value="HOOK — scrub the OUTGOING messages" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1">
23+ <mxGeometry x="500" y="230" width="260" height="40" as="geometry"/>
24+ </mxCell>
25+ <mxCell id="llm" value="LLM model&#xa;(local, via Docker Model Runner)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontStyle=1" parent="1" vertex="1">
26+ <mxGeometry x="230" y="310" width="200" height="50" as="geometry"/>
27+ </mxCell>
28+ <mxCell id="alc" value="after_llm_call" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1" parent="1" vertex="1">
29+ <mxGeometry x="240" y="400" width="180" height="40" as="geometry"/>
30+ </mxCell>
31+ <mxCell id="h3" value="HOOK — OBSERVE the response (audit)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1">
32+ <mxGeometry x="500" y="400" width="260" height="40" as="geometry"/>
33+ </mxCell>
34+ <mxCell id="ptu" value="pre_tool_use" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1" parent="1" vertex="1">
35+ <mxGeometry x="240" y="480" width="180" height="40" as="geometry"/>
36+ </mxCell>
37+ <mxCell id="h4" value="HOOK — allow / deny an ACTION" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1">
38+ <mxGeometry x="500" y="480" width="260" height="40" as="geometry"/>
39+ </mxCell>
40+ <mxCell id="tool" value="tool execution" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontStyle=1" parent="1" vertex="1">
41+ <mxGeometry x="230" y="560" width="200" height="40" as="geometry"/>
42+ </mxCell>
43+ <mxCell id="trt" value="tool_response_transform" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1" parent="1" vertex="1">
44+ <mxGeometry x="230" y="640" width="200" height="40" as="geometry"/>
45+ </mxCell>
46+ <mxCell id="h5" value="HOOK — rewrite / redact the OUTPUT (e.g. PII)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1">
47+ <mxGeometry x="500" y="640" width="260" height="40" as="geometry"/>
48+ </mxCell>
49+ <mxCell id="back" value="result returned to the model — the loop continues" style="text;html=1;align=center;fontStyle=2;fontSize=11;" parent="1" vertex="1">
50+ <mxGeometry x="200" y="700" width="360" height="20" as="geometry"/>
51+ </mxCell>
52+ <mxCell id="contract" value="Hook contract&lt;br&gt;&lt;br&gt;docker-agent&amp;nbsp; ── event JSON ─▶ stdin&lt;br&gt;hook ◀─ decision JSON ── stdout&lt;br&gt;&lt;br&gt;{} (empty) = &quot;let it pass&quot;&lt;br&gt;otherwise: allow / deny · block · system_message ·&lt;br&gt;additional_context · updated_* …&lt;br&gt;&lt;br&gt;A hook is one command. No dependency: standard library only." style="rounded=1;whiteSpace=wrap;html=1;align=left;fillColor=#f5f5f5;strokeColor=#666666;fontFamily=Courier New;fontSize=11;verticalAlign=top;spacingLeft=8;spacingTop=6;arcSize=5;" parent="1" vertex="1">
53+ <mxGeometry x="160" y="740" width="540" height="180" as="geometry"/>
54+ </mxCell>
55+ <mxCell id="e_user_ups" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" parent="1" source="user" target="ups" edge="1">
56+ <mxGeometry relative="1" as="geometry"/>
57+ </mxCell>
58+ <mxCell id="e_ups_blc" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" parent="1" source="ups" target="blc" edge="1">
59+ <mxGeometry relative="1" as="geometry"/>
60+ </mxCell>
61+ <mxCell id="e_blc_llm" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" parent="1" source="blc" target="llm" edge="1">
62+ <mxGeometry relative="1" as="geometry"/>
63+ </mxCell>
64+ <mxCell id="e_llm_alc" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" parent="1" source="llm" target="alc" edge="1">
65+ <mxGeometry relative="1" as="geometry"/>
66+ </mxCell>
67+ <mxCell id="e_alc_ptu" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" parent="1" source="alc" target="ptu" edge="1">
68+ <mxGeometry relative="1" as="geometry">
69+ <Array as="points"/>
70+ </mxGeometry>
71+ </mxCell>
72+ <mxCell id="lbl_tool" value="model wants a tool?" style="edgeLabel;html=1;fontSize=10;fontStyle=2;" parent="e_alc_ptu" vertex="1" connectable="0">
73+ <mxGeometry x="-0.2" relative="1" as="geometry"/>
74+ </mxCell>
75+ <mxCell id="e_ptu_tool" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" parent="1" source="ptu" target="tool" edge="1">
76+ <mxGeometry relative="1" as="geometry"/>
77+ </mxCell>
78+ <mxCell id="e_tool_trt" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;" parent="1" source="tool" target="trt" edge="1">
79+ <mxGeometry relative="1" as="geometry"/>
80+ </mxCell>
81+ <mxCell id="e_h1" style="edgeStyle=none;html=1;startArrow=block;endArrow=block;dashed=1;strokeColor=#d6b656;" parent="1" source="ups" target="h1" edge="1">
82+ <mxGeometry relative="1" as="geometry"/>
83+ </mxCell>
84+ <mxCell id="e_h2" style="edgeStyle=none;html=1;startArrow=block;endArrow=block;dashed=1;strokeColor=#d6b656;" parent="1" source="blc" target="h2" edge="1">
85+ <mxGeometry relative="1" as="geometry"/>
86+ </mxCell>
87+ <mxCell id="e_h3" style="edgeStyle=none;html=1;startArrow=block;endArrow=block;dashed=1;strokeColor=#d6b656;" parent="1" source="alc" target="h3" edge="1">
88+ <mxGeometry relative="1" as="geometry"/>
89+ </mxCell>
90+ <mxCell id="e_h4" style="edgeStyle=none;html=1;startArrow=block;endArrow=block;dashed=1;strokeColor=#d6b656;" parent="1" source="ptu" target="h4" edge="1">
91+ <mxGeometry relative="1" as="geometry"/>
92+ </mxCell>
93+ <mxCell id="e_h5" style="edgeStyle=none;html=1;startArrow=block;endArrow=block;dashed=1;strokeColor=#d6b656;" parent="1" source="trt" target="h5" edge="1">
94+ <mxGeometry relative="1" as="geometry"/>
95+ </mxCell>
96+ </root>
97+ </mxGraphModel>
98+ </diagram>
99+</mxfile>
\ No newline at end of file\ No newline at end of file
modified vendor.sh +25 -1
@@ -28,6 +28,9 @@ nothing from the network: everything it uses is in the binary.
2828 before it enters the page
2929 web/vendor/asciidoctor/ AsciiDoc -> HTML, for the editor's preview; loaded
3030 on the first .adoc preview (750 KB)
31+ web/vendor/drawio/ the draw.io viewer, for the preview of .drawio
32+ files; loaded on the first one (4 MB). Not on npm:
33+ taken from the jgraph/drawio repository, at a tag
3134 COMMENT
3235
3336 set -euo pipefail
@@ -43,6 +46,7 @@ MARKED_VERSION="9.1.2" # later majors: check renderMarkdown in app.js
4346 HIGHLIGHT_VERSION="11.8.0"
4447 DOMPURIFY_VERSION="3.4.16" # a security filter: keep it current
4548 ASCIIDOCTOR_VERSION="3.0.4" # 4.x is an ESM rewrite with an async API: see renderAsciidoc in app.js
49+DRAWIO_VERSION="31.5.3" # a tag of github.com/jgraph/drawio; check the preview of a .drawio after a bump
4650
4751 cd "$(dirname "${BASH_SOURCE[0]}")"
4852 VENDOR="web/vendor"
@@ -71,6 +75,10 @@ marked ${MARKED_VERSION} latest
7175 dompurify ${DOMPURIFY_VERSION} latest
7276 @asciidoctor/core ${ASCIIDOCTOR_VERSION} latest
7377 EOF
78+ drawio="$(curl -fsSL https://api.github.com/repos/jgraph/drawio/releases/latest | grep -o '"tag_name": *"v[^"]*"' | cut -d'"' -f4 | tr -d v || echo '?')"
79+ mark=""
80+ [ "${drawio}" != "${DRAWIO_VERSION}" ] && mark=" ⬆"
81+ printf '%-24s %-10s %-10s%s\n' "jgraph/drawio (GitHub)" "${DRAWIO_VERSION}" "${drawio}" "${mark}"
7482 echo ""
7583 echo "The comments next to the versions in $0 say which updates need more than a version bump."
7684 exit 0
@@ -104,8 +112,15 @@ fetch @asciidoctor/core "${ASCIIDOCTOR_VERSION}" asciidoctor
104112 echo "📥 cdn.tailwindcss.com/${TAILWIND_VERSION}"
105113 curl -fsSL "https://cdn.tailwindcss.com/${TAILWIND_VERSION}" -o "${TMP}/tailwind.js"
106114
115+# The draw.io viewer is not published on npm: it is a file of the jgraph/drawio
116+# repository, taken at a release tag.
117+DRAWIO_RAW="https://raw.githubusercontent.com/jgraph/drawio/v${DRAWIO_VERSION}"
118+echo "📥 jgraph/drawio v${DRAWIO_VERSION}"
119+curl -fsSL "${DRAWIO_RAW}/src/main/webapp/js/viewer-static.min.js" -o "${TMP}/viewer-static.min.js"
120+curl -fsSL "${DRAWIO_RAW}/LICENSE" -o "${TMP}/drawio-LICENSE"
121+
107122 rm -rf "${VENDOR}"
108-mkdir -p "${VENDOR}"/{monaco,xterm,material-icons,tailwind,marked,highlight,dompurify,asciidoctor}
123+mkdir -p "${VENDOR}"/{monaco,xterm,material-icons,tailwind,marked,highlight,dompurify,asciidoctor,drawio/nomath}
109124
110125 cp -R "${TMP}/monaco/min/vs" "${VENDOR}/monaco/vs"
111126 # English only: the other UI languages weigh 2 MB and are never loaded.
@@ -136,6 +151,14 @@ cp "${TMP}/dompurify/dist/purify.min.js" "${TMP}/dompurify/LICENSE" "${VENDOR}/d
136151 # no eval under the page's CSP (checked in Chromium).
137152 cp "${TMP}/asciidoctor/dist/browser/asciidoctor.min.js" "${TMP}/asciidoctor/LICENSE" "${VENDOR}/asciidoctor/"
138153
154+# The viewer loads MathJax's startup.js on every diagram, from the network by
155+# default; app.js points it here instead (DRAW_MATH_URL), at an empty stand-in:
156+# MathJax is not embedded, so a formula shows as its source. It needs no eval
157+# under the page's CSP (checked in Chromium).
158+cp "${TMP}/viewer-static.min.js" "${VENDOR}/drawio/"
159+cp "${TMP}/drawio-LICENSE" "${VENDOR}/drawio/LICENSE"
160+echo "// MathJax is not embedded (see vendor.sh): a formula in a diagram shows as its source." >"${VENDOR}/drawio/nomath/startup.js"
161+
139162 cat >"${VENDOR}/VERSIONS" <<EOF
140163 monaco-editor ${MONACO_VERSION}
141164 @xterm/xterm ${XTERM_VERSION}
@@ -146,6 +169,7 @@ marked ${MARKED_VERSION}
146169 @highlightjs/cdn-assets ${HIGHLIGHT_VERSION}
147170 dompurify ${DOMPURIFY_VERSION}
148171 @asciidoctor/core ${ASCIIDOCTOR_VERSION}
172+jgraph/drawio ${DRAWIO_VERSION} (viewer-static.min.js, from GitHub)
149173 EOF
150174
151175 echo "✅ ${VENDOR}: $(du -sh "${VENDOR}" | cut -f1)"
@@ -28,6 +28,9 @@ nothing from the network: everything it uses is in the binary.
28 before it enters the page28 before it enters the page
29 web/vendor/asciidoctor/ AsciiDoc -> HTML, for the editor's preview; loaded29 web/vendor/asciidoctor/ AsciiDoc -> HTML, for the editor's preview; loaded
30 on the first .adoc preview (750 KB)30 on the first .adoc preview (750 KB)
31+ web/vendor/drawio/ the draw.io viewer, for the preview of .drawio
32+ files; loaded on the first one (4 MB). Not on npm:
33+ taken from the jgraph/drawio repository, at a tag
31 COMMENT34 COMMENT
32 35
33 set -euo pipefail36 set -euo pipefail
@@ -43,6 +46,7 @@ MARKED_VERSION="9.1.2" # later majors: check renderMarkdown in app.js
43 HIGHLIGHT_VERSION="11.8.0"46 HIGHLIGHT_VERSION="11.8.0"
44 DOMPURIFY_VERSION="3.4.16" # a security filter: keep it current47 DOMPURIFY_VERSION="3.4.16" # a security filter: keep it current
45 ASCIIDOCTOR_VERSION="3.0.4" # 4.x is an ESM rewrite with an async API: see renderAsciidoc in app.js48 ASCIIDOCTOR_VERSION="3.0.4" # 4.x is an ESM rewrite with an async API: see renderAsciidoc in app.js
49+DRAWIO_VERSION="31.5.3" # a tag of github.com/jgraph/drawio; check the preview of a .drawio after a bump
46 50
47 cd "$(dirname "${BASH_SOURCE[0]}")"51 cd "$(dirname "${BASH_SOURCE[0]}")"
48 VENDOR="web/vendor"52 VENDOR="web/vendor"
@@ -71,6 +75,10 @@ marked ${MARKED_VERSION} latest
71 dompurify ${DOMPURIFY_VERSION} latest75 dompurify ${DOMPURIFY_VERSION} latest
72 @asciidoctor/core ${ASCIIDOCTOR_VERSION} latest76 @asciidoctor/core ${ASCIIDOCTOR_VERSION} latest
73 EOF77 EOF
78+ drawio="$(curl -fsSL https://api.github.com/repos/jgraph/drawio/releases/latest | grep -o '"tag_name": *"v[^"]*"' | cut -d'"' -f4 | tr -d v || echo '?')"
79+ mark=""
80+ [ "${drawio}" != "${DRAWIO_VERSION}" ] && mark=" ⬆"
81+ printf '%-24s %-10s %-10s%s\n' "jgraph/drawio (GitHub)" "${DRAWIO_VERSION}" "${drawio}" "${mark}"
74 echo ""82 echo ""
75 echo "The comments next to the versions in $0 say which updates need more than a version bump."83 echo "The comments next to the versions in $0 say which updates need more than a version bump."
76 exit 084 exit 0
@@ -104,8 +112,15 @@ fetch @asciidoctor/core "${ASCIIDOCTOR_VERSION}" asciidoctor
104 echo "📥 cdn.tailwindcss.com/${TAILWIND_VERSION}"112 echo "📥 cdn.tailwindcss.com/${TAILWIND_VERSION}"
105 curl -fsSL "https://cdn.tailwindcss.com/${TAILWIND_VERSION}" -o "${TMP}/tailwind.js"113 curl -fsSL "https://cdn.tailwindcss.com/${TAILWIND_VERSION}" -o "${TMP}/tailwind.js"
106 114
115+# The draw.io viewer is not published on npm: it is a file of the jgraph/drawio
116+# repository, taken at a release tag.
117+DRAWIO_RAW="https://raw.githubusercontent.com/jgraph/drawio/v${DRAWIO_VERSION}"
118+echo "📥 jgraph/drawio v${DRAWIO_VERSION}"
119+curl -fsSL "${DRAWIO_RAW}/src/main/webapp/js/viewer-static.min.js" -o "${TMP}/viewer-static.min.js"
120+curl -fsSL "${DRAWIO_RAW}/LICENSE" -o "${TMP}/drawio-LICENSE"
121+
107 rm -rf "${VENDOR}"122 rm -rf "${VENDOR}"
108-mkdir -p "${VENDOR}"/{monaco,xterm,material-icons,tailwind,marked,highlight,dompurify,asciidoctor}123+mkdir -p "${VENDOR}"/{monaco,xterm,material-icons,tailwind,marked,highlight,dompurify,asciidoctor,drawio/nomath}
109 124
110 cp -R "${TMP}/monaco/min/vs" "${VENDOR}/monaco/vs"125 cp -R "${TMP}/monaco/min/vs" "${VENDOR}/monaco/vs"
111 # English only: the other UI languages weigh 2 MB and are never loaded.126 # English only: the other UI languages weigh 2 MB and are never loaded.
@@ -136,6 +151,14 @@ cp "${TMP}/dompurify/dist/purify.min.js" "${TMP}/dompurify/LICENSE" "${VENDOR}/d
136 # no eval under the page's CSP (checked in Chromium).151 # no eval under the page's CSP (checked in Chromium).
137 cp "${TMP}/asciidoctor/dist/browser/asciidoctor.min.js" "${TMP}/asciidoctor/LICENSE" "${VENDOR}/asciidoctor/"152 cp "${TMP}/asciidoctor/dist/browser/asciidoctor.min.js" "${TMP}/asciidoctor/LICENSE" "${VENDOR}/asciidoctor/"
138 153
154+# The viewer loads MathJax's startup.js on every diagram, from the network by
155+# default; app.js points it here instead (DRAW_MATH_URL), at an empty stand-in:
156+# MathJax is not embedded, so a formula shows as its source. It needs no eval
157+# under the page's CSP (checked in Chromium).
158+cp "${TMP}/viewer-static.min.js" "${VENDOR}/drawio/"
159+cp "${TMP}/drawio-LICENSE" "${VENDOR}/drawio/LICENSE"
160+echo "// MathJax is not embedded (see vendor.sh): a formula in a diagram shows as its source." >"${VENDOR}/drawio/nomath/startup.js"
161+
139 cat >"${VENDOR}/VERSIONS" <<EOF162 cat >"${VENDOR}/VERSIONS" <<EOF
140 monaco-editor ${MONACO_VERSION}163 monaco-editor ${MONACO_VERSION}
141 @xterm/xterm ${XTERM_VERSION}164 @xterm/xterm ${XTERM_VERSION}
@@ -146,6 +169,7 @@ marked ${MARKED_VERSION}
146 @highlightjs/cdn-assets ${HIGHLIGHT_VERSION}169 @highlightjs/cdn-assets ${HIGHLIGHT_VERSION}
147 dompurify ${DOMPURIFY_VERSION}170 dompurify ${DOMPURIFY_VERSION}
148 @asciidoctor/core ${ASCIIDOCTOR_VERSION}171 @asciidoctor/core ${ASCIIDOCTOR_VERSION}
172+jgraph/drawio ${DRAWIO_VERSION} (viewer-static.min.js, from GitHub)
149 EOF173 EOF
150 174
151 echo "✅ ${VENDOR}: $(du -sh "${VENDOR}" | cut -f1)"175 echo "✅ ${VENDOR}: $(du -sh "${VENDOR}" | cut -f1)"
modified web/app.css +4 -0
@@ -138,6 +138,10 @@ details:not(.thinking) .spinner-ring { display: none; }
138138 .preview { flex: 1; min-height: 0; overflow: auto; padding: 1rem 1.5rem; font-size: 0.9rem; line-height: 1.6; }
139139 .preview-frame { flex: 1; min-height: 0; width: 100%; border: 0; background: #fff; }
140140 .preview[hidden], .preview-frame[hidden] { display: none; }
141+/* An image: centred, on a checkerboard so transparency shows. A diagram: on
142+ white, as draw.io draws it (its colours assume a light page). */
143+.preview-image { display: block; margin: 0 auto; max-width: 100%; height: auto; background-color: #fff; background-image: linear-gradient(45deg, #ddd 25%, transparent 25%), linear-gradient(-45deg, #ddd 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ddd 75%), linear-gradient(-45deg, transparent 75%, #ddd 75%); background-size: 16px 16px; background-position: 0 0, 0 8px, 8px -8px, -8px 0; }
144+.preview-diagram { background: #fff; border-radius: 0.375rem; min-height: 100%; color: #000; }
141145 .preview h1 { font-size: 1.6rem; border-bottom: 1px solid var(--border); padding-bottom: 0.3rem; }
142146 .preview h2 { font-size: 1.3rem; border-bottom: 1px solid var(--border); padding-bottom: 0.2rem; }
143147 .preview h3 { font-size: 1.1rem; }
@@ -138,6 +138,10 @@ details:not(.thinking) .spinner-ring { display: none; }
138 .preview { flex: 1; min-height: 0; overflow: auto; padding: 1rem 1.5rem; font-size: 0.9rem; line-height: 1.6; }138 .preview { flex: 1; min-height: 0; overflow: auto; padding: 1rem 1.5rem; font-size: 0.9rem; line-height: 1.6; }
139 .preview-frame { flex: 1; min-height: 0; width: 100%; border: 0; background: #fff; }139 .preview-frame { flex: 1; min-height: 0; width: 100%; border: 0; background: #fff; }
140 .preview[hidden], .preview-frame[hidden] { display: none; }140 .preview[hidden], .preview-frame[hidden] { display: none; }
141+/* An image: centred, on a checkerboard so transparency shows. A diagram: on
142+ white, as draw.io draws it (its colours assume a light page). */
143+.preview-image { display: block; margin: 0 auto; max-width: 100%; height: auto; background-color: #fff; background-image: linear-gradient(45deg, #ddd 25%, transparent 25%), linear-gradient(-45deg, #ddd 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ddd 75%), linear-gradient(-45deg, transparent 75%, #ddd 75%); background-size: 16px 16px; background-position: 0 0, 0 8px, 8px -8px, -8px 0; }
144+.preview-diagram { background: #fff; border-radius: 0.375rem; min-height: 100%; color: #000; }
141 .preview h1 { font-size: 1.6rem; border-bottom: 1px solid var(--border); padding-bottom: 0.3rem; }145 .preview h1 { font-size: 1.6rem; border-bottom: 1px solid var(--border); padding-bottom: 0.3rem; }
142 .preview h2 { font-size: 1.3rem; border-bottom: 1px solid var(--border); padding-bottom: 0.2rem; }146 .preview h2 { font-size: 1.3rem; border-bottom: 1px solid var(--border); padding-bottom: 0.2rem; }
143 .preview h3 { font-size: 1.1rem; }147 .preview h3 { font-size: 1.1rem; }
modified web/app.js +181 -20
@@ -932,7 +932,7 @@ const explorerMsg = document.getElementById('explorer-msg');
932932 const expanded = new Set(); // directories open in the tree
933933 const dirViews = new Map(); // directory path -> { container, depth } as rendered
934934 let selectedPath = null; // last row clicked in the tree
935-const tabs = new Map(); // path -> { path, model, mtime, saved, view, el, preview }
935+const tabs = new Map(); // path -> { path, model, mtime, saved, view, el, preview } (model: null for an image)
936936 let activePath = null;
937937 let editor = null;
938938 let monacoReady = null;
@@ -1273,7 +1273,7 @@ function movePaths(from, to) {
12731273 tab.el.title = tab.path;
12741274 tab.el.querySelector('.tab-name').textContent = baseName(tab.path);
12751275 setIcon(tab.el.querySelector('.tab-icon'), baseName(tab.path), false);
1276- monaco.editor.setModelLanguage(tab.model, languageFor(tab.path));
1276+ if (tab.model) monaco.editor.setModelLanguage(tab.model, languageFor(tab.path));
12771277 }
12781278 const entries = [...tabs.values()].map(t => [t.path, t]);
12791279 tabs.clear();
@@ -1435,7 +1435,7 @@ function setEditorMsg(text, cls = '') {
14351435 editorMsgEl.className = cls;
14361436 }
14371437
1438-const isDirty = (tab) => tab.model.getAlternativeVersionId() !== tab.saved;
1438+const isDirty = (tab) => Boolean(tab.model) && tab.model.getAlternativeVersionId() !== tab.saved;
14391439
14401440 function paintTab(tab) {
14411441 tab.el.classList.toggle('dirty', isDirty(tab));
@@ -1445,6 +1445,7 @@ function paintTab(tab) {
14451445
14461446 async function openFile(path) {
14471447 if (tabs.has(path)) return activate(path);
1448+ if (previewKind(path) === 'image') return openImage(path);
14481449 let data;
14491450 try {
14501451 [data] = await Promise.all([api(`/api/file?path=${q(path)}`), loadMonaco(), loadIconTheme()]);
@@ -1454,7 +1455,32 @@ async function openFile(path) {
14541455 }
14551456 if (tabs.has(path)) return activate(path); // opened twice meanwhile
14561457 // The URI's extension is what Monaco picks the language from.
1457- const model = monaco.editor.createModel(data.content, undefined, monaco.Uri.file('/' + path));
1458+ // Monaco does not know .drawio: it is XML.
1459+ const lang = previewKind(path) === 'drawio' ? 'xml' : undefined;
1460+ const model = monaco.editor.createModel(data.content, lang, monaco.Uri.file('/' + path));
1461+ // A picture opens as a picture: an SVG or a diagram shows rendered first,
1462+ // its source one click away; Markdown & co. open as source.
1463+ const tab = addTab(path, { model, mtime: data.mtime, saved: model.getAlternativeVersionId(), preview: opensRendered(path) });
1464+ model.onDidChangeContent(() => { paintTab(tab); schedulePreview(tab); });
1465+ activate(path);
1466+}
1467+
1468+// An image other than SVG is not text: no model, no source, no save — a tab
1469+// that shows it, read through /api/raw.
1470+async function openImage(path) {
1471+ try {
1472+ await Promise.all([loadMonaco(), loadIconTheme()]);
1473+ } catch (e) {
1474+ appendError(`Cannot open ${path}: ${e.message}`);
1475+ return;
1476+ }
1477+ if (tabs.has(path)) return activate(path);
1478+ addTab(path, { model: null, mtime: '', saved: 0, preview: true, stamp: Date.now() });
1479+ activate(path);
1480+}
1481+
1482+// addTab makes the tab element of a file and registers it.
1483+function addTab(path, fields) {
14581484 const el = document.createElement('div');
14591485 el.className = 'tab';
14601486 el.title = path;
@@ -1465,14 +1491,14 @@ async function openFile(path) {
14651491 close.className = 'tab-close';
14661492 close.title = 'Close';
14671493 el.append(iconImg('tab-icon', baseName(path), false), name, close);
1468- const tab = { path, model, mtime: data.mtime, saved: model.getAlternativeVersionId(), view: null, el, preview: false };
1469- el.onclick = () => activate(path);
1470- el.onauxclick = (e) => { if (e.button === 1) closeTab(path); };
1471- close.onclick = (e) => { e.stopPropagation(); closeTab(path); };
1472- model.onDidChangeContent(() => { paintTab(tab); schedulePreview(tab); });
1494+ const tab = { path, view: null, el, ...fields };
1495+ // The handlers read tab.path, not `path`: a rename moves the tab.
1496+ el.onclick = () => activate(tab.path);
1497+ el.onauxclick = (e) => { if (e.button === 1) closeTab(tab.path); };
1498+ close.onclick = (e) => { e.stopPropagation(); closeTab(tab.path); };
14731499 tabs.set(path, tab);
14741500 tabsEl.append(el);
1475- activate(path);
1501+ return tab;
14761502 }
14771503
14781504 function activate(path) {
@@ -1481,10 +1507,10 @@ function activate(path) {
14811507 showEditor(true);
14821508 ensureEditor();
14831509 const prev = tabs.get(activePath);
1484- if (prev && prev !== tab) prev.view = editor.saveViewState();
1510+ if (prev && prev !== tab && prev.model) prev.view = editor.saveViewState();
14851511 activePath = path;
14861512 editor.setModel(tab.model);
1487- if (tab.view) editor.restoreViewState(tab.view);
1513+ if (tab.model && tab.view) editor.restoreViewState(tab.view);
14881514 editorPathEl.textContent = path;
14891515 editorPathEl.title = path;
14901516 setEditorMsg('');
@@ -1521,12 +1547,12 @@ function closeTab(path, force = false) {
15211547 markActiveRow();
15221548 }
15231549 }
1524- tab.model.dispose();
1550+ tab.model?.dispose();
15251551 }
15261552
15271553 async function saveActive(force = false) {
15281554 const tab = tabs.get(activePath);
1529- if (!tab) return;
1555+ if (!tab?.model) return;
15301556 // What is sent is what gets marked as saved, even if the user
15311557 // types on while the request is in flight.
15321558 const version = tab.model.getAlternativeVersionId();
@@ -1601,15 +1627,25 @@ const PREVIEW_PURIFY = { ...PURIFY, SANITIZE_NAMED_PROPS: true };
16011627 // an include shows as a link.
16021628 const ADOC_OPTIONS = { safe: 'secure', attributes: { showtitle: true } };
16031629
1604-// How a file is previewed, from its extension; null: it is not.
1630+// How a file is previewed, from its extension; null: it is not. An .svg is
1631+// text (a model, a source, a preview); the other images are 'image' — no
1632+// source at all. A .drawio.svg is an SVG and a .drawio.png a PNG: draw.io
1633+// embeds the diagram in both, and they show as the pictures they are.
1634+const IMAGE_EXTS = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'bmp', 'ico'];
16051635 function previewKind(path) {
16061636 const ext = baseName(path).toLowerCase().split('.').pop();
16071637 if (['md', 'markdown', 'mdown', 'mkd'].includes(ext)) return 'markdown';
16081638 if (['adoc', 'asciidoc', 'asc'].includes(ext)) return 'asciidoc';
16091639 if (['html', 'htm'].includes(ext)) return 'html';
1640+ if (ext === 'svg') return 'svg';
1641+ if (['drawio', 'dio'].includes(ext)) return 'drawio';
1642+ if (IMAGE_EXTS.includes(ext)) return 'image';
16101643 return null;
16111644 }
16121645
1646+// Pictures open rendered; documents open as source.
1647+const opensRendered = (path) => ['svg', 'drawio', 'image'].includes(previewKind(path));
1648+
16131649 // Asciidoctor.js (750 KB) is loaded on the first AsciiDoc preview. Its
16141650 // browser build is a classic script that defines one global.
16151651 function loadAsciidoctor() {
@@ -1659,8 +1695,8 @@ function resolveIn(dir, href) {
16591695 // the button that switches: only for a file that has a preview.
16601696 function showPane(tab) {
16611697 const kind = tab ? previewKind(tab.path) : null;
1662- const on = Boolean(kind && tab.preview);
1663- previewBtn.hidden = !kind;
1698+ const on = Boolean(kind && (tab.preview || !tab.model));
1699+ previewBtn.hidden = !kind || !tab.model; // an image has no source to go back to
16641700 previewBtn.textContent = on ? 'Source' : 'Preview';
16651701 previewBtn.classList.toggle('on', on);
16661702 editorEl.hidden = !tab || on;
@@ -1670,14 +1706,21 @@ function showPane(tab) {
16701706 renderPreview(tab);
16711707 } else {
16721708 // Nothing of a closed preview stays in the page.
1673- previewEl.replaceChildren();
1709+ clearPreview();
16741710 previewFrame.removeAttribute('srcdoc');
16751711 }
16761712 }
16771713
1714+// Empties the preview, the draw.io viewer included (it holds listeners).
1715+function clearPreview() {
1716+ drawioViewer?.graph?.destroy();
1717+ drawioViewer = null;
1718+ previewEl.replaceChildren();
1719+}
1720+
16781721 function togglePreview() {
16791722 const tab = tabs.get(activePath);
1680- if (!tab || !previewKind(tab.path)) return;
1723+ if (!tab?.model || !previewKind(tab.path)) return;
16811724 tab.preview = !tab.preview;
16821725 showPane(tab);
16831726 if (!tab.preview) editor.focus();
@@ -1693,11 +1736,23 @@ function schedulePreview(tab) {
16931736
16941737 async function renderPreview(tab) {
16951738 const kind = previewKind(tab.path);
1696- const text = tab.model.getValue();
1739+ const text = tab.model ? tab.model.getValue() : '';
16971740 if (kind === 'html') {
16981741 previewFrame.srcdoc = text;
16991742 return;
17001743 }
1744+ if (kind === 'image' || kind === 'svg') {
1745+ // An SVG shows what the editor holds, unsaved edits included; as an
1746+ // <img>, its scripts never run.
1747+ showImage(tab, kind === 'svg'
1748+ ? `data:image/svg+xml;charset=utf-8,${encodeURIComponent(text)}`
1749+ : `/api/raw?path=${q(tab.path)}&v=${tab.stamp || 0}`);
1750+ return;
1751+ }
1752+ if (kind === 'drawio') {
1753+ await renderDrawio(tab, text);
1754+ return;
1755+ }
17011756 let html;
17021757 try {
17031758 if (kind === 'markdown') {
@@ -1713,6 +1768,7 @@ async function renderPreview(tab) {
17131768 // The user may have switched tab, or back to the source, meanwhile.
17141769 if (html === null || tab.path !== activePath || !tab.preview) return;
17151770 const scroll = previewEl.scrollTop;
1771+ clearPreview();
17161772 previewEl.innerHTML = DOMPurify.sanitize(html, PREVIEW_PURIFY);
17171773 const dir = parentOf(tab.path);
17181774 // Images next to the document come from /api/raw (images only, inside
@@ -1733,6 +1789,104 @@ async function renderPreview(tab) {
17331789 previewEl.scrollTop = scroll;
17341790 }
17351791
1792+// showImage puts one picture in the preview, and its size in the status bar.
1793+function showImage(tab, src) {
1794+ const img = document.createElement('img');
1795+ img.className = 'preview-image';
1796+ img.alt = baseName(tab.path);
1797+ img.onload = () => { if (tab.path === activePath) setEditorMsg(`${img.naturalWidth} × ${img.naturalHeight}`); };
1798+ img.onerror = () => {
1799+ if (tab.path !== activePath) return;
1800+ clearPreview();
1801+ previewEl.textContent = `Cannot show ${tab.path}: not a valid image`;
1802+ };
1803+ clearPreview();
1804+ previewEl.append(img);
1805+ img.src = src;
1806+}
1807+
1808+// --- draw.io
1809+//
1810+// The official viewer (viewer-static.min.js, 4 MB) renders a .drawio file as
1811+// draw.io itself does: pages, layers, zoom. Loaded on the first diagram. It
1812+// runs in the page, which is acceptable because of the CSP: it sanitises the
1813+// HTML of labels itself, and whatever got through could not run a script
1814+// (no inline scripts, no handlers, no eval here) nor load anything from
1815+// elsewhere. Its resources would come from viewer.diagrams.net: the globals
1816+// below point them into the binary, MathJax at an empty stand-in (a formula
1817+// shows as its source), the rest at paths that do not exist — shape
1818+// libraries fetched on demand (some vendor stencils) are therefore missing.
1819+
1820+const DRAWIO = 'vendor/drawio';
1821+let drawioLoad = null;
1822+let drawioViewer = null;
1823+
1824+function loadDrawio() {
1825+ if (drawioLoad) return drawioLoad;
1826+ Object.assign(window, {
1827+ DRAW_MATH_URL: `${DRAWIO}/nomath`,
1828+ STENCIL_PATH: `${DRAWIO}/stencils`,
1829+ GRAPH_IMAGE_PATH: `${DRAWIO}/img`,
1830+ mxBasePath: `${DRAWIO}/mxgraph`,
1831+ RESOURCES_PATH: `${DRAWIO}/resources`,
1832+ mxLoadResources: false,
1833+ mxLoadStylesheets: false
1834+ });
1835+ drawioLoad = new Promise((resolve, reject) => {
1836+ // Monaco's AMD loader is there by now: with a global `define`, the
1837+ // libraries bundled in the viewer register as AMD modules ("Can only
1838+ // have one anonymous define call per script file") instead of setting
1839+ // their globals. Hidden while the viewer's script runs.
1840+ const amd = window.define;
1841+ window.define = undefined;
1842+ const done = () => { window.define = amd; };
1843+ const script = document.createElement('script');
1844+ script.src = `${DRAWIO}/viewer-static.min.js`;
1845+ script.onload = () => {
1846+ done();
1847+ if (window.GraphViewer) resolve();
1848+ else reject(new Error('the draw.io viewer did not start'));
1849+ };
1850+ script.onerror = () => {
1851+ done();
1852+ reject(new Error('cannot load the draw.io viewer'));
1853+ };
1854+ document.head.append(script);
1855+ });
1856+ drawioLoad.catch(() => { drawioLoad = null; });
1857+ return drawioLoad;
1858+}
1859+
1860+async function renderDrawio(tab, text) {
1861+ try {
1862+ await loadDrawio();
1863+ } catch (e) {
1864+ previewEl.textContent = `Cannot render ${tab.path}: ${e.message}`;
1865+ return;
1866+ }
1867+ if (tab.path !== activePath || !tab.preview) return;
1868+ const doc = mxUtils.parseXml(text);
1869+ const root = doc.documentElement;
1870+ if (!root || root.nodeName === 'parsererror' || doc.getElementsByTagName('parsererror').length) {
1871+ clearPreview();
1872+ previewEl.textContent = `Cannot render ${tab.path}: not valid draw.io XML`;
1873+ return;
1874+ }
1875+ clearPreview();
1876+ const box = document.createElement('div');
1877+ box.className = 'preview-diagram';
1878+ previewEl.append(box);
1879+ try {
1880+ drawioViewer = new GraphViewer(box, root, {
1881+ highlight: '#0969da', nav: true, lightbox: false, resize: true, center: true,
1882+ toolbar: 'zoom layers pages', 'toolbar-nohide': true, edit: null
1883+ });
1884+ } catch (e) {
1885+ clearPreview();
1886+ previewEl.textContent = `Cannot render ${tab.path}: ${e.message}`;
1887+ }
1888+}
1889+
17361890 // An anchor scrolls the preview (DOMPurify prefixed the ids); a relative
17371891 // link opens the file, in preview when it has one.
17381892 previewEl.addEventListener('click', async (e) => {
@@ -1778,6 +1932,13 @@ function scheduleFilesSync() {
17781932 async function syncFiles() {
17791933 refreshTree();
17801934 for (const tab of [...tabs.values()]) {
1935+ if (!tab.model) {
1936+ // An image: shown again from the disk (no-cache: an unchanged
1937+ // file costs a 304).
1938+ tab.stamp = Date.now();
1939+ if (tab.path === activePath) renderPreview(tab);
1940+ continue;
1941+ }
17811942 if (isDirty(tab)) continue;
17821943 try {
17831944 const data = await api(`/api/file?path=${q(tab.path)}`);
@@ -932,7 +932,7 @@ const explorerMsg = document.getElementById('explorer-msg');
932 const expanded = new Set(); // directories open in the tree932 const expanded = new Set(); // directories open in the tree
933 const dirViews = new Map(); // directory path -> { container, depth } as rendered933 const dirViews = new Map(); // directory path -> { container, depth } as rendered
934 let selectedPath = null; // last row clicked in the tree934 let selectedPath = null; // last row clicked in the tree
935-const tabs = new Map(); // path -> { path, model, mtime, saved, view, el, preview }935+const tabs = new Map(); // path -> { path, model, mtime, saved, view, el, preview } (model: null for an image)
936 let activePath = null;936 let activePath = null;
937 let editor = null;937 let editor = null;
938 let monacoReady = null;938 let monacoReady = null;
@@ -1273,7 +1273,7 @@ function movePaths(from, to) {
1273 tab.el.title = tab.path;1273 tab.el.title = tab.path;
1274 tab.el.querySelector('.tab-name').textContent = baseName(tab.path);1274 tab.el.querySelector('.tab-name').textContent = baseName(tab.path);
1275 setIcon(tab.el.querySelector('.tab-icon'), baseName(tab.path), false);1275 setIcon(tab.el.querySelector('.tab-icon'), baseName(tab.path), false);
1276- monaco.editor.setModelLanguage(tab.model, languageFor(tab.path));1276+ if (tab.model) monaco.editor.setModelLanguage(tab.model, languageFor(tab.path));
1277 }1277 }
1278 const entries = [...tabs.values()].map(t => [t.path, t]);1278 const entries = [...tabs.values()].map(t => [t.path, t]);
1279 tabs.clear();1279 tabs.clear();
@@ -1435,7 +1435,7 @@ function setEditorMsg(text, cls = '') {
1435 editorMsgEl.className = cls;1435 editorMsgEl.className = cls;
1436 }1436 }
1437 1437
1438-const isDirty = (tab) => tab.model.getAlternativeVersionId() !== tab.saved;1438+const isDirty = (tab) => Boolean(tab.model) && tab.model.getAlternativeVersionId() !== tab.saved;
1439 1439
1440 function paintTab(tab) {1440 function paintTab(tab) {
1441 tab.el.classList.toggle('dirty', isDirty(tab));1441 tab.el.classList.toggle('dirty', isDirty(tab));
@@ -1445,6 +1445,7 @@ function paintTab(tab) {
1445 1445
1446 async function openFile(path) {1446 async function openFile(path) {
1447 if (tabs.has(path)) return activate(path);1447 if (tabs.has(path)) return activate(path);
1448+ if (previewKind(path) === 'image') return openImage(path);
1448 let data;1449 let data;
1449 try {1450 try {
1450 [data] = await Promise.all([api(`/api/file?path=${q(path)}`), loadMonaco(), loadIconTheme()]);1451 [data] = await Promise.all([api(`/api/file?path=${q(path)}`), loadMonaco(), loadIconTheme()]);
@@ -1454,7 +1455,32 @@ async function openFile(path) {
1454 }1455 }
1455 if (tabs.has(path)) return activate(path); // opened twice meanwhile1456 if (tabs.has(path)) return activate(path); // opened twice meanwhile
1456 // The URI's extension is what Monaco picks the language from.1457 // The URI's extension is what Monaco picks the language from.
1457- const model = monaco.editor.createModel(data.content, undefined, monaco.Uri.file('/' + path));1458+ // Monaco does not know .drawio: it is XML.
1459+ const lang = previewKind(path) === 'drawio' ? 'xml' : undefined;
1460+ const model = monaco.editor.createModel(data.content, lang, monaco.Uri.file('/' + path));
1461+ // A picture opens as a picture: an SVG or a diagram shows rendered first,
1462+ // its source one click away; Markdown & co. open as source.
1463+ const tab = addTab(path, { model, mtime: data.mtime, saved: model.getAlternativeVersionId(), preview: opensRendered(path) });
1464+ model.onDidChangeContent(() => { paintTab(tab); schedulePreview(tab); });
1465+ activate(path);
1466+}
1467+
1468+// An image other than SVG is not text: no model, no source, no save — a tab
1469+// that shows it, read through /api/raw.
1470+async function openImage(path) {
1471+ try {
1472+ await Promise.all([loadMonaco(), loadIconTheme()]);
1473+ } catch (e) {
1474+ appendError(`Cannot open ${path}: ${e.message}`);
1475+ return;
1476+ }
1477+ if (tabs.has(path)) return activate(path);
1478+ addTab(path, { model: null, mtime: '', saved: 0, preview: true, stamp: Date.now() });
1479+ activate(path);
1480+}
1481+
1482+// addTab makes the tab element of a file and registers it.
1483+function addTab(path, fields) {
1458 const el = document.createElement('div');1484 const el = document.createElement('div');
1459 el.className = 'tab';1485 el.className = 'tab';
1460 el.title = path;1486 el.title = path;
@@ -1465,14 +1491,14 @@ async function openFile(path) {
1465 close.className = 'tab-close';1491 close.className = 'tab-close';
1466 close.title = 'Close';1492 close.title = 'Close';
1467 el.append(iconImg('tab-icon', baseName(path), false), name, close);1493 el.append(iconImg('tab-icon', baseName(path), false), name, close);
1468- const tab = { path, model, mtime: data.mtime, saved: model.getAlternativeVersionId(), view: null, el, preview: false };1494+ const tab = { path, view: null, el, ...fields };
1469- el.onclick = () => activate(path);1495+ // The handlers read tab.path, not `path`: a rename moves the tab.
1470- el.onauxclick = (e) => { if (e.button === 1) closeTab(path); };1496+ el.onclick = () => activate(tab.path);
1471- close.onclick = (e) => { e.stopPropagation(); closeTab(path); };1497+ el.onauxclick = (e) => { if (e.button === 1) closeTab(tab.path); };
1472- model.onDidChangeContent(() => { paintTab(tab); schedulePreview(tab); });1498+ close.onclick = (e) => { e.stopPropagation(); closeTab(tab.path); };
1473 tabs.set(path, tab);1499 tabs.set(path, tab);
1474 tabsEl.append(el);1500 tabsEl.append(el);
1475- activate(path);1501+ return tab;
1476 }1502 }
1477 1503
1478 function activate(path) {1504 function activate(path) {
@@ -1481,10 +1507,10 @@ function activate(path) {
1481 showEditor(true);1507 showEditor(true);
1482 ensureEditor();1508 ensureEditor();
1483 const prev = tabs.get(activePath);1509 const prev = tabs.get(activePath);
1484- if (prev && prev !== tab) prev.view = editor.saveViewState();1510+ if (prev && prev !== tab && prev.model) prev.view = editor.saveViewState();
1485 activePath = path;1511 activePath = path;
1486 editor.setModel(tab.model);1512 editor.setModel(tab.model);
1487- if (tab.view) editor.restoreViewState(tab.view);1513+ if (tab.model && tab.view) editor.restoreViewState(tab.view);
1488 editorPathEl.textContent = path;1514 editorPathEl.textContent = path;
1489 editorPathEl.title = path;1515 editorPathEl.title = path;
1490 setEditorMsg('');1516 setEditorMsg('');
@@ -1521,12 +1547,12 @@ function closeTab(path, force = false) {
1521 markActiveRow();1547 markActiveRow();
1522 }1548 }
1523 }1549 }
1524- tab.model.dispose();1550+ tab.model?.dispose();
1525 }1551 }
1526 1552
1527 async function saveActive(force = false) {1553 async function saveActive(force = false) {
1528 const tab = tabs.get(activePath);1554 const tab = tabs.get(activePath);
1529- if (!tab) return;1555+ if (!tab?.model) return;
1530 // What is sent is what gets marked as saved, even if the user1556 // What is sent is what gets marked as saved, even if the user
1531 // types on while the request is in flight.1557 // types on while the request is in flight.
1532 const version = tab.model.getAlternativeVersionId();1558 const version = tab.model.getAlternativeVersionId();
@@ -1601,15 +1627,25 @@ const PREVIEW_PURIFY = { ...PURIFY, SANITIZE_NAMED_PROPS: true };
1601 // an include shows as a link.1627 // an include shows as a link.
1602 const ADOC_OPTIONS = { safe: 'secure', attributes: { showtitle: true } };1628 const ADOC_OPTIONS = { safe: 'secure', attributes: { showtitle: true } };
1603 1629
1604-// How a file is previewed, from its extension; null: it is not.1630+// How a file is previewed, from its extension; null: it is not. An .svg is
1631+// text (a model, a source, a preview); the other images are 'image' — no
1632+// source at all. A .drawio.svg is an SVG and a .drawio.png a PNG: draw.io
1633+// embeds the diagram in both, and they show as the pictures they are.
1634+const IMAGE_EXTS = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'bmp', 'ico'];
1605 function previewKind(path) {1635 function previewKind(path) {
1606 const ext = baseName(path).toLowerCase().split('.').pop();1636 const ext = baseName(path).toLowerCase().split('.').pop();
1607 if (['md', 'markdown', 'mdown', 'mkd'].includes(ext)) return 'markdown';1637 if (['md', 'markdown', 'mdown', 'mkd'].includes(ext)) return 'markdown';
1608 if (['adoc', 'asciidoc', 'asc'].includes(ext)) return 'asciidoc';1638 if (['adoc', 'asciidoc', 'asc'].includes(ext)) return 'asciidoc';
1609 if (['html', 'htm'].includes(ext)) return 'html';1639 if (['html', 'htm'].includes(ext)) return 'html';
1640+ if (ext === 'svg') return 'svg';
1641+ if (['drawio', 'dio'].includes(ext)) return 'drawio';
1642+ if (IMAGE_EXTS.includes(ext)) return 'image';
1610 return null;1643 return null;
1611 }1644 }
1612 1645
1646+// Pictures open rendered; documents open as source.
1647+const opensRendered = (path) => ['svg', 'drawio', 'image'].includes(previewKind(path));
1648+
1613 // Asciidoctor.js (750 KB) is loaded on the first AsciiDoc preview. Its1649 // Asciidoctor.js (750 KB) is loaded on the first AsciiDoc preview. Its
1614 // browser build is a classic script that defines one global.1650 // browser build is a classic script that defines one global.
1615 function loadAsciidoctor() {1651 function loadAsciidoctor() {
@@ -1659,8 +1695,8 @@ function resolveIn(dir, href) {
1659 // the button that switches: only for a file that has a preview.1695 // the button that switches: only for a file that has a preview.
1660 function showPane(tab) {1696 function showPane(tab) {
1661 const kind = tab ? previewKind(tab.path) : null;1697 const kind = tab ? previewKind(tab.path) : null;
1662- const on = Boolean(kind && tab.preview);1698+ const on = Boolean(kind && (tab.preview || !tab.model));
1663- previewBtn.hidden = !kind;1699+ previewBtn.hidden = !kind || !tab.model; // an image has no source to go back to
1664 previewBtn.textContent = on ? 'Source' : 'Preview';1700 previewBtn.textContent = on ? 'Source' : 'Preview';
1665 previewBtn.classList.toggle('on', on);1701 previewBtn.classList.toggle('on', on);
1666 editorEl.hidden = !tab || on;1702 editorEl.hidden = !tab || on;
@@ -1670,14 +1706,21 @@ function showPane(tab) {
1670 renderPreview(tab);1706 renderPreview(tab);
1671 } else {1707 } else {
1672 // Nothing of a closed preview stays in the page.1708 // Nothing of a closed preview stays in the page.
1673- previewEl.replaceChildren();1709+ clearPreview();
1674 previewFrame.removeAttribute('srcdoc');1710 previewFrame.removeAttribute('srcdoc');
1675 }1711 }
1676 }1712 }
1677 1713
1714+// Empties the preview, the draw.io viewer included (it holds listeners).
1715+function clearPreview() {
1716+ drawioViewer?.graph?.destroy();
1717+ drawioViewer = null;
1718+ previewEl.replaceChildren();
1719+}
1720+
1678 function togglePreview() {1721 function togglePreview() {
1679 const tab = tabs.get(activePath);1722 const tab = tabs.get(activePath);
1680- if (!tab || !previewKind(tab.path)) return;1723+ if (!tab?.model || !previewKind(tab.path)) return;
1681 tab.preview = !tab.preview;1724 tab.preview = !tab.preview;
1682 showPane(tab);1725 showPane(tab);
1683 if (!tab.preview) editor.focus();1726 if (!tab.preview) editor.focus();
@@ -1693,11 +1736,23 @@ function schedulePreview(tab) {
1693 1736
1694 async function renderPreview(tab) {1737 async function renderPreview(tab) {
1695 const kind = previewKind(tab.path);1738 const kind = previewKind(tab.path);
1696- const text = tab.model.getValue();1739+ const text = tab.model ? tab.model.getValue() : '';
1697 if (kind === 'html') {1740 if (kind === 'html') {
1698 previewFrame.srcdoc = text;1741 previewFrame.srcdoc = text;
1699 return;1742 return;
1700 }1743 }
1744+ if (kind === 'image' || kind === 'svg') {
1745+ // An SVG shows what the editor holds, unsaved edits included; as an
1746+ // <img>, its scripts never run.
1747+ showImage(tab, kind === 'svg'
1748+ ? `data:image/svg+xml;charset=utf-8,${encodeURIComponent(text)}`
1749+ : `/api/raw?path=${q(tab.path)}&v=${tab.stamp || 0}`);
1750+ return;
1751+ }
1752+ if (kind === 'drawio') {
1753+ await renderDrawio(tab, text);
1754+ return;
1755+ }
1701 let html;1756 let html;
1702 try {1757 try {
1703 if (kind === 'markdown') {1758 if (kind === 'markdown') {
@@ -1713,6 +1768,7 @@ async function renderPreview(tab) {
1713 // The user may have switched tab, or back to the source, meanwhile.1768 // The user may have switched tab, or back to the source, meanwhile.
1714 if (html === null || tab.path !== activePath || !tab.preview) return;1769 if (html === null || tab.path !== activePath || !tab.preview) return;
1715 const scroll = previewEl.scrollTop;1770 const scroll = previewEl.scrollTop;
1771+ clearPreview();
1716 previewEl.innerHTML = DOMPurify.sanitize(html, PREVIEW_PURIFY);1772 previewEl.innerHTML = DOMPurify.sanitize(html, PREVIEW_PURIFY);
1717 const dir = parentOf(tab.path);1773 const dir = parentOf(tab.path);
1718 // Images next to the document come from /api/raw (images only, inside1774 // Images next to the document come from /api/raw (images only, inside
@@ -1733,6 +1789,104 @@ async function renderPreview(tab) {
1733 previewEl.scrollTop = scroll;1789 previewEl.scrollTop = scroll;
1734 }1790 }
1735 1791
1792+// showImage puts one picture in the preview, and its size in the status bar.
1793+function showImage(tab, src) {
1794+ const img = document.createElement('img');
1795+ img.className = 'preview-image';
1796+ img.alt = baseName(tab.path);
1797+ img.onload = () => { if (tab.path === activePath) setEditorMsg(`${img.naturalWidth} × ${img.naturalHeight}`); };
1798+ img.onerror = () => {
1799+ if (tab.path !== activePath) return;
1800+ clearPreview();
1801+ previewEl.textContent = `Cannot show ${tab.path}: not a valid image`;
1802+ };
1803+ clearPreview();
1804+ previewEl.append(img);
1805+ img.src = src;
1806+}
1807+
1808+// --- draw.io
1809+//
1810+// The official viewer (viewer-static.min.js, 4 MB) renders a .drawio file as
1811+// draw.io itself does: pages, layers, zoom. Loaded on the first diagram. It
1812+// runs in the page, which is acceptable because of the CSP: it sanitises the
1813+// HTML of labels itself, and whatever got through could not run a script
1814+// (no inline scripts, no handlers, no eval here) nor load anything from
1815+// elsewhere. Its resources would come from viewer.diagrams.net: the globals
1816+// below point them into the binary, MathJax at an empty stand-in (a formula
1817+// shows as its source), the rest at paths that do not exist — shape
1818+// libraries fetched on demand (some vendor stencils) are therefore missing.
1819+
1820+const DRAWIO = 'vendor/drawio';
1821+let drawioLoad = null;
1822+let drawioViewer = null;
1823+
1824+function loadDrawio() {
1825+ if (drawioLoad) return drawioLoad;
1826+ Object.assign(window, {
1827+ DRAW_MATH_URL: `${DRAWIO}/nomath`,
1828+ STENCIL_PATH: `${DRAWIO}/stencils`,
1829+ GRAPH_IMAGE_PATH: `${DRAWIO}/img`,
1830+ mxBasePath: `${DRAWIO}/mxgraph`,
1831+ RESOURCES_PATH: `${DRAWIO}/resources`,
1832+ mxLoadResources: false,
1833+ mxLoadStylesheets: false
1834+ });
1835+ drawioLoad = new Promise((resolve, reject) => {
1836+ // Monaco's AMD loader is there by now: with a global `define`, the
1837+ // libraries bundled in the viewer register as AMD modules ("Can only
1838+ // have one anonymous define call per script file") instead of setting
1839+ // their globals. Hidden while the viewer's script runs.
1840+ const amd = window.define;
1841+ window.define = undefined;
1842+ const done = () => { window.define = amd; };
1843+ const script = document.createElement('script');
1844+ script.src = `${DRAWIO}/viewer-static.min.js`;
1845+ script.onload = () => {
1846+ done();
1847+ if (window.GraphViewer) resolve();
1848+ else reject(new Error('the draw.io viewer did not start'));
1849+ };
1850+ script.onerror = () => {
1851+ done();
1852+ reject(new Error('cannot load the draw.io viewer'));
1853+ };
1854+ document.head.append(script);
1855+ });
1856+ drawioLoad.catch(() => { drawioLoad = null; });
1857+ return drawioLoad;
1858+}
1859+
1860+async function renderDrawio(tab, text) {
1861+ try {
1862+ await loadDrawio();
1863+ } catch (e) {
1864+ previewEl.textContent = `Cannot render ${tab.path}: ${e.message}`;
1865+ return;
1866+ }
1867+ if (tab.path !== activePath || !tab.preview) return;
1868+ const doc = mxUtils.parseXml(text);
1869+ const root = doc.documentElement;
1870+ if (!root || root.nodeName === 'parsererror' || doc.getElementsByTagName('parsererror').length) {
1871+ clearPreview();
1872+ previewEl.textContent = `Cannot render ${tab.path}: not valid draw.io XML`;
1873+ return;
1874+ }
1875+ clearPreview();
1876+ const box = document.createElement('div');
1877+ box.className = 'preview-diagram';
1878+ previewEl.append(box);
1879+ try {
1880+ drawioViewer = new GraphViewer(box, root, {
1881+ highlight: '#0969da', nav: true, lightbox: false, resize: true, center: true,
1882+ toolbar: 'zoom layers pages', 'toolbar-nohide': true, edit: null
1883+ });
1884+ } catch (e) {
1885+ clearPreview();
1886+ previewEl.textContent = `Cannot render ${tab.path}: ${e.message}`;
1887+ }
1888+}
1889+
1736 // An anchor scrolls the preview (DOMPurify prefixed the ids); a relative1890 // An anchor scrolls the preview (DOMPurify prefixed the ids); a relative
1737 // link opens the file, in preview when it has one.1891 // link opens the file, in preview when it has one.
1738 previewEl.addEventListener('click', async (e) => {1892 previewEl.addEventListener('click', async (e) => {
@@ -1778,6 +1932,13 @@ function scheduleFilesSync() {
1778 async function syncFiles() {1932 async function syncFiles() {
1779 refreshTree();1933 refreshTree();
1780 for (const tab of [...tabs.values()]) {1934 for (const tab of [...tabs.values()]) {
1935+ if (!tab.model) {
1936+ // An image: shown again from the disk (no-cache: an unchanged
1937+ // file costs a 304).
1938+ tab.stamp = Date.now();
1939+ if (tab.path === activePath) renderPreview(tab);
1940+ continue;
1941+ }
1781 if (isDirty(tab)) continue;1942 if (isDirty(tab)) continue;
1782 try {1943 try {
1783 const data = await api(`/api/file?path=${q(tab.path)}`);1944 const data = await api(`/api/file?path=${q(tab.path)}`);
modified web/vendor/VERSIONS +1 -0
@@ -7,3 +7,4 @@ marked 9.1.2
77 @highlightjs/cdn-assets 11.8.0
88 dompurify 3.4.16
99 @asciidoctor/core 3.0.4
10+jgraph/drawio 31.5.3 (viewer-static.min.js, from GitHub)
@@ -7,3 +7,4 @@ marked 9.1.2
7 @highlightjs/cdn-assets 11.8.07 @highlightjs/cdn-assets 11.8.0
8 dompurify 3.4.168 dompurify 3.4.16
9 @asciidoctor/core 3.0.49 @asciidoctor/core 3.0.4
10+jgraph/drawio 31.5.3 (viewer-static.min.js, from GitHub)
added web/vendor/drawio/LICENSE +201 -0
new file mode 100644
@@ -0,0 +1,201 @@
1+ Apache License
2+ Version 2.0, January 2004
3+ http://www.apache.org/licenses/
4+
5+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6+
7+ 1. Definitions.
8+
9+ "License" shall mean the terms and conditions for use, reproduction,
10+ and distribution as defined by Sections 1 through 9 of this document.
11+
12+ "Licensor" shall mean the copyright owner or entity authorized by
13+ the copyright owner that is granting the License.
14+
15+ "Legal Entity" shall mean the union of the acting entity and all
16+ other entities that control, are controlled by, or are under common
17+ control with that entity. For the purposes of this definition,
18+ "control" means (i) the power, direct or indirect, to cause the
19+ direction or management of such entity, whether by contract or
20+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21+ outstanding shares, or (iii) beneficial ownership of such entity.
22+
23+ "You" (or "Your") shall mean an individual or Legal Entity
24+ exercising permissions granted by this License.
25+
26+ "Source" form shall mean the preferred form for making modifications,
27+ including but not limited to software source code, documentation
28+ source, and configuration files.
29+
30+ "Object" form shall mean any form resulting from mechanical
31+ transformation or translation of a Source form, including but
32+ not limited to compiled object code, generated documentation,
33+ and conversions to other media types.
34+
35+ "Work" shall mean the work of authorship, whether in Source or
36+ Object form, made available under the License, as indicated by a
37+ copyright notice that is included in or attached to the work
38+ (an example is provided in the Appendix below).
39+
40+ "Derivative Works" shall mean any work, whether in Source or Object
41+ form, that is based on (or derived from) the Work and for which the
42+ editorial revisions, annotations, elaborations, or other modifications
43+ represent, as a whole, an original work of authorship. For the purposes
44+ of this License, Derivative Works shall not include works that remain
45+ separable from, or merely link (or bind by name) to the interfaces of,
46+ the Work and Derivative Works thereof.
47+
48+ "Contribution" shall mean any work of authorship, including
49+ the original version of the Work and any modifications or additions
50+ to that Work or Derivative Works thereof, that is intentionally
51+ submitted to Licensor for inclusion in the Work by the copyright owner
52+ or by an individual or Legal Entity authorized to submit on behalf of
53+ the copyright owner. For the purposes of this definition, "submitted"
54+ means any form of electronic, verbal, or written communication sent
55+ to the Licensor or its representatives, including but not limited to
56+ communication on electronic mailing lists, source code control systems,
57+ and issue tracking systems that are managed by, or on behalf of, the
58+ Licensor for the purpose of discussing and improving the Work, but
59+ excluding communication that is conspicuously marked or otherwise
60+ designated in writing by the copyright owner as "Not a Contribution."
61+
62+ "Contributor" shall mean Licensor and any individual or Legal Entity
63+ on behalf of whom a Contribution has been received by Licensor and
64+ subsequently incorporated within the Work.
65+
66+ 2. Grant of Copyright License. Subject to the terms and conditions of
67+ this License, each Contributor hereby grants to You a perpetual,
68+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69+ copyright license to reproduce, prepare Derivative Works of,
70+ publicly display, publicly perform, sublicense, and distribute the
71+ Work and such Derivative Works in Source or Object form.
72+
73+ 3. Grant of Patent License. Subject to the terms and conditions of
74+ this License, each Contributor hereby grants to You a perpetual,
75+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76+ (except as stated in this section) patent license to make, have made,
77+ use, offer to sell, sell, import, and otherwise transfer the Work,
78+ where such license applies only to those patent claims licensable
79+ by such Contributor that are necessarily infringed by their
80+ Contribution(s) alone or by combination of their Contribution(s)
81+ with the Work to which such Contribution(s) was submitted. If You
82+ institute patent litigation against any entity (including a
83+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84+ or a Contribution incorporated within the Work constitutes direct
85+ or contributory patent infringement, then any patent licenses
86+ granted to You under this License for that Work shall terminate
87+ as of the date such litigation is filed.
88+
89+ 4. Redistribution. You may reproduce and distribute copies of the
90+ Work or Derivative Works thereof in any medium, with or without
91+ modifications, and in Source or Object form, provided that You
92+ meet the following conditions:
93+
94+ (a) You must give any other recipients of the Work or
95+ Derivative Works a copy of this License; and
96+
97+ (b) You must cause any modified files to carry prominent notices
98+ stating that You changed the files; and
99+
100+ (c) You must retain, in the Source form of any Derivative Works
101+ that You distribute, all copyright, patent, trademark, and
102+ attribution notices from the Source form of the Work,
103+ excluding those notices that do not pertain to any part of
104+ the Derivative Works; and
105+
106+ (d) If the Work includes a "NOTICE" text file as part of its
107+ distribution, then any Derivative Works that You distribute must
108+ include a readable copy of the attribution notices contained
109+ within such NOTICE file, excluding those notices that do not
110+ pertain to any part of the Derivative Works, in at least one
111+ of the following places: within a NOTICE text file distributed
112+ as part of the Derivative Works; within the Source form or
113+ documentation, if provided along with the Derivative Works; or,
114+ within a display generated by the Derivative Works, if and
115+ wherever such third-party notices normally appear. The contents
116+ of the NOTICE file are for informational purposes only and
117+ do not modify the License. You may add Your own attribution
118+ notices within Derivative Works that You distribute, alongside
119+ or as an addendum to the NOTICE text from the Work, provided
120+ that such additional attribution notices cannot be construed
121+ as modifying the License.
122+
123+ You may add Your own copyright statement to Your modifications and
124+ may provide additional or different license terms and conditions
125+ for use, reproduction, or distribution of Your modifications, or
126+ for any such Derivative Works as a whole, provided Your use,
127+ reproduction, and distribution of the Work otherwise complies with
128+ the conditions stated in this License.
129+
130+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131+ any Contribution intentionally submitted for inclusion in the Work
132+ by You to the Licensor shall be under the terms and conditions of
133+ this License, without any additional terms or conditions.
134+ Notwithstanding the above, nothing herein shall supersede or modify
135+ the terms of any separate license agreement you may have executed
136+ with Licensor regarding such Contributions.
137+
138+ 6. Trademarks. This License does not grant permission to use the trade
139+ names, trademarks, service marks, or product names of the Licensor,
140+ except as required for reasonable and customary use in describing the
141+ origin of the Work and reproducing the content of the NOTICE file.
142+
143+ 7. Disclaimer of Warranty. Unless required by applicable law or
144+ agreed to in writing, Licensor provides the Work (and each
145+ Contributor provides its Contributions) on an "AS IS" BASIS,
146+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147+ implied, including, without limitation, any warranties or conditions
148+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149+ PARTICULAR PURPOSE. You are solely responsible for determining the
150+ appropriateness of using or redistributing the Work and assume any
151+ risks associated with Your exercise of permissions under this License.
152+
153+ 8. Limitation of Liability. In no event and under no legal theory,
154+ whether in tort (including negligence), contract, or otherwise,
155+ unless required by applicable law (such as deliberate and grossly
156+ negligent acts) or agreed to in writing, shall any Contributor be
157+ liable to You for damages, including any direct, indirect, special,
158+ incidental, or consequential damages of any character arising as a
159+ result of this License or out of the use or inability to use the
160+ Work (including but not limited to damages for loss of goodwill,
161+ work stoppage, computer failure or malfunction, or any and all
162+ other commercial damages or losses), even if such Contributor
163+ has been advised of the possibility of such damages.
164+
165+ 9. Accepting Warranty or Additional Liability. While redistributing
166+ the Work or Derivative Works thereof, You may choose to offer,
167+ and charge a fee for, acceptance of support, warranty, indemnity,
168+ or other liability obligations and/or rights consistent with this
169+ License. However, in accepting such obligations, You may act only
170+ on Your own behalf and on Your sole responsibility, not on behalf
171+ of any other Contributor, and only if You agree to indemnify,
172+ defend, and hold each Contributor harmless for any liability
173+ incurred by, or claims asserted against, such Contributor by reason
174+ of your accepting any such warranty or additional liability.
175+
176+ END OF TERMS AND CONDITIONS
177+
178+ APPENDIX: How to apply the Apache License to your work.
179+
180+ To apply the Apache License to your work, attach the following
181+ boilerplate notice, with the fields enclosed by brackets "[]"
182+ replaced with your own identifying information. (Don't include
183+ the brackets!) The text should be enclosed in the appropriate
184+ comment syntax for the file format. We also recommend that a
185+ file or class name and description of purpose be included on the
186+ same "printed page" as the copyright notice for easier
187+ identification within third-party archives.
188+
189+ Copyright [yyyy] [name of copyright owner]
190+
191+ Licensed under the Apache License, Version 2.0 (the "License");
192+ you may not use this file except in compliance with the License.
193+ You may obtain a copy of the License at
194+
195+ http://www.apache.org/licenses/LICENSE-2.0
196+
197+ Unless required by applicable law or agreed to in writing, software
198+ distributed under the License is distributed on an "AS IS" BASIS,
199+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200+ See the License for the specific language governing permissions and
201+ limitations under the License.
\ No newline at end of file
new file mode 100644
@@ -0,0 +1,201 @@
1+ Apache License
2+ Version 2.0, January 2004
3+ http://www.apache.org/licenses/
4+
5+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6+
7+ 1. Definitions.
8+
9+ "License" shall mean the terms and conditions for use, reproduction,
10+ and distribution as defined by Sections 1 through 9 of this document.
11+
12+ "Licensor" shall mean the copyright owner or entity authorized by
13+ the copyright owner that is granting the License.
14+
15+ "Legal Entity" shall mean the union of the acting entity and all
16+ other entities that control, are controlled by, or are under common
17+ control with that entity. For the purposes of this definition,
18+ "control" means (i) the power, direct or indirect, to cause the
19+ direction or management of such entity, whether by contract or
20+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21+ outstanding shares, or (iii) beneficial ownership of such entity.
22+
23+ "You" (or "Your") shall mean an individual or Legal Entity
24+ exercising permissions granted by this License.
25+
26+ "Source" form shall mean the preferred form for making modifications,
27+ including but not limited to software source code, documentation
28+ source, and configuration files.
29+
30+ "Object" form shall mean any form resulting from mechanical
31+ transformation or translation of a Source form, including but
32+ not limited to compiled object code, generated documentation,
33+ and conversions to other media types.
34+
35+ "Work" shall mean the work of authorship, whether in Source or
36+ Object form, made available under the License, as indicated by a
37+ copyright notice that is included in or attached to the work
38+ (an example is provided in the Appendix below).
39+
40+ "Derivative Works" shall mean any work, whether in Source or Object
41+ form, that is based on (or derived from) the Work and for which the
42+ editorial revisions, annotations, elaborations, or other modifications
43+ represent, as a whole, an original work of authorship. For the purposes
44+ of this License, Derivative Works shall not include works that remain
45+ separable from, or merely link (or bind by name) to the interfaces of,
46+ the Work and Derivative Works thereof.
47+
48+ "Contribution" shall mean any work of authorship, including
49+ the original version of the Work and any modifications or additions
50+ to that Work or Derivative Works thereof, that is intentionally
51+ submitted to Licensor for inclusion in the Work by the copyright owner
52+ or by an individual or Legal Entity authorized to submit on behalf of
53+ the copyright owner. For the purposes of this definition, "submitted"
54+ means any form of electronic, verbal, or written communication sent
55+ to the Licensor or its representatives, including but not limited to
56+ communication on electronic mailing lists, source code control systems,
57+ and issue tracking systems that are managed by, or on behalf of, the
58+ Licensor for the purpose of discussing and improving the Work, but
59+ excluding communication that is conspicuously marked or otherwise
60+ designated in writing by the copyright owner as "Not a Contribution."
61+
62+ "Contributor" shall mean Licensor and any individual or Legal Entity
63+ on behalf of whom a Contribution has been received by Licensor and
64+ subsequently incorporated within the Work.
65+
66+ 2. Grant of Copyright License. Subject to the terms and conditions of
67+ this License, each Contributor hereby grants to You a perpetual,
68+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69+ copyright license to reproduce, prepare Derivative Works of,
70+ publicly display, publicly perform, sublicense, and distribute the
71+ Work and such Derivative Works in Source or Object form.
72+
73+ 3. Grant of Patent License. Subject to the terms and conditions of
74+ this License, each Contributor hereby grants to You a perpetual,
75+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76+ (except as stated in this section) patent license to make, have made,
77+ use, offer to sell, sell, import, and otherwise transfer the Work,
78+ where such license applies only to those patent claims licensable
79+ by such Contributor that are necessarily infringed by their
80+ Contribution(s) alone or by combination of their Contribution(s)
81+ with the Work to which such Contribution(s) was submitted. If You
82+ institute patent litigation against any entity (including a
83+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84+ or a Contribution incorporated within the Work constitutes direct
85+ or contributory patent infringement, then any patent licenses
86+ granted to You under this License for that Work shall terminate
87+ as of the date such litigation is filed.
88+
89+ 4. Redistribution. You may reproduce and distribute copies of the
90+ Work or Derivative Works thereof in any medium, with or without
91+ modifications, and in Source or Object form, provided that You
92+ meet the following conditions:
93+
94+ (a) You must give any other recipients of the Work or
95+ Derivative Works a copy of this License; and
96+
97+ (b) You must cause any modified files to carry prominent notices
98+ stating that You changed the files; and
99+
100+ (c) You must retain, in the Source form of any Derivative Works
101+ that You distribute, all copyright, patent, trademark, and
102+ attribution notices from the Source form of the Work,
103+ excluding those notices that do not pertain to any part of
104+ the Derivative Works; and
105+
106+ (d) If the Work includes a "NOTICE" text file as part of its
107+ distribution, then any Derivative Works that You distribute must
108+ include a readable copy of the attribution notices contained
109+ within such NOTICE file, excluding those notices that do not
110+ pertain to any part of the Derivative Works, in at least one
111+ of the following places: within a NOTICE text file distributed
112+ as part of the Derivative Works; within the Source form or
113+ documentation, if provided along with the Derivative Works; or,
114+ within a display generated by the Derivative Works, if and
115+ wherever such third-party notices normally appear. The contents
116+ of the NOTICE file are for informational purposes only and
117+ do not modify the License. You may add Your own attribution
118+ notices within Derivative Works that You distribute, alongside
119+ or as an addendum to the NOTICE text from the Work, provided
120+ that such additional attribution notices cannot be construed
121+ as modifying the License.
122+
123+ You may add Your own copyright statement to Your modifications and
124+ may provide additional or different license terms and conditions
125+ for use, reproduction, or distribution of Your modifications, or
126+ for any such Derivative Works as a whole, provided Your use,
127+ reproduction, and distribution of the Work otherwise complies with
128+ the conditions stated in this License.
129+
130+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131+ any Contribution intentionally submitted for inclusion in the Work
132+ by You to the Licensor shall be under the terms and conditions of
133+ this License, without any additional terms or conditions.
134+ Notwithstanding the above, nothing herein shall supersede or modify
135+ the terms of any separate license agreement you may have executed
136+ with Licensor regarding such Contributions.
137+
138+ 6. Trademarks. This License does not grant permission to use the trade
139+ names, trademarks, service marks, or product names of the Licensor,
140+ except as required for reasonable and customary use in describing the
141+ origin of the Work and reproducing the content of the NOTICE file.
142+
143+ 7. Disclaimer of Warranty. Unless required by applicable law or
144+ agreed to in writing, Licensor provides the Work (and each
145+ Contributor provides its Contributions) on an "AS IS" BASIS,
146+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147+ implied, including, without limitation, any warranties or conditions
148+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149+ PARTICULAR PURPOSE. You are solely responsible for determining the
150+ appropriateness of using or redistributing the Work and assume any
151+ risks associated with Your exercise of permissions under this License.
152+
153+ 8. Limitation of Liability. In no event and under no legal theory,
154+ whether in tort (including negligence), contract, or otherwise,
155+ unless required by applicable law (such as deliberate and grossly
156+ negligent acts) or agreed to in writing, shall any Contributor be
157+ liable to You for damages, including any direct, indirect, special,
158+ incidental, or consequential damages of any character arising as a
159+ result of this License or out of the use or inability to use the
160+ Work (including but not limited to damages for loss of goodwill,
161+ work stoppage, computer failure or malfunction, or any and all
162+ other commercial damages or losses), even if such Contributor
163+ has been advised of the possibility of such damages.
164+
165+ 9. Accepting Warranty or Additional Liability. While redistributing
166+ the Work or Derivative Works thereof, You may choose to offer,
167+ and charge a fee for, acceptance of support, warranty, indemnity,
168+ or other liability obligations and/or rights consistent with this
169+ License. However, in accepting such obligations, You may act only
170+ on Your own behalf and on Your sole responsibility, not on behalf
171+ of any other Contributor, and only if You agree to indemnify,
172+ defend, and hold each Contributor harmless for any liability
173+ incurred by, or claims asserted against, such Contributor by reason
174+ of your accepting any such warranty or additional liability.
175+
176+ END OF TERMS AND CONDITIONS
177+
178+ APPENDIX: How to apply the Apache License to your work.
179+
180+ To apply the Apache License to your work, attach the following
181+ boilerplate notice, with the fields enclosed by brackets "[]"
182+ replaced with your own identifying information. (Don't include
183+ the brackets!) The text should be enclosed in the appropriate
184+ comment syntax for the file format. We also recommend that a
185+ file or class name and description of purpose be included on the
186+ same "printed page" as the copyright notice for easier
187+ identification within third-party archives.
188+
189+ Copyright [yyyy] [name of copyright owner]
190+
191+ Licensed under the Apache License, Version 2.0 (the "License");
192+ you may not use this file except in compliance with the License.
193+ You may obtain a copy of the License at
194+
195+ http://www.apache.org/licenses/LICENSE-2.0
196+
197+ Unless required by applicable law or agreed to in writing, software
198+ distributed under the License is distributed on an "AS IS" BASIS,
199+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200+ See the License for the specific language governing permissions and
201+ limitations under the License.
\ No newline at end of file\ No newline at end of file
added web/vendor/drawio/nomath/startup.js +1 -0
new file mode 100644
@@ -0,0 +1 @@
1+// MathJax is not embedded (see vendor.sh): a formula in a diagram shows as its source.
new file mode 100644
@@ -0,0 +1 @@
1+// MathJax is not embedded (see vendor.sh): a formula in a diagram shows as its source.
added web/vendor/drawio/viewer-static.min.js +8766 -0
Diff too large to display here (+8766 −0) — view the file at the compare branch.