🛟 Updated.
51fba89 parent: f7a5060The 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 '=')" | ||
| 68 | 68 | |
| 69 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 99 | ### Examples |
| 97 | 100 | |
| @@ -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 | ### Examples | 99 | ### 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 differ | Binary files /dev/null and b/demo/20260713-header.png differ | ||
modified
demo/README.md +12 -0 | @@ -1,5 +1,8 @@ | ||
| 1 | 1 | # README |
| 2 | 2 | |
| 3 | +this is an image | |
| 4 | + | |
| 5 | + | |
| 3 | 6 | Dossier de travail. |
| 4 | 7 | |
| 5 | 8 | ## Fichiers |
| @@ -13,3 +16,12 @@ Dossier de travail. | ||
| 13 | 16 | ```bash |
| 14 | 17 | ./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 | ||
| @@ -1,5 +1,8 @@ | |||
| 1 | # README | 1 | # README |
| 2 | 2 | ||
| 3 | +this is an image | ||
| 4 | + | ||
| 5 | + | ||
| 3 | Dossier de travail. | 6 | Dossier de travail. |
| 4 | 7 | ||
| 5 | ## Fichiers | 8 | ## Fichiers |
| @@ -13,3 +16,12 @@ Dossier de travail. | |||
| 13 | ```bash | 16 | ```bash |
| 14 | ./start.sh | 17 | ./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
(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
(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<br><br>docker-agent&nbsp; ── event JSON ─▶ stdin<br>hook ◀─ decision JSON ── stdout<br><br>{} (empty) = "let it pass"<br>otherwise: allow / deny · block · system_message ·<br>additional_context · updated_* …<br><br>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
(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
(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<br><br>docker-agent&nbsp; ── event JSON ─▶ stdin<br>hook ◀─ decision JSON ── stdout<br><br>{} (empty) = "let it pass"<br>otherwise: allow / deny · block · system_message ·<br>additional_context · updated_* …<br><br>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. | ||
| 28 | 28 | before it enters the page |
| 29 | 29 | web/vendor/asciidoctor/ AsciiDoc -> HTML, for the editor's preview; loaded |
| 30 | 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 | 34 | COMMENT |
| 32 | 35 | |
| 33 | 36 | set -euo pipefail |
| @@ -43,6 +46,7 @@ MARKED_VERSION="9.1.2" # later majors: check renderMarkdown in app.js | ||
| 43 | 46 | HIGHLIGHT_VERSION="11.8.0" |
| 44 | 47 | DOMPURIFY_VERSION="3.4.16" # a security filter: keep it current |
| 45 | 48 | 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 | 51 | cd "$(dirname "${BASH_SOURCE[0]}")" |
| 48 | 52 | VENDOR="web/vendor" |
| @@ -71,6 +75,10 @@ marked ${MARKED_VERSION} latest | ||
| 71 | 75 | dompurify ${DOMPURIFY_VERSION} latest |
| 72 | 76 | @asciidoctor/core ${ASCIIDOCTOR_VERSION} latest |
| 73 | 77 | 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 | 82 | echo "" |
| 75 | 83 | echo "The comments next to the versions in $0 say which updates need more than a version bump." |
| 76 | 84 | exit 0 |
| @@ -104,8 +112,15 @@ fetch @asciidoctor/core "${ASCIIDOCTOR_VERSION}" asciidoctor | ||
| 104 | 112 | echo "📥 cdn.tailwindcss.com/${TAILWIND_VERSION}" |
| 105 | 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 | 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 | 125 | cp -R "${TMP}/monaco/min/vs" "${VENDOR}/monaco/vs" |
| 111 | 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 | 151 | # no eval under the page's CSP (checked in Chromium). |
| 137 | 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 | 162 | cat >"${VENDOR}/VERSIONS" <<EOF |
| 140 | 163 | monaco-editor ${MONACO_VERSION} |
| 141 | 164 | @xterm/xterm ${XTERM_VERSION} |
| @@ -146,6 +169,7 @@ marked ${MARKED_VERSION} | ||
| 146 | 169 | @highlightjs/cdn-assets ${HIGHLIGHT_VERSION} |
| 147 | 170 | dompurify ${DOMPURIFY_VERSION} |
| 148 | 171 | @asciidoctor/core ${ASCIIDOCTOR_VERSION} |
| 172 | +jgraph/drawio ${DRAWIO_VERSION} (viewer-static.min.js, from GitHub) | |
| 149 | 173 | EOF |
| 150 | 174 | |
| 151 | 175 | 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 page | 28 | before it enters the page |
| 29 | web/vendor/asciidoctor/ AsciiDoc -> HTML, for the editor's preview; loaded | 29 | 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 | COMMENT | 34 | COMMENT |
| 32 | 35 | ||
| 33 | set -euo pipefail | 36 | 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 current | 47 | 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.js | 48 | 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} latest | 75 | dompurify ${DOMPURIFY_VERSION} latest |
| 72 | @asciidoctor/core ${ASCIIDOCTOR_VERSION} latest | 76 | @asciidoctor/core ${ASCIIDOCTOR_VERSION} latest |
| 73 | EOF | 77 | 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 0 | 84 | 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" <<EOF | 162 | 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 | EOF | 173 | 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; } | ||
| 138 | 138 | .preview { flex: 1; min-height: 0; overflow: auto; padding: 1rem 1.5rem; font-size: 0.9rem; line-height: 1.6; } |
| 139 | 139 | .preview-frame { flex: 1; min-height: 0; width: 100%; border: 0; background: #fff; } |
| 140 | 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 | 145 | .preview h1 { font-size: 1.6rem; border-bottom: 1px solid var(--border); padding-bottom: 0.3rem; } |
| 142 | 146 | .preview h2 { font-size: 1.3rem; border-bottom: 1px solid var(--border); padding-bottom: 0.2rem; } |
| 143 | 147 | .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'); | ||
| 932 | 932 | const expanded = new Set(); // directories open in the tree |
| 933 | 933 | const dirViews = new Map(); // directory path -> { container, depth } as rendered |
| 934 | 934 | 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 | 936 | let activePath = null; |
| 937 | 937 | let editor = null; |
| 938 | 938 | let monacoReady = null; |
| @@ -1273,7 +1273,7 @@ function movePaths(from, to) { | ||
| 1273 | 1273 | tab.el.title = tab.path; |
| 1274 | 1274 | tab.el.querySelector('.tab-name').textContent = baseName(tab.path); |
| 1275 | 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 | 1278 | const entries = [...tabs.values()].map(t => [t.path, t]); |
| 1279 | 1279 | tabs.clear(); |
| @@ -1435,7 +1435,7 @@ function setEditorMsg(text, cls = '') { | ||
| 1435 | 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 | 1440 | function paintTab(tab) { |
| 1441 | 1441 | tab.el.classList.toggle('dirty', isDirty(tab)); |
| @@ -1445,6 +1445,7 @@ function paintTab(tab) { | ||
| 1445 | 1445 | |
| 1446 | 1446 | async function openFile(path) { |
| 1447 | 1447 | if (tabs.has(path)) return activate(path); |
| 1448 | + if (previewKind(path) === 'image') return openImage(path); | |
| 1448 | 1449 | let data; |
| 1449 | 1450 | try { |
| 1450 | 1451 | [data] = await Promise.all([api(`/api/file?path=${q(path)}`), loadMonaco(), loadIconTheme()]); |
| @@ -1454,7 +1455,32 @@ async function openFile(path) { | ||
| 1454 | 1455 | } |
| 1455 | 1456 | if (tabs.has(path)) return activate(path); // opened twice meanwhile |
| 1456 | 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 | 1484 | const el = document.createElement('div'); |
| 1459 | 1485 | el.className = 'tab'; |
| 1460 | 1486 | el.title = path; |
| @@ -1465,14 +1491,14 @@ async function openFile(path) { | ||
| 1465 | 1491 | close.className = 'tab-close'; |
| 1466 | 1492 | close.title = 'Close'; |
| 1467 | 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 }; | |
| 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); }; | |
| 1473 | 1499 | tabs.set(path, tab); |
| 1474 | 1500 | tabsEl.append(el); |
| 1475 | - activate(path); | |
| 1501 | + return tab; | |
| 1476 | 1502 | } |
| 1477 | 1503 | |
| 1478 | 1504 | function activate(path) { |
| @@ -1481,10 +1507,10 @@ function activate(path) { | ||
| 1481 | 1507 | showEditor(true); |
| 1482 | 1508 | ensureEditor(); |
| 1483 | 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 | 1511 | activePath = path; |
| 1486 | 1512 | editor.setModel(tab.model); |
| 1487 | - if (tab.view) editor.restoreViewState(tab.view); | |
| 1513 | + if (tab.model && tab.view) editor.restoreViewState(tab.view); | |
| 1488 | 1514 | editorPathEl.textContent = path; |
| 1489 | 1515 | editorPathEl.title = path; |
| 1490 | 1516 | setEditorMsg(''); |
| @@ -1521,12 +1547,12 @@ function closeTab(path, force = false) { | ||
| 1521 | 1547 | markActiveRow(); |
| 1522 | 1548 | } |
| 1523 | 1549 | } |
| 1524 | - tab.model.dispose(); | |
| 1550 | + tab.model?.dispose(); | |
| 1525 | 1551 | } |
| 1526 | 1552 | |
| 1527 | 1553 | async function saveActive(force = false) { |
| 1528 | 1554 | const tab = tabs.get(activePath); |
| 1529 | - if (!tab) return; | |
| 1555 | + if (!tab?.model) return; | |
| 1530 | 1556 | // What is sent is what gets marked as saved, even if the user |
| 1531 | 1557 | // types on while the request is in flight. |
| 1532 | 1558 | const version = tab.model.getAlternativeVersionId(); |
| @@ -1601,15 +1627,25 @@ const PREVIEW_PURIFY = { ...PURIFY, SANITIZE_NAMED_PROPS: true }; | ||
| 1601 | 1627 | // an include shows as a link. |
| 1602 | 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 | 1635 | function previewKind(path) { |
| 1606 | 1636 | const ext = baseName(path).toLowerCase().split('.').pop(); |
| 1607 | 1637 | if (['md', 'markdown', 'mdown', 'mkd'].includes(ext)) return 'markdown'; |
| 1608 | 1638 | if (['adoc', 'asciidoc', 'asc'].includes(ext)) return 'asciidoc'; |
| 1609 | 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 | 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 | 1649 | // Asciidoctor.js (750 KB) is loaded on the first AsciiDoc preview. Its |
| 1614 | 1650 | // browser build is a classic script that defines one global. |
| 1615 | 1651 | function loadAsciidoctor() { |
| @@ -1659,8 +1695,8 @@ function resolveIn(dir, href) { | ||
| 1659 | 1695 | // the button that switches: only for a file that has a preview. |
| 1660 | 1696 | function showPane(tab) { |
| 1661 | 1697 | 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 | |
| 1664 | 1700 | previewBtn.textContent = on ? 'Source' : 'Preview'; |
| 1665 | 1701 | previewBtn.classList.toggle('on', on); |
| 1666 | 1702 | editorEl.hidden = !tab || on; |
| @@ -1670,14 +1706,21 @@ function showPane(tab) { | ||
| 1670 | 1706 | renderPreview(tab); |
| 1671 | 1707 | } else { |
| 1672 | 1708 | // Nothing of a closed preview stays in the page. |
| 1673 | - previewEl.replaceChildren(); | |
| 1709 | + clearPreview(); | |
| 1674 | 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 | 1721 | function togglePreview() { |
| 1679 | 1722 | const tab = tabs.get(activePath); |
| 1680 | - if (!tab || !previewKind(tab.path)) return; | |
| 1723 | + if (!tab?.model || !previewKind(tab.path)) return; | |
| 1681 | 1724 | tab.preview = !tab.preview; |
| 1682 | 1725 | showPane(tab); |
| 1683 | 1726 | if (!tab.preview) editor.focus(); |
| @@ -1693,11 +1736,23 @@ function schedulePreview(tab) { | ||
| 1693 | 1736 | |
| 1694 | 1737 | async function renderPreview(tab) { |
| 1695 | 1738 | const kind = previewKind(tab.path); |
| 1696 | - const text = tab.model.getValue(); | |
| 1739 | + const text = tab.model ? tab.model.getValue() : ''; | |
| 1697 | 1740 | if (kind === 'html') { |
| 1698 | 1741 | previewFrame.srcdoc = text; |
| 1699 | 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 | 1756 | let html; |
| 1702 | 1757 | try { |
| 1703 | 1758 | if (kind === 'markdown') { |
| @@ -1713,6 +1768,7 @@ async function renderPreview(tab) { | ||
| 1713 | 1768 | // The user may have switched tab, or back to the source, meanwhile. |
| 1714 | 1769 | if (html === null || tab.path !== activePath || !tab.preview) return; |
| 1715 | 1770 | const scroll = previewEl.scrollTop; |
| 1771 | + clearPreview(); | |
| 1716 | 1772 | previewEl.innerHTML = DOMPurify.sanitize(html, PREVIEW_PURIFY); |
| 1717 | 1773 | const dir = parentOf(tab.path); |
| 1718 | 1774 | // Images next to the document come from /api/raw (images only, inside |
| @@ -1733,6 +1789,104 @@ async function renderPreview(tab) { | ||
| 1733 | 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 | 1890 | // An anchor scrolls the preview (DOMPurify prefixed the ids); a relative |
| 1737 | 1891 | // link opens the file, in preview when it has one. |
| 1738 | 1892 | previewEl.addEventListener('click', async (e) => { |
| @@ -1778,6 +1932,13 @@ function scheduleFilesSync() { | ||
| 1778 | 1932 | async function syncFiles() { |
| 1779 | 1933 | refreshTree(); |
| 1780 | 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 | 1942 | if (isDirty(tab)) continue; |
| 1782 | 1943 | try { |
| 1783 | 1944 | 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 tree | 932 | const expanded = new Set(); // directories open in the tree |
| 933 | const dirViews = new Map(); // directory path -> { container, depth } as rendered | 933 | const dirViews = new Map(); // directory path -> { container, depth } as rendered |
| 934 | let selectedPath = null; // last row clicked in the tree | 934 | 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 meanwhile | 1456 | 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 user | 1556 | // 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. Its | 1649 | // 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, inside | 1774 | // 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 relative | 1890 | // 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 | ||
| 7 | 7 | @highlightjs/cdn-assets 11.8.0 |
| 8 | 8 | dompurify 3.4.16 |
| 9 | 9 | @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.0 | 7 | @highlightjs/cdn-assets 11.8.0 |
| 8 | dompurify 3.4.16 | 8 | dompurify 3.4.16 |
| 9 | @asciidoctor/core 3.0.4 | 9 | @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.