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 +21 -0
- package/README.md +40 -0
- package/bin/projview.js +83 -0
- package/docs/README.md +28 -0
- package/docs/diagrams/architecture.mmd +9 -0
- package/docs/diagrams/broken-diagram.mmd +5 -0
- package/docs/diagrams/sequence.mmd +13 -0
- package/docs/edge-cases/deep/nested/buried.md +11 -0
- package/docs/edge-cases/faulty-refs.md +23 -0
- package/docs/edge-cases/syntax-stress.md +48 -0
- package/docs/getting-started.md +66 -0
- package/docs/guides/configuration.md +29 -0
- package/docs/guides/installation.md +22 -0
- package/docs/guides/troubleshooting.md +17 -0
- package/docs/reference/api.md +82 -0
- package/docs/reference/glossary.md +16 -0
- package/package.json +45 -0
- package/public/breadcrumb.js +39 -0
- package/public/dom.js +11 -0
- package/public/icons.js +7 -0
- package/public/index.html +71 -0
- package/public/links.js +31 -0
- package/public/live.js +31 -0
- package/public/main.js +27 -0
- package/public/mermaid.js +77 -0
- package/public/paths.js +14 -0
- package/public/search.js +163 -0
- package/public/store.js +8 -0
- package/public/style.css +551 -0
- package/public/toast.js +15 -0
- package/public/toc.js +51 -0
- package/public/tree.js +111 -0
- package/public/vendor/hljs.css +10 -0
- package/public/vendor/mermaid.min.js +3405 -0
- package/public/viewer.js +83 -0
- package/src/render.js +95 -0
- package/src/search.js +101 -0
- package/src/server.js +160 -0
- package/src/walk.js +57 -0
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.
|
package/bin/projview.js
ADDED
|
@@ -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,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) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function cssEsc(s) {
|
|
10
|
+
return (window.CSS && CSS.escape) ? CSS.escape(s) : s.replace(/"/g, '\\"');
|
|
11
|
+
}
|
package/public/icons.js
ADDED
|
@@ -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);
|