Henri Burau ace966

@henriburau/lapdog (0.7.0)

Published 2026-10-04 11:50:36 +00:00 by ace966

Installation

@henriburau:registry=https://gitea.henriburau.de/api/packages/ace966/npm/
npm install @henriburau/lapdog@0.7.0
"@henriburau/lapdog": "0.7.0"

About this package

Markdown seminar labs, live in your browser or ready to print.

lapdog

Markdown labs for the browser and the printed page. A TypeScript CLI and Vue 3 frontend for running hands-on seminars from a directory of Markdown files.

  • Right-side navigation with frontmatter lab titles and nested task links.
  • Every level-one heading (#) becomes a task; each file can contain several tasks.
  • Live reload on file edits, additions, renames, removals, and local image changes.
  • One print-ready workbook containing every lab, with a linked table of contents upfront.
  • Headless PDF export, syntax highlighting, tables, relative images, and links between labs.
  • Code tabs for Bash/PowerShell or npm/pnpm variants, linked across the whole seminar.
  • Downloadable files for each task, such as starter code or data sets, one click away.
  • Responsive layout, keyboard navigation, and locally bundled fonts for offline use.
  • Optional password-protected admin page to show tasks one by one to every participant live.
  • English and German interface with a persistent language selector, powered by vue-i18n.

Quick start

Private npm package

Requires Node.js 22.12+. Configure the Gitea registry and authenticate with a personal access token with package read permission:

npm config set @henriburau:registry https://gitea.henriburau.de/api/packages/ace966/npm/
npm config set -- '//gitea.henriburau.de/api/packages/ace966/npm/:_authToken' "$GITEA_TOKEN"
npm install --global @henriburau/lapdog
lapdog serve ./my-labs --title "My seminar"

Open http://127.0.0.1:4173. The package includes the built frontend, fonts, CLI, and sample labs; no build step is needed after installation. The npm scope is @henriburau, while the Gitea package owner is ace966, matching the existing Gitea projects.

For CLI PDF export, install the Chromium version used by the installed package once:

node "$(npm root -g)/@henriburau/lapdog/node_modules/playwright/cli.js" install --with-deps chromium
lapdog pdf ./my-labs --title "My seminar" --output ./workbook.pdf

Docker

The image includes Node.js, the built app, fonts, Chromium, and its Linux dependencies. It runs as the unprivileged node user, serves /labs on 0.0.0.0:4173, and supports both AMD64 and ARM64.

docker login gitea.henriburau.de --username ace966
docker run --rm --init \
  -p 4173:4173 \
  --mount "type=bind,source=$(pwd)/my-labs,target=/labs,readonly" \
  gitea.henriburau.de/ace966/lapdog:latest

Use your package-read token as the registry password. Open http://localhost:4173. The mounted directory must exist and be readable by the container user. Edits to the host files are live-reloaded even with a read-only mount. Without a mount, the image serves the bundled sample seminar.

The default command uses the title Seminar labs. To set your own, append serve /labs --host 0.0.0.0 --title "My seminar" to the image name. Arguments replace the default Docker command, so include serve, the directory, and the bind address. On filesystems where Docker does not forward file events, add -e CHOKIDAR_USEPOLLING=true.

Export a PDF without installing anything on the host other than Docker:

mkdir -p output
docker run --rm --init \
  --user "$(id -u):$(id -g)" \
  --mount "type=bind,source=$(pwd)/my-labs,target=/labs,readonly" \
  --mount "type=bind,source=$(pwd)/output,target=/output" \
  gitea.henriburau.de/ace966/lapdog:latest \
  pdf /labs --title "My seminar" --output /output/workbook.pdf

The output directory is writable by your host UID/GID; the bundled browser is accessible to that user. Image tags are latest for stable releases, the exact package version (for example, 0.1.0), edge for main, and sha-<full-commit-sha> for each published build.

From source

Requires Node.js 22.12+ and pnpm 10.18.0 (pinned in package.json).

pnpm install
pnpm run build
pnpm start ./examples --title "JavaScript workshop"

Open http://127.0.0.1:4173. Replace ./examples with your Markdown directory (relative or absolute). Markdown live reload works in both built and development modes.

For frontend development, run pnpm run dev. This starts the example seminar with Vite hot module replacement as well as Markdown live reload. For a different directory:

pnpm exec tsx src/server/cli.ts serve ./my-labs --title "My seminar" --dev

To use the lapdog command locally after building:

pnpm link --global
lapdog serve ./my-labs --title "My seminar"
lapdog serve ./my-labs --title "My seminar" --host 0.0.0.0 --port 8080

Show tasks one by one

Start the server with an admin password to decide which tasks participants see:

lapdog serve ./my-labs --title "My seminar" --admin-password "choose-a-password"
# Or keep the password out of your shell history and process list:
LAPDOG_ADMIN_PASSWORD="choose-a-password" lapdog serve ./my-labs --title "My seminar"

With Docker, pass -e LAPDOG_ADMIN_PASSWORD=…. Open /admin (for example, http://127.0.0.1:4173/admin) and log in. Every task starts hidden. Switch tasks on one at a time, use Show next to show the next hidden task in seminar order, or use a lab's checkbox to show all of its tasks. Changes reach all connected browsers immediately.

Hidden tasks are removed on the server, so they're not in the participant page's source either, and their files can't be downloaded. Labs are hidden until at least one of their tasks is shown; labs without any tasks are always visible. The lab introduction before the first task is shown with the lab.

Which tasks are shown is kept in memory: restarting the server hides every task again. A task is identified by its file and heading, so renaming either hides it again. Without an admin password, every task is visible and /admin explains how to turn the page on. PDF export always contains every task.

Interface language

Choose English or Deutsch from the language selector in the header. On the first visit, lapdog uses the first supported language in your browser preferences (including regional variants such as de-DE), falling back to English. Your selection is saved in this browser for future visits.

Navigation, status messages, accessibility labels, task counts, and the printed table of contents follow the selected language. Browser printing uses the current selection; CLI export uses --language (English by default). Seminar titles, Markdown content, frontmatter values, and server diagnostic details are displayed as authored.

Translations live in src/client/locales/en.ts and src/client/locales/de.ts; shared vue-i18n setup and language preference handling live in src/client/i18n.ts.

Write a lab

---
title: Working with data
description: Explore arrays and transformations.
order: 2
duration: 30
---

An optional introduction to this lab.

# Build a collection

Your first task goes here.

## A helpful hint

Subheadings stay inside the task.

# Find what matters

Your second task goes here.

Frontmatter

Field Required Meaning
title Yes Non-empty string used in navigation, lab headings, and the table of contents
description No Short string shown below the lab title
order No Number controlling lab order; lower numbers come first
duration No Positive number of minutes, such as 30; shown as “30 minutes”

Files are discovered recursively, including .MD. Hidden paths and node_modules are skipped, and discovery does not follow symlinks. Labs with an order come first; ties and labs without an order use natural filename ordering (2 before 10). Every discovered Markdown file must have a title. Invalid files produce a useful error in the browser; the last valid workbook stays visible until you fix the file.

Markdown is parsed with markdown-it, frontmatter with gray-matter / js-yaml, and fenced code blocks with highlight.js. Raw HTML is escaped. Supported Markdown includes headings, lists, quotes, code, tables, links, and images. A fenced # comment is not a task. Standard Markdown level-one setext headings also count as tasks.

Code tabs

Show the same step for different shells or tools by wrapping fenced code blocks in a code-group container. The bracketed label names each tab. Without a label, the language is used instead.

Common shells, package managers, and languages get an icon next to their label, for example Bash, PowerShell, cmd, npm, pnpm, yarn, Docker, Node.js, Python, and TypeScript. The icon matches the label first (so "Git Bash" gets the shell icon), then the code block's language. Icons come from the bundled VS Code Icons set, so they work offline. Labels without a matching icon are shown as text only.

::: code-group

```bash [Bash]
export NAME="Ada"
```

```powershell [PowerShell]
$env:NAME = "Ada"
```

:::

Tabs with the same label are linked, ignoring case. When a participant picks PowerShell, every code group with a PowerShell tab switches too. Each group shows the most recent choice it offers, so Bash/PowerShell and npm/pnpm/yarn groups keep separate choices. The choice lasts for the browser session. Print and PDF output show every variant, each with its label.

A code group may contain only fenced code blocks, and labels must be unique within the group. Otherwise, you'll see an error with the file and line number.

Downloadable files

List the files a task needs in a files block anywhere in the task, for example right below its heading or at its end. Participants download a file by clicking it.

# Build a collection

::: files
files/books.js
data/reading-list.csv
:::

Write one path per line; list items (- files/books.js) work too. Paths are resolved like links: relative to the Markdown file, or to the lab directory with a leading /. Any file type works, and each file gets an icon for its type. A files block before the first task belongs to the lab introduction. To offer a whole folder, zip it first.

Every listed file must exist in the lab directory. Hidden files, node_modules, symlinks, and paths outside the directory can't be listed; you'll see an error with the file and line number instead. Only listed files are served for download. With an admin password, files listed in hidden tasks can't be downloaded until the task is shown. Print and PDF output list the file names.

Resolve paths relative to the Markdown file; leading / paths are relative to the lab directory:

![Diagram](./images/diagram.png)
[Next lab](./02-next-lab.md)
[A task in another lab](./02-next-lab.md#build-a-collection)
[A heading in this lab](#a-helpful-hint)

Heading fragments are lowercased, punctuation is removed, and spaces become hyphens. Duplicate headings get suffixes (-2, -3). Each lab has its own anchor namespace. Common image, media, font, PDF, text, and CSV assets are served from the selected directory. Assets cannot resolve outside that directory.

Print and PDF

From the browser

Click Print / PDF (or use your browser’s print command), then choose Save as PDF. The print stylesheet includes all labs, regardless of the selected lab or navigation filter, puts the table of contents first, and starts each lab on a new page. Navigation and controls are hidden. For the cleanest output, turn off the browser’s default headers and footers.

From the command line

Install Playwright’s Chromium once, then export:

pnpm exec playwright install chromium
pnpm run pdf ./examples --title "JavaScript workshop" --output ./workbook.pdf
# Or, after pnpm link --global:
lapdog pdf ./my-labs --title "My seminar" --output ./workbook.pdf
# German table of contents and labels:
lapdog pdf ./my-labs --title "Mein Seminar" --language de

When running from source, build the frontend with pnpm run build before exporting. On Linux systems missing browser libraries, use pnpm exec playwright install --with-deps chromium. The published package already contains the frontend, and the Docker image also contains Chromium and its libraries.

Export starts a temporary loopback server, waits for the Vue app, fonts, and images, writes an A4 PDF, and shuts down. The PDF contains a linked lab/task table of contents, document outline, and page-number footers. The table of contents provides links, rather than calculated page numbers. Fonts are bundled locally; external images still need a network connection. Missing images fail CLI export rather than silently producing an incomplete workbook.

Commands

lapdog serve [directory]  --title <title> [--port <port>] [--host <host>] [--dev] [--admin-password <password>]
lapdog pdf   [directory]  --title <title> [--output <file>] [--language <en|de>]

--title is required. Defaults: directory examples relative to your current working directory, host 127.0.0.1, port 4173 (or the next free port if 4173 is taken; an explicit --port is used as given), output lapdog.pdf, language en.

Development and verification

pnpm run dev           # Vue HMR + Markdown live reload
pnpm run build         # Type-check Vue/TypeScript and build frontend + CLI
pnpm test              # Parser, ordering, links, validation, and discovery tests
pnpm run test:e2e      # Browser, hot reload, print, and real PDF export (build + Chromium required)
pnpm run test:package  # Install a packed release in isolation and exercise its CLI and frontend (build required)
pnpm run test:docker   # Test lapdog:test with read-only mounted labs and real PDF export
pnpm run lint          # Lint TypeScript and Vue scripts with Oxlint
pnpm run lint:fix      # Apply automatic lint fixes
pnpm run format        # Format the source with Oxfmt
pnpm run format:check  # Check formatting without writing changes

Oxc provides linting through Oxlint and formatting through Oxfmt. Configuration lives in .oxlintrc.json and .oxfmtrc.json. For reproducible installs, use pnpm install --frozen-lockfile.

Build and test the Docker image locally:

docker build -t lapdog:test .
pnpm run test:docker

Gitea publishing

.gitea/workflows/publish.yml follows the instance settings in ../gitlab-for-developer: gitea.henriburau.de, the ace966 owner, the ubuntu runner label, and the REGISTRY_TOKEN secret.

Instance and repository setup

  1. Host this repository in Gitea and enable Repository Settings → Actions and the package registry.
  2. Use an ubuntu-labelled runner with Node action support and access to a Docker daemon. Docker Buildx and QEMU are set up by the workflow; the daemon must allow the privileged binfmt setup for cross-platform builds. The verification job also needs permission to install Playwright's OS libraries.
  3. Add the repository Actions secret REGISTRY_TOKEN, a personal access token with package read/write permission for the publishing account. As in the reference workflow, Docker login uses gitea.actor; the release pusher and token owner should be the same account (ace966).
  4. Make the package owner private in Gitea before publishing private packages. Gitea package read access follows the user/organization visibility, not the linked repository's visibility. For the configured user namespace, set ace966's visibility to private; only that user can read its private packages. If packages need to be shared with a team, use a private organization and update the owner in .npmrc, package.json's publishConfig, the workflow, and the install/image URLs. Linking a package to a private repository alone does not make it private. publishConfig.access: restricted does not replace Gitea's owner access rules.

The package's publishConfig.registry directs publishing to Gitea. Authentication comes from the Actions secret via the temporary npm configuration created by setup-node.

Pipeline and releases

  • Pull requests: lint, formatting, unit tests, build, browser/PDF tests, isolated npm package installation, and a native Docker build with a read-only-volume/PDF smoke test.
  • Pushes to main: the same checks, then publish multi-platform Docker tags edge and sha-<commit>.
  • Version tags (v*): the same checks, then publish the versioned Docker image and npm package. The tag must match package.json exactly, for example v0.1.0 for version 0.1.0. Stable releases update Docker/npm latest; prereleases such as v0.2.0-beta.1 use their versioned Docker tag and npm next.

To release, update package.json's version, commit it, and push a matching tag:

git add package.json
git commit -m "Release 0.1.0"
git tag v0.1.0
git push origin main
git push origin v0.1.0

The CLI reads its version from package.json. npm's prepack hook builds the release before upload. Gitea rejects duplicate npm versions, so increment the version for each new release. After the first publication, the packages can be linked to the lapdog repository from their Gitea package settings.

Structure

src/server/     TypeScript CLI, Markdown pipeline, Express/SSE server, PDF export
src/client/     Vue application shell and shared base styles
  components/   Focused UI components with colocated screen/print styles
  composables/  Seminar loading/live updates and hash-based lab navigation
  styles/       Styles for server-rendered Markdown
src/shared/     Shared lab, task, and seminar types
examples/       A three-lab sample seminar
tests/          Parser and end-to-end checks

The built server serves the Vue frontend and watches the selected directory with Chokidar. Server-sent events trigger content refreshes without a full-page reload. Vite is only needed for development and builds; PDF export uses Playwright’s Chromium and the same Vue rendering and print CSS as the browser.

Dependencies

Dependencies

ID Version
@fontsource-variable/dm-sans ^5.2.6
@fontsource-variable/manrope ^5.2.6
@iconify-json/vscode-icons ^1.2.82
chokidar ^4.0.3
commander ^14.0.0
express ^5.1.0
gray-matter ^4.0.3
highlight.js ^11.11.1
js-yaml ^4.1.0
markdown-it ^14.1.0
markdown-it-container ^4.0.0
nanospinner ^1.2.2
picocolors ^1.1.1
playwright ^1.55.0
vue ^3.5.21
vue-i18n ^11.4.12

Development Dependencies

ID Version
@types/express ^5.0.3
@types/js-yaml ^4.0.9
@types/markdown-it ^14.1.2
@types/node ^24.3.0
@vitejs/plugin-vue ^6.0.1
husky ^9.1.7
oxfmt ^0.70.0
oxlint ^1.85.0
tsx ^4.20.5
typescript ^5.9.2
vite ^7.1.3
vue-tsc ^3.0.6
Details
npm
2026-10-04 11:50:36 +00:00
6
218 KiB
Assets (1)
Versions (10) View all
0.7.1 2026-10-04
0.7.0 2026-10-04
0.6.0 2026-09-27
0.5.0 2026-09-26
0.4.0 2026-09-26