projview 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Wikus Bergh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # projview
2
+
3
+ ephemeral local previewer for **markdown** + **mermaid** files. run it in any
4
+ directory, get a soft-dark cartoonish localhost view of every `.md` / `.mmd`
5
+ under it, close it and nothing's left behind.
6
+
7
+ ## install
8
+
9
+ ```bash
10
+ npm i -g projview # global CLI
11
+ # or run without installing:
12
+ npx projview
13
+ ```
14
+
15
+ ## usage
16
+
17
+ ```bash
18
+ # try it on the bundled sample docs (no setup needed)
19
+ projview --demo
20
+
21
+ # from the directory you want to browse
22
+ projview
23
+
24
+ # or point it somewhere
25
+ projview ./docs --port 5000
26
+ projview /path/to/notes --no-open
27
+ ```
28
+
29
+ ## what it does
30
+
31
+ | step | detail |
32
+ | --- | --- |
33
+ | walk | recursively finds `.md` / `.mmd`, skips `node_modules`, `dist`, dot-dirs |
34
+ | index | builds an in-memory tree (nothing written to disk) |
35
+ | serve | spawns a localhost site + opens your browser |
36
+ | live | edits to files reload in the browser automatically |
37
+
38
+ > ctrl-c and it's gone - "indexing" is never persisted.
39
+
40
+ see [samples/architecture.md](samples/architecture.md) for a mermaid demo.
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ import path from 'node:path';
3
+ import { spawn } from 'node:child_process';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { startServer } from '../src/server.js';
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const DEMO_DIR = path.join(__dirname, '..', 'docs'); // bundled sample docs
9
+
10
+ function parseArgs(argv) {
11
+ const opts = {
12
+ root: process.cwd(),
13
+ port: Number(process.env.PORT) || 4321,
14
+ host: process.env.HOST || '127.0.0.1',
15
+ open: true
16
+ };
17
+ const rest = [];
18
+ for (let i = 0; i < argv.length; i++) {
19
+ const arg = argv[i];
20
+ if (arg === '--port' || arg === '-p') opts.port = Number(argv[++i]) || opts.port;
21
+ else if (arg === '--no-open') opts.open = false;
22
+ else if (arg === '--demo') opts.demo = true;
23
+ else if (arg === '--help' || arg === '-h') opts.help = true;
24
+ else rest.push(arg);
25
+ }
26
+ if (rest[0]) opts.root = path.resolve(process.cwd(), rest[0]);
27
+ if (opts.demo) opts.root = DEMO_DIR;
28
+ return opts;
29
+ }
30
+
31
+ function openBrowser(url) {
32
+ const cmd = process.platform === 'darwin' ? 'open'
33
+ : process.platform === 'win32' ? 'cmd' : 'xdg-open';
34
+ const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
35
+ try {
36
+ spawn(cmd, args, { stdio: 'ignore', detached: true }).unref();
37
+ } catch { /* no browser, just print the url */ }
38
+ }
39
+
40
+ const HELP = `projview - ephemeral previewer for markdown + mermaid
41
+
42
+ usage:
43
+ projview [path] [options]
44
+
45
+ arguments:
46
+ path directory to index (default: current directory)
47
+
48
+ options:
49
+ -p, --port <n> preferred port (default: 4321, climbs if taken)
50
+ --demo preview projview's bundled sample docs
51
+ --no-open do not auto-open the browser
52
+ -h, --help show this help
53
+
54
+ nothing is written to disk - ctrl-c and it's gone.`;
55
+
56
+ async function main() {
57
+ const opts = parseArgs(process.argv.slice(2));
58
+ if (opts.help) {
59
+ console.log(HELP);
60
+ return;
61
+ }
62
+
63
+ const { port, close } = await startServer(opts.root, { port: opts.port, host: opts.host });
64
+ const url = `http://${opts.host}:${port}`;
65
+
66
+ console.log(`\n projview -> ${url}`);
67
+ console.log(` indexing -> ${opts.root}`);
68
+ console.log(` (ctrl-c to stop - nothing is persisted)\n`);
69
+
70
+ if (opts.open) openBrowser(url);
71
+
72
+ const shutdown = async () => {
73
+ await close();
74
+ process.exit(0);
75
+ };
76
+ process.on('SIGINT', shutdown);
77
+ process.on('SIGTERM', shutdown);
78
+ }
79
+
80
+ main().catch((err) => {
81
+ console.error('projview failed to start:', err.message);
82
+ process.exit(1);
83
+ });
package/docs/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # projview test docs
2
+
3
+ a deliberately mixed doc set - **most of it should render perfectly**, and a
4
+ few files contain **intentional breakage** so we can test how projview handles
5
+ faults. use this tree while iterating on the tool.
6
+
7
+ ## index
8
+
9
+ | area | doc | what it exercises |
10
+ | --- | --- | --- |
11
+ | start | [getting started](getting-started.md) | headings h1-h6, code, tables, anchors, a valid diagram |
12
+ | guides | [installation](guides/installation.md) | relative links up/down, one dead link |
13
+ | guides | [configuration](guides/configuration.md) | explicit `{#id}` anchors, a missing-anchor link |
14
+ | guides | [troubleshooting](guides/troubleshooting.md) | a link to a file that doesn't exist |
15
+ | diagrams | [architecture](diagrams/architecture.mmd) | standalone valid flowchart |
16
+ | diagrams | [sequence](diagrams/sequence.mmd) | standalone valid sequence |
17
+ | diagrams | [broken diagram](diagrams/broken-diagram.mmd) | **invalid mermaid** - error card test |
18
+ | reference | [api](reference/api.md) | long doc - TOC + scroll-spy |
19
+ | reference | [glossary](reference/glossary.md) | cross-links + external links |
20
+ | edge | [syntax stress](edge-cases/syntax-stress.md) | gnarly markdown + inline broken fence |
21
+ | edge | [faulty refs](edge-cases/faulty-refs.md) | **several dead links** on purpose |
22
+ | edge | [buried file](edge-cases/deep/nested/buried.md) | deep nesting - indentation + auto-expand |
23
+
24
+ ## intentional faults (should NOT crash the app)
25
+
26
+ - a dead link right here: [this file is missing](does-not-exist.md)
27
+ - an external link: [anthropic](https://www.anthropic.com)
28
+ - a link to a missing anchor: [no such section](#nope)
@@ -0,0 +1,9 @@
1
+ flowchart LR
2
+ subgraph cli[projview CLI]
3
+ walk[walk fs] --> tree[in-mem tree]
4
+ end
5
+ tree --> server[http server]
6
+ server -->|/api/tree| browser
7
+ server -->|/api/file| browser
8
+ server -. SSE .-> browser
9
+ browser --> render[rendered view]
@@ -0,0 +1,5 @@
1
+ flowchart LR
2
+ A[start] --> B{missing closing brace
3
+ B --> C[this will never parse]
4
+ C -->|no arrow target
5
+ %% intentionally broken mermaid to exercise the inline error card
@@ -0,0 +1,13 @@
1
+ sequenceDiagram
2
+ autonumber
3
+ participant U as user
4
+ participant C as CLI
5
+ participant B as browser
6
+ U->>C: projview ./docs
7
+ C->>C: walk + index
8
+ C->>B: open localhost
9
+ B->>C: GET /api/tree
10
+ C-->>B: tree json
11
+ B->>C: GET /api/file?p=...
12
+ C-->>B: rendered html
13
+ Note over C,B: edit a file -> SSE -> live reload
@@ -0,0 +1,11 @@
1
+ # buried file
2
+
3
+ I live at `edge-cases/deep/nested/buried.md` - four levels deep.
4
+
5
+ Use me to check:
6
+
7
+ - the indent guides clearly show how deep this is
8
+ - navigating here via a link auto-expands `edge-cases → deep → nested`
9
+ - the breadcrumb at the top shows the full path
10
+
11
+ [back up to the index](../../../README.md)
@@ -0,0 +1,23 @@
1
+ # faulty refs
2
+
3
+ Every internal link here is broken on purpose. None should crash the app;
4
+ each should land on the "not found" state (for files) or no-op (for anchors).
5
+
6
+ ## dead file links
7
+
8
+ - [sibling that doesn't exist](nonexistent.md)
9
+ - [up into a missing folder](../../way/up/gone.md)
10
+ - [a missing .mmd](missing-diagram.mmd)
11
+
12
+ ## dead anchors
13
+
14
+ - [missing anchor on this page](#not-a-real-heading)
15
+
16
+ ## non-previewable (should be ignored, not navigated)
17
+
18
+ - [an image link](../assets/picture.png)
19
+ - [a pdf link](../assets/manual.pdf)
20
+
21
+ ## one valid link for contrast
22
+
23
+ - [back to the index](../README.md)
@@ -0,0 +1,48 @@
1
+ # syntax stress
2
+
3
+ Gnarly-but-valid markdown, plus one broken inline fence.
4
+
5
+ ## nested everything
6
+
7
+ > blockquote
8
+ > > nested blockquote
9
+ > > > triple nested
10
+
11
+ 1. ordered
12
+ 1. nested ordered
13
+ - mixed bullet
14
+ ```js
15
+ const deep = "code in a list";
16
+ ```
17
+ 2. back to top level
18
+
19
+ ## inline html
20
+
21
+ <details>
22
+ <summary>click to expand (raw html)</summary>
23
+
24
+ Hidden content with a <kbd>Ctrl</kbd>+<kbd>K</kbd> hint.
25
+
26
+ </details>
27
+
28
+ ## a table with code + pipes
29
+
30
+ | token | meaning |
31
+ | --- | --- |
32
+ | `a \| b` | escaped pipe |
33
+ | `` `code` `` | literal backticks |
34
+
35
+ ## broken mermaid fence (inline)
36
+
37
+ The diagram below is malformed - it should become an error card, and the rest
38
+ of the page must still render fine:
39
+
40
+ ```mermaid
41
+ flowchart TD
42
+ X[ no closing bracket
43
+ X --> Y -->
44
+ ```
45
+
46
+ ## after the broken fence
47
+
48
+ This paragraph proves rendering continued past the broken diagram.
@@ -0,0 +1,66 @@
1
+ # getting started
2
+
3
+ a kitchen-sink markdown file. the TOC on the right should list **only** the
4
+ h1/h2/h3 headings below - the h4/h5/h6 ones must be ignored.
5
+
6
+ ## section one - text
7
+
8
+ Some prose with **bold (peach)**, _italic_, `inline code`, and a
9
+ [link back to the index](README.md). Also a [jump to section three](#section-three---a-diagram).
10
+
11
+ ### subsection 1a
12
+
13
+ A nested heading - should appear in the TOC.
14
+
15
+ #### h4 should be hidden
16
+
17
+ ##### h5 should be hidden
18
+
19
+ ###### h6 should be hidden
20
+
21
+ ## section two - code + tables
22
+
23
+ ```js
24
+ // javascript, highlighted
25
+ export function add(a, b) {
26
+ return a + b;
27
+ }
28
+ ```
29
+
30
+ ```bash
31
+ # a shell block
32
+ npx projview ./docs --port 4500
33
+ ```
34
+
35
+ ```python
36
+ def greet(name: str) -> str:
37
+ return f"hello {name}"
38
+ ```
39
+
40
+ | col a | col b | col c |
41
+ | --- | --- | --- |
42
+ | 1 | two | three |
43
+ | 4 | five | six |
44
+
45
+ ### subsection 2a - lists
46
+
47
+ - bullet one
48
+ - bullet two
49
+ - nested bullet
50
+ - deeper
51
+ - [ ] unchecked task
52
+ - [x] checked task
53
+
54
+ ## section three - a diagram
55
+
56
+ ```mermaid
57
+ flowchart TD
58
+ A[start] --> B{valid?}
59
+ B -->|yes| C[render]
60
+ B -->|no| D[error card]
61
+ C --> E[done]
62
+ D --> E
63
+ ```
64
+
65
+ > if the diagram above rendered and the TOC shows three h2s + two h3s (no h4-h6),
66
+ > this file passes.
@@ -0,0 +1,29 @@
1
+ # configuration
2
+
3
+ Tests explicit `{#id}` heading anchors and a link to a **missing** anchor.
4
+
5
+ ## ports {#ports}
6
+
7
+ projview picks a free port starting at 4321. Jump target: [#ports](#ports).
8
+
9
+ ## env overrides {#env-overrides}
10
+
11
+ Set `PROJVIEW_PORT` to pin a port. This heading has an explicit id so
12
+ `configuration.md#env-overrides` resolves to it.
13
+
14
+ ## a valid diagram
15
+
16
+ ```mermaid
17
+ sequenceDiagram
18
+ participant CLI
19
+ participant Server
20
+ CLI->>Server: start(root, port)
21
+ Server-->>CLI: listening
22
+ ```
23
+
24
+ ## broken anchor link
25
+
26
+ This link points at an anchor that doesn't exist on this page - it should
27
+ no-op gracefully (no jump, no crash): [go nowhere](#this-anchor-is-missing).
28
+
29
+ Back to [installation](installation.md).
@@ -0,0 +1,22 @@
1
+ # installation
2
+
3
+ Tests relative links that go **up** and **sideways** in the tree.
4
+
5
+ ## prerequisites
6
+
7
+ - node 18+
8
+ - a directory with `.md` / `.mmd` files
9
+
10
+ ## steps
11
+
12
+ 1. see the [project index](../README.md) (up one level)
13
+ 2. then read [configuration](configuration.md) (same folder)
14
+ 3. if stuck, the [troubleshooting guide](troubleshooting.md) (same folder)
15
+ 4. jump straight to [a config anchor](configuration.md#env-overrides) (cross-doc anchor)
16
+
17
+ ## a deliberately dead link
18
+
19
+ This one points at a file that does not exist - clicking it should show a
20
+ friendly "not found" state, not a blank screen:
21
+
22
+ [missing page](./does-not-exist.md)
@@ -0,0 +1,17 @@
1
+ # troubleshooting
2
+
3
+ ## diagram won't render?
4
+
5
+ Check the browser console - a malformed diagram shows an inline error card
6
+ instead of taking down the page. See [the broken diagram](../diagrams/broken-diagram.mmd).
7
+
8
+ ## link goes nowhere?
9
+
10
+ Internal links to files that don't exist show a "not found" state. Example of a
11
+ link to a file in a folder that doesn't even exist:
12
+
13
+ [ghost doc](../nope/ghost.md)
14
+
15
+ ## still stuck?
16
+
17
+ Open an issue (external): [github](https://github.com/anthropics/claude-code/issues).
@@ -0,0 +1,82 @@
1
+ # api reference
2
+
3
+ A long document on purpose - enough h2/h3 sections to test the right-side TOC
4
+ and scroll-spy (the active section should track as you scroll).
5
+
6
+ ## overview
7
+
8
+ projview exposes a tiny HTTP surface. Everything is read-only and in-memory.
9
+
10
+ ## endpoints
11
+
12
+ ### GET /api/tree
13
+
14
+ Returns the in-memory file tree as JSON.
15
+
16
+ ### GET /api/file
17
+
18
+ Renders a single file to HTML on demand. Query param `p` is the relative path.
19
+
20
+ ### GET /api/events
21
+
22
+ Server-sent events stream for live reload.
23
+
24
+ ## the walker
25
+
26
+ ### ignored directories
27
+
28
+ `node_modules`, `dist`, `build`, dot-dirs, and friends are skipped.
29
+
30
+ ### previewable extensions
31
+
32
+ `.md` and `.mmd` for now. Images and mdx are deferred.
33
+
34
+ ## the renderer
35
+
36
+ ### markdown
37
+
38
+ markdown-it with highlight.js for fenced code.
39
+
40
+ ### mermaid fences
41
+
42
+ A ```` ```mermaid ```` fence becomes a div the client renders.
43
+
44
+ ### heading anchors
45
+
46
+ Every heading gets a slug id; explicit `{#id}` wins when present.
47
+
48
+ ## the server
49
+
50
+ ### static assets
51
+
52
+ The SPA shell plus vendored mermaid + hljs css.
53
+
54
+ ### path safety
55
+
56
+ Requested paths are resolved and confined to the root.
57
+
58
+ ## the client
59
+
60
+ ### tree nav
61
+
62
+ Collapsible dirs, filter box, auto-expand on navigate.
63
+
64
+ ### viewer
65
+
66
+ On-demand fetch + render, in-app link routing, anchor jumps.
67
+
68
+ ### live reload
69
+
70
+ A chokidar watcher broadcasts change/tree events over SSE.
71
+
72
+ ## performance notes
73
+
74
+ Indexing is just a directory walk - no persistence, no cache files.
75
+
76
+ ## limitations
77
+
78
+ No auth, localhost only, single root per process. By design.
79
+
80
+ ## wrap up
81
+
82
+ If the TOC tracked your scroll position through these sections, scroll-spy works.
@@ -0,0 +1,16 @@
1
+ # glossary
2
+
3
+ Cross-links plus external links.
4
+
5
+ ## terms
6
+
7
+ - **index** - the in-memory file tree. See [the api](api.md#the-walker).
8
+ - **previewable** - a `.md` or `.mmd` file. See [extensions](api.md#previewable-extensions).
9
+ - **live reload** - SSE-driven refresh. See [events](api.md#get-apievents).
10
+
11
+ ## see also
12
+
13
+ - [getting started](../getting-started.md)
14
+ - [configuration](../guides/configuration.md#env-overrides)
15
+ - external: [markdown-it](https://github.com/markdown-it/markdown-it)
16
+ - external: [mermaid](https://mermaid.js.org)
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "projview",
3
+ "version": "0.1.0",
4
+ "description": "Ephemeral local previewer for markdown + mermaid files in any directory",
5
+ "type": "module",
6
+ "bin": {
7
+ "projview": "bin/projview.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node bin/projview.js",
11
+ "demo": "node bin/projview.js --demo",
12
+ "test": "node --test"
13
+ },
14
+ "files": [
15
+ "bin",
16
+ "src",
17
+ "public",
18
+ "docs"
19
+ ],
20
+ "engines": {
21
+ "node": ">=18"
22
+ },
23
+ "keywords": [
24
+ "markdown",
25
+ "mermaid",
26
+ "preview",
27
+ "docs",
28
+ "cli"
29
+ ],
30
+ "license": "MIT",
31
+ "author": "Wikus Bergh (https://github.com/vikingsearth)",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/vikingsearth/projview.git"
35
+ },
36
+ "homepage": "https://github.com/vikingsearth/projview#readme",
37
+ "bugs": {
38
+ "url": "https://github.com/vikingsearth/projview/issues"
39
+ },
40
+ "dependencies": {
41
+ "chokidar": "^4.0.3",
42
+ "highlight.js": "^11.11.1",
43
+ "markdown-it": "^14.1.0"
44
+ }
45
+ }
@@ -0,0 +1,39 @@
1
+ /* clickable path breadcrumb at the top of the viewer */
2
+
3
+ import { $ } from './dom.js';
4
+ import { state } from './store.js';
5
+ import { revealDir } from './tree.js';
6
+
7
+ const breadcrumbEl = $('breadcrumb');
8
+
9
+ export function renderBreadcrumb(p) {
10
+ if (!p) { breadcrumbEl.hidden = true; return; }
11
+ breadcrumbEl.hidden = false;
12
+ const parts = p.split('/');
13
+ const frag = document.createDocumentFragment();
14
+
15
+ const root = document.createElement('span');
16
+ root.className = 'crumb root';
17
+ root.textContent = state.rootName;
18
+ frag.appendChild(root);
19
+
20
+ let acc = '';
21
+ parts.forEach((seg, i) => {
22
+ const sep = document.createElement('span');
23
+ sep.className = 'crumb-sep';
24
+ sep.textContent = '/';
25
+ frag.appendChild(sep);
26
+
27
+ acc = acc ? `${acc}/${seg}` : seg;
28
+ const isFile = i === parts.length - 1;
29
+ const crumb = document.createElement(isFile ? 'span' : 'button');
30
+ crumb.className = 'crumb' + (isFile ? ' current' : ' dir');
31
+ crumb.textContent = seg;
32
+ if (!isFile) {
33
+ const dirPath = acc;
34
+ crumb.addEventListener('click', () => revealDir(dirPath));
35
+ }
36
+ frag.appendChild(crumb);
37
+ });
38
+ breadcrumbEl.replaceChildren(frag);
39
+ }
package/public/dom.js ADDED
@@ -0,0 +1,11 @@
1
+ /* tiny DOM helpers shared across modules */
2
+
3
+ export const $ = (id) => document.getElementById(id);
4
+
5
+ export function esc(s) {
6
+ return s.replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
7
+ }
8
+
9
+ export function cssEsc(s) {
10
+ return (window.CSS && CSS.escape) ? CSS.escape(s) : s.replace(/"/g, '\\"');
11
+ }
@@ -0,0 +1,7 @@
1
+ /* small monochrome inline icons (inherit currentColor) - shared by tree + palette */
2
+
3
+ export const SVG_FOLDER = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h3.6l2 2H19a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>`;
4
+ export const SVG_FILE = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5"/></svg>`;
5
+ export const SVG_DIAGRAM = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="2.6"/><circle cx="6" cy="12" r="2.6"/><circle cx="18" cy="19" r="2.6"/><line x1="8.4" y1="10.7" x2="15.6" y2="6.3"/><line x1="8.4" y1="13.3" x2="15.6" y2="17.7"/></svg>`;
6
+
7
+ export const fileIcon = (ext) => (ext === '.mmd' ? SVG_DIAGRAM : SVG_FILE);