A self-contained copyparty browser plugin that upgrades copyparty's Markdown viewing with Mermaid and PlantUML diagrams, math (KaTeX), rich code highlighting, a table-of-contents, in-document search, zoomable diagrams, light/dark theming, and client-side export.
It is built by vendoring (copying) the rendering pipeline from the
Markdown-Preview-Unified
(MPU) project and bundling it into a single browser artifact that copyparty loads
into its Markdown viewer page via --js-other. There is no extra service to run
at runtime — the only optional external piece is a self-hostable PlantUML/Kroki server,
needed only for PlantUML/Graphviz diagrams.
Copyparty (Python) --js-other--> markdown-plus.js (one bundled artifact)
|
integration/ detector, coordinator, sanitizer, cache, config, loader
renderer/ markdown-it pipeline (mirrors MPU engine.ts)
diagrams/ DiagramManager + Mermaid / PlantUML / Kroki adapters
features/ ToC, search, zoom, export, theme bridge
vendor/mpu/ copied portable MPU source (PlantUML encoder, themes, constants)
Loads on the viewer page, not the file browser. copyparty injects
--js-otherinto "all other pages", which includes the Markdown viewer (shown when you open a.mdwith?v).--js-browseronly covers the directory/file-browser page and is left free for other plugins — it hosts the companion Video.js plugin.
Already installed? If you only changed the plugin source and want the running server to pick it up, jump straight to Updating the plugin — the short version is
npm run build, and it is the same in both deployment modes.
- Mermaid diagrams rendered in-browser (flowchart, sequence, class, state, gantt, ...)
- PlantUML / Graphviz via a self-hostable PlantUML or Kroki server
- Math with KaTeX (
$inline$and$$block$$) - Syntax highlighting (highlight.js)
- GitHub-style Markdown: tables, task lists, footnotes, emoji, anchored headings
- Admonitions:
::: note | tip | info | warning | caution | danger - Table of contents sidebar, in-document search, click-to-open diagram/image viewer (plain wheel / two-finger scroll to pan; Ctrl+wheel or trackpad pinch to zoom [Chrome/Firefox]; drag to pan; double-click/double-tap to toggle zoom)
- Zoom the whole document in/out from the toolbar (
−/+, scales text, images and diagrams together; click the percentage to reset; persisted) - Light / dark theme that recolors diagrams on toggle
- Toggle between a fixed reading column and full screen width (toolbar
↔, persisted) - Copy-code buttons
- Export: standalone HTML + browser Print-to-PDF
- Floating toolbar with crisp SVG icons that render everywhere (incl. Android) and enlarge to comfortable tap targets on touch devices
- HTML/SVG sanitization (DOMPurify); works fully offline when assets are self-hosted
The plugin is just a file that copyparty is told to inject. How copyparty is started is what differs between machines, and it decides where the plugin flags live:
| Mode A — systemd service | Mode B — local / foreground launcher | |
|---|---|---|
| How copyparty runs | copyparty.service, in the background, starts at boot |
start_copyparty.sh, in your terminal |
| Where the plugin flags live | the unit's ExecStart (/etc/systemd/system/copyparty.service) |
the launcher script's argument list |
| Set up by | install_copyparty_service.sh (Gist) |
nothing — just run the script |
| Start / stop | sudo systemctl start|stop copyparty |
run it / Ctrl+C |
| Logs | journalctl -u copyparty -f |
stdout in your terminal |
| To ship a source change | npm run build |
npm run build |
The update procedure is the same in both modes — see
Updating the plugin. Only the rare cases that change
copyparty's command line (plugin flags, MDPLUS_CONFIG, args.conf) differ, and
those are spelled out per mode.
systemctl is-active copyparty.service # active | inactive
systemctl cat copyparty.service 2>/dev/null | grep -c js-other # 1 = unit carries the pluginactiveand1→ Mode A: the service delivers the plugin.- otherwise → Mode B: only
start_copyparty.shdelivers the plugin. (A unit may still exist but bedisabled/plugin-less; running both would fight over port4321.)
Do not run the service and the launcher at once — they both bind port
4321(Address already in use).
npm run build runs build.mjs, which invokes esbuild to bundle the
plugin from source into a single browser-loadable file.
git clone https://github.com/techcaotri/copyparty-markdown-viewer.git
cd copyparty-markdown-viewer
npm install # installs the libraries that get bundled in (markdown-it, KaTeX, ...)
npm run build # -> dist/markdown-plus.jsBuild pipeline — inputs, processing, and the single output:
| Stage | Detail |
|---|---|
| Entry point | src/integration/index.js — composition root + IIFE bootstrap |
| Source inputs | everything under src/: integration/, renderer/, diagrams/, features/, vendor/mpu/, plus src/integration/styles.css imported as a text string |
| Bundled into the JS | markdown-it + its plugins (anchor, container, emoji, footnote, task-lists, texmath), KaTeX (rendering JS), highlight.js, DOMPurify, pako — everything in package.json dependencies |
| Not bundled (lazy-loaded at runtime) | Mermaid (JS) and KaTeX CSS + fonts — fetched from assetBaseUrl (CDN by default; self-host with npm run build:assets, see Offline / air-gapped) |
| Bundler config | esbuild: format: iife, platform: browser, target: es2020, minified, process.env.NODE_ENV=production, no sourcemap |
| CSS handling | the .css: 'text' esbuild loader turns styles.css into a JS string; the entry injects it at runtime as one <style id="mdplus-styles"> tag — so there is no separate .css file and no --css-browser flag |
| Output | dist/markdown-plus.js — one self-contained, minified IIFE (~1 MB) with the CSS inlined |
| Git | dist/ is git-ignored (see .gitignore); it is a generated artifact, so rebuild it after every git pull |
dist/markdown-plus.js is the only thing copyparty needs at runtime. There is no
dist/markdown-plus.css — the CSS lives inside the JS.
The build artifact is a plain file on disk. copyparty does not "install" it anywhere — it only needs to serve it by URL so the browser can fetch it. The path from a built file to a live plugin:
dist/markdown-plus.js (file on disk, inside the repo)
│ the read-only /dev volume maps the host path to a URL:
│ /home/tripham/Dev -> /dev
▼
/dev/Playground_Copyparty/copyparty-markdown-viewer/dist/markdown-plus.js (URL)
│ --js-other tells copyparty to inject <script src="..."> for it
▼
<script src="/dev/.../markdown-plus.js"> (on the Markdown viewer page, opened with ?v)
│ the browser fetches it same-origin and the IIFE runs
▼
injects <style>, observes the DOM, renders markdown into #mw
copyparty's Markdown viewer page (shown when you open a .md, i.e. ?v) is injected
via --js-other — not --js-browser (which only covers the file-browser page). The
plugin is a single self-contained JS file and injects its own CSS, so no --css-browser
is needed. The bundle must be reachable by URL; the easiest way is to serve it from
one of your copyparty volumes.
# Expose your dev tree read-only so the browser can fetch the bundle same-origin.
# Here /home/tripham/Dev is shared as /dev, so the built bundle is reachable at
# /dev/Playground_Copyparty/copyparty-markdown-viewer/dist/markdown-plus.js
copyparty -c /etc/copyparty/args.conf \
-v /home/tripham/Dev:/dev:r,tripham,readuser \
--js-other /dev/Playground_Copyparty/copyparty-markdown-viewer/dist/markdown-plus.js \
--html-head '<script>window.MDPLUS_CONFIG={diagramBackend:"mermaid+puml"}</script>'Two properties of this design are why updates need no server restart:
- copyparty re-reads
markdown-plus.jsfrom disk on every request — it does not hold the file in memory. Overwriting the file (rebuilding) is enough for the running server to start handing out the new bytes. --js-otherfixes only the URL, not the bytes. It tells copyparty "put a<script src="/dev/.../markdown-plus.js">on the viewer page"; whatever bytes live at that URL when the browser requests it is what runs. Changing the URL (the flag) needs a restart; changing the file at that URL does not. (The browser still caches the old JS, so a hard-refresh is the last step — see Updating the plugin.)
Open a .md file in the copyparty web UI — it renders with diagrams, math, a ToC,
search, and export controls (a floating toolbar appears top-right). The plugin reads
the raw markdown from copyparty's #mt textarea, renders into #mw, and hides
copyparty's native output/ToC.
Verified against copyparty v1.20.2; also deployed on v1.19.17. If a future version changes the viewer DOM, set
viewerSelector(see Configuration); the detector also falls back to a MutationObserver + URL heuristics.
start_copyparty.sh is a foreground launcher that starts copyparty
with /etc/copyparty/args.conf plus both local plugins wired in. On a Mode B
machine this is the deployment mechanism — there is no plugin-carrying systemd service.
On a Mode A machine you normally do not use it at all (the service is already running
with the same flags); see Deploy as a systemd service.
start_copyparty.sh # Video.js + Markdown viewer (default)
start_copyparty.sh --no-videojs # Markdown viewer only
start_copyparty.sh --no-markdown # Video.js only
start_copyparty.sh --no-ftp # don't start the FTP server
start_copyparty.sh -h # helpStop it with Ctrl+C (it runs in the foreground).
| Flag | Default | Effect |
|---|---|---|
--videojs / --no-videojs |
on | load the Video.js player on the file-browser page (--js-browser) |
--markdown / --no-markdown |
on | load this Markdown viewer on the viewer page (--js-other) |
--ftp / --no-ftp |
on | enable copyparty's FTP server (port 3921) |
Env overrides: ENABLE_VIDEOJS, ENABLE_MARKDOWN, ENABLE_FTP, DIAGRAM_BACKEND,
DIAGRAM_BACKEND_URL, COPYPARTY, CONF.
The repo holds the canonical copy; ~/bin/start_copyparty.sh is a symlink to it,
and ~/bin is on $PATH — so start_copyparty.sh works from any directory:
ls -l ~/bin/start_copyparty.sh
# ~/bin/start_copyparty.sh -> /home/tripham/Dev/Playground_Copyparty/copyparty-markdown-viewer/start_copyparty.shThe script resolves symlinks (readlink -f) before locating the plugin repos, so editing
the file in the repo immediately changes what start_copyparty.sh does — there is nothing
to copy or re-install. If the symlink is ever missing, recreate it with:
ln -sfn /home/tripham/Dev/Playground_Copyparty/copyparty-markdown-viewer/start_copyparty.sh \
~/bin/start_copyparty.sh-
Auto-builds the bundle if
dist/markdown-plus.jsis missing (but not if it is merely stale — see Updating the plugin). -
Locates both plugin repos relative to the resolved script path and converts them to
/dev/...URLs. -
Adds the read-only
/devvolume on the command line only ifargs.confdoes not already define it (this machine'sargs.confdoes, so it is reused). -
FTP degrades gracefully:
--ftpneeds thepyftpdlibmodule inside copyparty's Python. The launcher probes for it and, if absent, starts without FTP and prints how to enable it, instead of letting copyparty die withModuleNotFoundError. To enable FTP:sudo /opt/copyparty/venv/bin/pip install pyftpdlib
Common to both deployments:
| Thing | Value |
|---|---|
| Repo | /home/tripham/Dev/Playground_Copyparty/copyparty-markdown-viewer |
| Config | /etc/copyparty/args.conf (-p 4321, -i 0.0.0.0) |
| Web UI | http://localhost:4321/ |
/dev volume |
-v /home/tripham/Dev:/dev:r,tripham,readuser (in args.conf) |
| Bundle URL | /dev/Playground_Copyparty/copyparty-markdown-viewer/dist/markdown-plus.js |
| Video.js URL | /dev/Playground_Copyparty/copyparty-video-plugin/videojs-enhanced.js |
Where they differ:
| Mode A (systemd service) | Mode B (launcher) | |
|---|---|---|
| copyparty binary | /home/tripham/.local/bin/copyparty (v1.19.17) |
/opt/copyparty/venv/bin/copyparty (v1.20.2) |
| Started by | copyparty.service (enabled + active) |
start_copyparty.sh |
The launcher auto-detects the binary (it prefers ~/.local/bin/copyparty, then the
/opt venv, then $PATH), so the same script works on both.
The port is
4321, not copyparty's default3923— it is set by-p 4321inargs.conf.
The one thing to remember: dist/markdown-plus.js is a build artifact and is
git-ignored. Changing the source does nothing until you rebuild it. copyparty serves
that file straight from disk on every request, so a rebuild is all it takes for the
running server to hand out the new code.
This works out the same whether copyparty runs as a systemd service (Mode A) or from the launcher (Mode B) — see Deployment modes. Neither mode rebuilds anything for you, and neither needs a restart for a source-only change.
Mode A gotcha: the service does not build the bundle. If
dist/markdown-plus.jsis missing,copyparty.servicestill starts happily — the injected<script src="/dev/.../markdown-plus.js">just 404s and the plugin silently does nothing. (The launcher auto-builds a missing bundle; the service never does.)
npm run build overwrites a single file — dist/markdown-plus.js — with a fresh bundle
built from the current src/. Nothing else is produced or moved:
- Input: your edited
src/(entrysrc/integration/index.js- the rest of the tree +
styles.css) plus the installednode_modules/libraries. See Build the artifact for the full pipeline.
- the rest of the tree +
- Output:
dist/markdown-plus.js(minified IIFE, CSS inlined). The old bytes are replaced in place; there is no separate.cssorassets/directory to update. (Runnpm run build:assetsonly if you self-host Mermaid/KaTeX and those assets changed.) - Effect on the running server: none, directly. copyparty serves that URL from disk per request, so the next page load fetches the new bytes. The browser, however, caches the old JS — hence the hard-refresh step below.
Both deployment modes point --js-other at this same on-disk file, so the update step is
identical — only the wiring differs:
| Mode A — systemd | Mode B — launcher | |
|---|---|---|
Who points --js-other at the bundle |
the unit's ExecStart |
start_copyparty.sh's command line |
| Who builds the bundle if missing | nobody (the <script> 404s) — run npm run build |
the launcher auto-builds it |
After npm run build |
hard-refresh the browser | hard-refresh the browser |
cd /home/tripham/Dev/Playground_Copyparty/copyparty-markdown-viewer
git pull # if the source came from the remote
npm install # only needed when dependencies changed
npm run build # -> regenerates dist/markdown-plus.js <-- the step that mattersThen hard-refresh the markdown page in your browser (Ctrl+Shift+R). That is the
whole procedure in both modes — no sudo, no systemctl, no restart.
1. Get the new source.
cd /home/tripham/Dev/Playground_Copyparty/copyparty-markdown-viewer
git pull # or: git merge origin/main2. Install dependencies if package.json changed. Harmless to run every time.
npm install3. Rebuild the bundle. This is the step that makes the update effective.
npm run build # -> dist/markdown-plus.js (CSS inlined — the only artifact)Confirm the artifact is newer than every source file — if this prints any path, the build did not take:
find src build.mjs -newer dist/markdown-plus.js4. (Recommended) Run the tests.
npm test5. Restart copyparty — almost never needed.
You do not need to restart copyparty for a source-only change, in either mode:
--js-other merely injects a <script src="/dev/.../markdown-plus.js"> tag, and
copyparty re-reads that file from disk on every request. Rebuilding is enough.
A restart is only needed when you change something baked into copyparty's command line at startup:
| Changed | Restart needed? |
|---|---|
Anything under src/ (then rebuilt) |
No — just hard-refresh the browser |
dist/markdown-plus.js (rebuilt) |
No |
Plugin flags, MDPLUS_CONFIG, DIAGRAM_BACKEND(_URL) |
Yes |
Enabling/disabling a plugin (--no-markdown, --no-videojs, --ftp) |
Yes |
/etc/copyparty/args.conf — port |
Yes |
/etc/copyparty/args.conf — accounts/volumes only |
Mode A: reload (no downtime) · Mode B: Yes |
Mode A (systemd service). The flags live in the unit's ExecStart, so changing them
means changing the unit:
sudo systemctl daemon-reload # only after editing the .service file
sudo systemctl restart copyparty
systemctl status copyparty --no-pager # confirm it came back up
journalctl -u copyparty -n 50 --no-pagerAccounts/volumes in args.conf are hot-reloadable — the unit ships
ExecReload=/bin/kill -s USR1 $MAINPID, so copyparty re-reads them without dropping
connections:
sudo systemctl reload copyparty # accounts + volumes only, no downtimeTo change which plugins the service loads, prefer re-running the installer over hand-editing the unit — it re-renders and backs up the file, then reloads + restarts for you:
./install_copyparty_service.sh --dry-run # preview the new unit, change nothing
./install_copyparty_service.sh --no-videojs # e.g. drop Video.js and applyMode B (launcher). Ctrl+C, then run start_copyparty.sh again with whatever flags
you want. There is nothing to install — the script is symlinked onto your PATH.
6. Force the browser to drop the old bundle. This is the most common reason an update "doesn't show up" — the browser is still running the cached JS.
- Hard-refresh:
Ctrl+Shift+R(orCmd+Shift+R). - Still stale? Open DevTools → Network → tick Disable cache, then reload.
7. Verify the server is really handing out the new bundle.
# byte-compare what copyparty serves against what you just built
curl -s 'http://localhost:4321/dev/Playground_Copyparty/copyparty-markdown-viewer/dist/markdown-plus.js?pw=trituyen' \
-o /tmp/served.js
cmp /tmp/served.js dist/markdown-plus.js && echo "OK: serving the new build"A 200 with a matching byte count means the plugin is live. Then open any .md in the
web UI and check the console for errors.
| Situation | Mode | What to do |
|---|---|---|
| Pulled new plugin source | both | npm install && npm run build, then hard-refresh |
Edited src/ locally |
both | npm run build (or leave npm run dev watching), then hard-refresh |
| Update "didn't apply" | both | you almost certainly skipped npm run build, or the browser cached the old JS |
| Bundle missing entirely | A | npm run build — the service will not build it for you (the <script> just 404s) |
| Bundle missing entirely | B | just run start_copyparty.sh — it auto-builds |
Changed plugin flags / MDPLUS_CONFIG / diagram backend |
A | re-run install_copyparty_service.sh (or edit the unit, then daemon-reload + restart) |
Changed plugin flags / MDPLUS_CONFIG / diagram backend |
B | restart it, e.g. DIAGRAM_BACKEND_URL=... start_copyparty.sh |
Changed accounts/volumes in args.conf |
A | sudo systemctl reload copyparty (hot-reload, no downtime) |
Changed accounts/volumes in args.conf |
B | Ctrl+C, then restart the launcher |
Edited start_copyparty.sh |
B | nothing to install (it is symlinked) — just restart it |
| Server won't come back up | A | journalctl -u copyparty -n 50 --no-pager |
This is Mode A — copyparty runs in the background, starts at boot, and the plugin
flags live in the unit's ExecStart. Check whether a given machine is on it with
Which mode is this machine on?.
Either way, updating the plugin is still just
npm run build— the unit points atdist/markdown-plus.json disk, so there is no service to restart for a source change. See Updating the plugin. Restart/daemon-reloadonly when the unit itself changes.
On a Mode B machine the unit may exist but be
disabled/inactivewith a plugin-lessExecStart(e.g.ExecStart=/opt/copyparty/venv/bin/copyparty -c /etc/copyparty/args.conf); there the launcher delivers the plugin instead. Never run both — they would fight over port4321.
For a persistent install, install_copyparty_service.sh writes
/etc/systemd/system/copyparty.service wiring up both local copyparty
enhancements, each independently toggleable, and (optionally) adds the read-only
/dev volume to /etc/copyparty/args.conf so the browser can load both plugins
same-origin:
| Plugin / option | Page | copyparty flag | Disable with | Default |
|---|---|---|---|---|
| Video.js enhanced player | file browser | --js-browser |
--no-videojs |
on |
| Markdown viewer (this repo) | viewer (?v) |
--js-other + --html-head |
--no-markdown |
on |
/dev read-only volume (-v /home/tripham/Dev:/dev:r,tripham,readuser) |
— | args.conf edit |
--no-dev-volume |
on |
| FTP server | — | --ftp 3921 |
--no-ftp |
on |
Because the two plugins live on different pages (Video.js on --js-browser, the
Markdown viewer on --js-other), they coexist without contending for a single
option.
The script is published as a secret Gist (it contains machine-specific paths, so it is not part of this repo):
➡️ install_copyparty_service.sh (Gist)
Run it as your normal user (so the nvm-based npm build works) — it calls sudo
only for the privileged steps and backs up every file it touches. Download it next to
this repo (it locates both plugin repos relative to its own path):
cd /home/tripham/Dev/Playground_Copyparty/copyparty-markdown-viewer
curl -fsSL https://gist.githubusercontent.com/techcaotri/6420fbd61783df624b61d7221eb8c4d0/raw/install_copyparty_service.sh \
-o install_copyparty_service.sh
chmod +x install_copyparty_service.sh
./install_copyparty_service.sh --dry-run # preview the plan + rendered unit, change nothing
./install_copyparty_service.sh # apply (prompts before writing; --yes to skip)Examples:
./install_copyparty_service.sh --no-videojs # Markdown viewer + /dev volume only
./install_copyparty_service.sh --no-markdown # Video.js only
./install_copyparty_service.sh --no-dev-volume # leave args.conf untouched
./install_copyparty_service.sh --no-ftp --no-restartWhat it does, in order: builds the Markdown bundle if missing → appends the /dev
volume to args.conf (idempotent, backed up) → renders and installs the unit (backed
up) → systemd-analyze verify → daemon-reload + enable + restart. The resulting
ExecStart (all features on) is:
/home/tripham/.local/bin/copyparty -c /etc/copyparty/args.conf --ftp 3921 \
--mime ".ts=video/mp2t" \
--js-browser /dev/Playground_Copyparty/copyparty-video-plugin/videojs-enhanced.js \
--js-other /dev/Playground_Copyparty/copyparty-markdown-viewer/dist/markdown-plus.js \
--html-head '<script>window.MDPLUS_CONFIG={diagramBackend:"mermaid+puml"}</script>'
systemctl status copyparty --no-pager # up? and which ExecStart is actually live
systemctl cat copyparty # the full unit as systemd sees it
journalctl -u copyparty -f # follow the logs (Mode A has no terminal output)
sudo systemctl daemon-reload # after editing the .service file
sudo systemctl restart copyparty # apply a changed command line
sudo systemctl reload copyparty # hot-reload accounts/volumes only (SIGUSR1)
sudo systemctl disable --now copyparty # stop + don't start at boot (e.g. to switch to Mode B)The installer backs up every file it replaces, so a rollback is always available:
ls /etc/systemd/system/copyparty.service.bak-* /etc/copyparty/args.conf.bak-*Set window.MDPLUS_CONFIG before the bundle loads, using copyparty's --html-head:
copyparty -c /etc/copyparty/args.conf \
--js-other /dev/.../dist/markdown-plus.js \
--html-head '<script>window.MDPLUS_CONFIG={diagramBackend:"kroki",diagramBackendUrl:"/kroki"}</script>'| Key | Default | Meaning |
|---|---|---|
diagramBackend |
"mermaid+puml" |
"mermaid+puml" or "kroki" |
diagramBackendUrl |
null |
PlantUML or Kroki base URL (required for PlantUML) |
diagramFormat |
"svg" |
"svg" or "png" for server-rendered diagrams |
assetBaseUrl |
"https://cdn.jsdelivr.net/npm" |
Where Mermaid + KaTeX CSS are fetched from |
mermaidUrl |
null |
Full override for the Mermaid script URL |
katexCssUrl |
null |
Full override for the KaTeX CSS URL |
mathRenderer |
"KaTeX" |
"KaTeX" or "none" |
mermaidSecurityLevel |
"strict" |
Mermaid security level |
features |
all true |
{ toc, search, zoom, contentZoom, export, copyCode } |
theme |
"auto" |
"auto", "light", or "dark" |
viewerSelector |
null |
Explicit CSS selector for copyparty's md container |
autoInit |
true |
Observe the DOM and render automatically |
The launcher and installer build MDPLUS_CONFIG from the DIAGRAM_BACKEND and
DIAGRAM_BACKEND_URL environment variables.
Mermaid and math are fully self-contained (in-browser). PlantUML/Graphviz need a server because their rendering is not pure JavaScript. Run one yourself — never point at a public host for private content.
PlantUML server:
docker run -d -p 8080:8080 plantuml/plantuml-server:jetty
# MDPLUS_CONFIG = { diagramBackend:"mermaid+puml", diagramBackendUrl:"http://localhost:8080" }Kroki (unified: PlantUML, Graphviz, BPMN, ...):
docker run -d -p 8000:8000 yuzutech/kroki
# MDPLUS_CONFIG = { diagramBackend:"kroki", diagramBackendUrl:"http://localhost:8000" }Tip: reverse-proxy the diagram server under copyparty's origin (e.g. /kroki) to
keep everything same-origin.
By default Mermaid and KaTeX's CSS/fonts load from a CDN. To self-host them:
npm run build:assets # copies KaTeX css/fonts (+ Mermaid if installed) into dist/assets/Serve dist/ from copyparty and set:
window.MDPLUS_CONFIG = {
assetBaseUrl: '/.mdplus/dist/assets',
mermaidUrl: '/.mdplus/dist/assets/mermaid/mermaid.min.js',
katexCssUrl: '/.mdplus/dist/assets/katex/katex.min.css',
diagramBackendUrl: '/kroki' // self-hosted diagram server
};With a self-hosted diagram server and assets, the plugin makes no external requests.
npm run dev # esbuild watch
npm run serve:demo # http://localhost:8099/demo/ (loads dist/, renders a sample)
npm test # unit (render + encoder) + jsdom integration test
npm run test:e2e # headless-Chrome test against a running copyparty (see below)The e2e test (test/e2e.mjs, uses playwright-core + system Chrome) drives a real
copyparty markdown viewer and checks Mermaid, math, highlighting, the toolbar/ToC,
and graceful PlantUML fallback. Point it at a server with env vars: BASE, CPPW
(password), MDPATH (viewer file path).
Project layout:
src/integration/ detector, coordinator, sanitizer, cache, config, library-loader, styles.css, index.js
src/renderer/ markdown-renderer.js (markdown-it pipeline)
src/diagrams/ index.js (DiagramManager) + mermaid/plantuml/kroki adapters
src/features/ index.js (FeatureUI) + toc/search/zoom/export/theme-bridge
src/vendor/mpu/ copied MPU source (do not edit; re-run the vendor script)
scripts/ vendor-from-mpu.sh, copy-assets.mjs, serve-demo.mjs
test/ smoke.mjs, plantuml.mjs, integration.mjs (jsdom), e2e.mjs (real copyparty)
examples/ sample.md (feature showcase)
start_copyparty.sh foreground launcher: args.conf + Video.js + Markdown viewer (toggleable)
canonical copy; ~/bin/start_copyparty.sh is a symlink to it
build.mjs esbuild bundler
dist/ BUILD ARTIFACT, git-ignored — rebuild with `npm run build` after any
source change, or the running copyparty keeps serving the old bundle
The systemd installer (install_copyparty_service.sh) is kept in a separate Gist because
it carries machine-specific paths; it is what sets up a Mode A machine — see
Deploy as a systemd service.
The rendering core is copied from MPU, not depended on at runtime. To refresh it against a newer MPU checkout:
MPU_REPO=/path/to/markdown-preview-unified MPU_PIN=<commit-sha> npm run vendor
npm run build && npm testOnly portable, low-dependency modules are vendored (PlantUML encoder, Catppuccin
diagram themes/skinparams, shared constants); the ToC/search/zoom modules are
re-implemented under src/features/ following MPU's design. See
docs/IMPLEMENTATION.md.
- All rendered Markdown HTML and SVG is sanitized with DOMPurify before injection.
- Mermaid runs with
securityLevel: "strict"by default. - The PlantUML/Kroki host must be explicitly configured — there is no public default, which avoids leaking private documents and SSRF surprises.
- The
/devvolume is mounted read-only for logged-in accounts only (r,tripham,readuser) — no anonymous access and no write, so exposing the dev tree to host the bundle cannot be used to modify it.
- My source change didn't take effect: you almost certainly did not rebuild.
dist/markdown-plus.jsis git-ignored and is not regenerated automatically when it already exists. Runnpm run build, then hard-refresh (Ctrl+Shift+R). Check withfind src -newer dist/markdown-plus.js— any output means the bundle is stale. See Updating the plugin. - Rebuilt, but the browser still runs the old code: the browser cached the bundle.
Hard-refresh (
Ctrl+Shift+R), or DevTools → Network → Disable cache → reload. copyparty itself does not cache it — it re-reads the file from disk per request, so no server restart is needed (in either deployment mode). - Mode A: the page loads but the plugin does nothing, and the bundle 404s. The service
does not build the bundle — if
dist/markdown-plus.jsis absent, the unit still starts and only the injected<script>fails. Runnpm run build. (Only the launcher auto-builds.) - Mode A: the service won't start after a change. Read the logs — the unit's quoting is
the usual culprit:
journalctl -u copyparty -n 50 --no-pager, and check the live command line withsystemctl cat copyparty. Re-runninginstall_copyparty_service.shre-renders a known good unit (andsystemd-analyze verifys it); the*.bak-*backups let you roll back. - Changed
args.confand nothing happened (Mode A): accounts/volumes need asudo systemctl reload copyparty(SIGUSR1 hot-reload); a port change needs a fullsudo systemctl restart copyparty. ModuleNotFoundError: No module named 'pyftpdlib': copyparty's--ftpneeds that module in its Python. Install it into the venv copyparty actually runs under —sudo /opt/copyparty/venv/bin/pip install pyftpdlib— or start with--no-ftp. (A plainpip install/pip install --userinstalls into a different site-packages that the venv will not see.) Current launchers detect this and start without FTP rather than crashing.- Can't reach the web UI: the port is
4321(from-p 4321inargs.conf), not copyparty's default3923. Address already in use: something else already holds4321— most likely anotherstart_copyparty.shor thecopyparty.serviceunit. Check withss -ltnp | grep 4321.- Nothing renders: confirm the bundle is loaded — the
--js-otherURL must be correct and reachable (open it directly in the browser; you should get the JS, not a 404/login page). Check the console. SetviewerSelectorif your copyparty version uses a different Markdown container. - Bundle 404s / redirects to login: the
/devvolume is missing or the file is outside it. Add-v /home/tripham/Dev:/dev:r,tripham,readuser(the installer's/dev volumestep) and make sure you are logged in. - PlantUML shows the source + an error: set
diagramBackendUrlto a reachable PlantUML/Kroki server. - Math is unstyled: the KaTeX CSS failed to load; self-host it and set
katexCssUrl, or check CDN access. - Mermaid diagrams missing: the Mermaid script failed to load; self-host it and
set
mermaidUrl.
MIT © Tri Pham. See LICENSE.
This plugin vendors source from Markdown-Preview-Unified (MIT) and bundles markdown-it, KaTeX, highlight.js, Mermaid, DOMPurify, and pako (each under their respective permissive licenses).