Skip to content

Repository files navigation

Copyparty Markdown Viewer

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-other into "all other pages", which includes the Markdown viewer (shown when you open a .md with ?v). --js-browser only 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.

Features

  • 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

Deployment modes

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.

Which mode is this machine on?

systemctl is-active copyparty.service                    # active | inactive
systemctl cat copyparty.service 2>/dev/null | grep -c js-other   # 1 = unit carries the plugin
  • active and 1Mode A: the service delivers the plugin.
  • otherwise → Mode B: only start_copyparty.sh delivers the plugin. (A unit may still exist but be disabled/plugin-less; running both would fight over port 4321.)

Do not run the service and the launcher at once — they both bind port 4321 (Address already in use).

Install

1. Build the artifact

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.js

Build 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.

2. Load it into copyparty

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-othernot --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.js from 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-other fixes 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.

3. Run it with the launcher (Mode B)

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              # help

Stop 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.

It is on your PATH via a symlink

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.sh

The 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

What the launcher does for you

  • Auto-builds the bundle if dist/markdown-plus.js is 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 /dev volume on the command line only if args.conf does not already define it (this machine's args.conf does, so it is reused).

  • FTP degrades gracefully: --ftp needs the pyftpdlib module 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 with ModuleNotFoundError. To enable FTP:

    sudo /opt/copyparty/venv/bin/pip install pyftpdlib

Reference settings (both machines)

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 default 3923 — it is set by -p 4321 in args.conf.

Updating the plugin

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.js is missing, copyparty.service still 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.)

What rebuilding actually 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/ (entry src/integration/index.js
    • the rest of the tree + styles.css) plus the installed node_modules/ libraries. See Build the artifact for the full pipeline.
  • Output: dist/markdown-plus.js (minified IIFE, CSS inlined). The old bytes are replaced in place; there is no separate .css or assets/ directory to update. (Run npm run build:assets only 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

The short version

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 matters

Then 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.

Step by step

1. Get the new source.

cd /home/tripham/Dev/Playground_Copyparty/copyparty-markdown-viewer
git pull                      # or: git merge origin/main

2. Install dependencies if package.json changed. Harmless to run every time.

npm install

3. 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.js

4. (Recommended) Run the tests.

npm test

5. 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-pager

Accounts/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 downtime

To 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 apply

Mode 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 (or Cmd+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.

Quick reference

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 buildthe 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

Deploy as a systemd service

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 at dist/markdown-plus.js on disk, so there is no service to restart for a source change. See Updating the plugin. Restart/daemon-reload only when the unit itself changes.

On a Mode B machine the unit may exist but be disabled/inactive with a plugin-less ExecStart (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 port 4321.

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-restart

What 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 verifydaemon-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>'

Managing the service

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-*

Configuration

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.

Diagram backends

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.

Offline / air-gapped

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.

Develop

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.

Vendoring / sync with MPU

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 test

Only 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.

Security

  • 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 /dev volume 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.

Troubleshooting

  • My source change didn't take effect: you almost certainly did not rebuild. dist/markdown-plus.js is git-ignored and is not regenerated automatically when it already exists. Run npm run build, then hard-refresh (Ctrl+Shift+R). Check with find 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.js is absent, the unit still starts and only the injected <script> fails. Run npm 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 with systemctl cat copyparty. Re-running install_copyparty_service.sh re-renders a known good unit (and systemd-analyze verifys it); the *.bak-* backups let you roll back.
  • Changed args.conf and nothing happened (Mode A): accounts/volumes need a sudo systemctl reload copyparty (SIGUSR1 hot-reload); a port change needs a full sudo systemctl restart copyparty.
  • ModuleNotFoundError: No module named 'pyftpdlib': copyparty's --ftp needs 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 plain pip install / pip install --user installs 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 4321 in args.conf), not copyparty's default 3923.
  • Address already in use: something else already holds 4321 — most likely another start_copyparty.sh or the copyparty.service unit. Check with ss -ltnp | grep 4321.
  • Nothing renders: confirm the bundle is loaded — the --js-other URL must be correct and reachable (open it directly in the browser; you should get the JS, not a 404/login page). Check the console. Set viewerSelector if your copyparty version uses a different Markdown container.
  • Bundle 404s / redirects to login: the /dev volume is missing or the file is outside it. Add -v /home/tripham/Dev:/dev:r,tripham,readuser (the installer's /dev volume step) and make sure you are logged in.
  • PlantUML shows the source + an error: set diagramBackendUrl to 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.

License

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).

About

Self-contained copyparty browser plugin for rich Markdown: Mermaid, PlantUML, KaTeX math, ToC, search, zoom, and export. Built by vendoring the Markdown-Preview-Unified pipeline into a single --js-browser bundle.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages