@inovus-medical/docs 0.2.1 → 0.5.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/README.md +2 -1
- package/assets/client.js +51 -0
- package/assets/styles.css +26 -2
- package/bin/cli.js +6 -4
- package/package.json +1 -1
- package/src/markdown.js +15 -0
- package/src/serve.js +42 -19
package/README.md
CHANGED
|
@@ -27,7 +27,8 @@ Flags: `--root <dir>` `--in <docs>` `--out <dist>` `--port <n>`.
|
|
|
27
27
|
## What it does
|
|
28
28
|
|
|
29
29
|
- Renders markdown with `markdown-it` + `@shikijs/markdown-it` (dual light/dark
|
|
30
|
-
syntax highlighting),
|
|
30
|
+
syntax highlighting), Mermaid diagrams (` ```mermaid ` fences), heading anchors,
|
|
31
|
+
tables, and frontmatter.
|
|
31
32
|
- Generates sidebar navigation from your folder structure.
|
|
32
33
|
- Builds a client-side search index, light/dark theme, on-page table of contents,
|
|
33
34
|
and prev/next paging.
|
package/assets/client.js
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
(function () {
|
|
2
2
|
var root = document.documentElement
|
|
3
3
|
|
|
4
|
+
// ---------- Mermaid (loaded only when diagrams are present) ----------
|
|
5
|
+
var mermaidLoading = null
|
|
6
|
+
|
|
7
|
+
function mermaidTheme() {
|
|
8
|
+
return root.getAttribute('data-theme') === 'dark' ? 'dark' : 'default'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function prepareMermaidNodes() {
|
|
12
|
+
var nodes = document.querySelectorAll('.mermaid')
|
|
13
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
14
|
+
var el = nodes[i]
|
|
15
|
+
if (!el.getAttribute('data-src')) {
|
|
16
|
+
el.setAttribute('data-src', el.textContent)
|
|
17
|
+
}
|
|
18
|
+
el.removeAttribute('data-processed')
|
|
19
|
+
el.removeAttribute('data-mermaid-svg')
|
|
20
|
+
el.textContent = el.getAttribute('data-src')
|
|
21
|
+
}
|
|
22
|
+
return nodes
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function renderMermaid() {
|
|
26
|
+
var nodes = document.querySelectorAll('.mermaid')
|
|
27
|
+
if (!nodes.length) return Promise.resolve()
|
|
28
|
+
|
|
29
|
+
if (!mermaidLoading) {
|
|
30
|
+
mermaidLoading = import('https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs')
|
|
31
|
+
.then(function (mod) { return mod.default })
|
|
32
|
+
.catch(function (err) {
|
|
33
|
+
mermaidLoading = null
|
|
34
|
+
console.error('Failed to load Mermaid', err)
|
|
35
|
+
throw err
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return mermaidLoading.then(function (mermaid) {
|
|
40
|
+
var list = prepareMermaidNodes()
|
|
41
|
+
mermaid.initialize({
|
|
42
|
+
startOnLoad: false,
|
|
43
|
+
securityLevel: 'strict',
|
|
44
|
+
theme: mermaidTheme()
|
|
45
|
+
})
|
|
46
|
+
return mermaid.run({ nodes: list })
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
4
50
|
// Theme toggle
|
|
5
51
|
var toggle = document.getElementById('theme-toggle')
|
|
6
52
|
if (toggle) {
|
|
@@ -8,9 +54,14 @@
|
|
|
8
54
|
var next = root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'
|
|
9
55
|
root.setAttribute('data-theme', next)
|
|
10
56
|
try { localStorage.setItem('theme', next) } catch (e) {}
|
|
57
|
+
if (document.querySelector('.mermaid')) renderMermaid()
|
|
11
58
|
})
|
|
12
59
|
}
|
|
13
60
|
|
|
61
|
+
if (document.querySelector('.mermaid')) {
|
|
62
|
+
renderMermaid()
|
|
63
|
+
}
|
|
64
|
+
|
|
14
65
|
// Mobile sidebar
|
|
15
66
|
var menuBtn = document.getElementById('menu-toggle')
|
|
16
67
|
var sidebar = document.querySelector('.sidebar')
|
package/assets/styles.css
CHANGED
|
@@ -102,8 +102,7 @@ a:hover { text-decoration: underline; }
|
|
|
102
102
|
.layout {
|
|
103
103
|
display: grid;
|
|
104
104
|
grid-template-columns: var(--sidebar-w) minmax(0, 1fr) var(--toc-w);
|
|
105
|
-
|
|
106
|
-
margin: 0 auto;
|
|
105
|
+
width: 100%;
|
|
107
106
|
}
|
|
108
107
|
.sidebar {
|
|
109
108
|
position: sticky; top: var(--header-h); align-self: start;
|
|
@@ -174,6 +173,31 @@ a:hover { text-decoration: underline; }
|
|
|
174
173
|
.header-anchor { color: var(--text-soft); opacity: 0; margin-left: .3em; font-weight: 400; text-decoration: none; }
|
|
175
174
|
.prose h2:hover .header-anchor, .prose h3:hover .header-anchor { opacity: 1; }
|
|
176
175
|
|
|
176
|
+
/* ---------- Mermaid ---------- */
|
|
177
|
+
.mermaid-wrap {
|
|
178
|
+
margin: 1.4em 0;
|
|
179
|
+
padding: 18px 16px;
|
|
180
|
+
background: var(--bg-soft);
|
|
181
|
+
border: 1px solid var(--border);
|
|
182
|
+
border-radius: 10px;
|
|
183
|
+
overflow-x: auto;
|
|
184
|
+
text-align: center;
|
|
185
|
+
}
|
|
186
|
+
.prose .mermaid-wrap pre.mermaid {
|
|
187
|
+
background: transparent;
|
|
188
|
+
border: none;
|
|
189
|
+
padding: 0;
|
|
190
|
+
margin: 0;
|
|
191
|
+
overflow: visible;
|
|
192
|
+
font-size: inherit;
|
|
193
|
+
line-height: normal;
|
|
194
|
+
text-align: center;
|
|
195
|
+
}
|
|
196
|
+
.mermaid-wrap .mermaid svg {
|
|
197
|
+
max-width: 100%;
|
|
198
|
+
height: auto;
|
|
199
|
+
}
|
|
200
|
+
|
|
177
201
|
/* Shiki dual-theme color swap */
|
|
178
202
|
[data-theme='dark'] .shiki,
|
|
179
203
|
[data-theme='dark'] .shiki span {
|
package/bin/cli.js
CHANGED
|
@@ -58,16 +58,18 @@ async function run() {
|
|
|
58
58
|
if (cmd === 'dev') {
|
|
59
59
|
const { pageCount, outPath, contentDir } = await build({ root, inDir, outDir })
|
|
60
60
|
console.log(`✓ Built ${pageCount} page(s) from ${contentDir === '.' ? 'repo root' : contentDir + '/'}`)
|
|
61
|
-
serve({ dir: outPath, port })
|
|
62
|
-
|
|
61
|
+
const { port: actual } = await serve({ dir: outPath, port })
|
|
62
|
+
const busy = actual !== port ? ` (port ${port} was busy)` : ''
|
|
63
|
+
console.log(`→ Serving http://localhost:${actual}${busy} (watching ${contentDir === '.' ? 'repo root' : contentDir + '/'} for changes)`)
|
|
63
64
|
watchAndRebuild({ root, inDir: contentDir, outDir })
|
|
64
65
|
return
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
if (cmd === 'serve') {
|
|
68
69
|
const outPath = path.join(root, outDir)
|
|
69
|
-
serve({ dir: outPath, port })
|
|
70
|
-
|
|
70
|
+
const { port: actual } = await serve({ dir: outPath, port })
|
|
71
|
+
const busy = actual !== port ? ` (port ${port} was busy)` : ''
|
|
72
|
+
console.log(`→ Serving ${outDir}/ at http://localhost:${actual}${busy}`)
|
|
71
73
|
return
|
|
72
74
|
}
|
|
73
75
|
|
package/package.json
CHANGED
package/src/markdown.js
CHANGED
|
@@ -58,6 +58,21 @@ export async function createRenderer() {
|
|
|
58
58
|
})
|
|
59
59
|
)
|
|
60
60
|
|
|
61
|
+
// Mermaid fences must not go through Shiki — emit source for client-side render.
|
|
62
|
+
const defaultFence = md.renderer.rules.fence
|
|
63
|
+
md.renderer.rules.fence = (tokens, idx, options, env, self) => {
|
|
64
|
+
const info = String(tokens[idx].info || '')
|
|
65
|
+
.trim()
|
|
66
|
+
.split(/\s+/)[0]
|
|
67
|
+
.toLowerCase()
|
|
68
|
+
if (info === 'mermaid' || info === 'mmd') {
|
|
69
|
+
const code = tokens[idx].content.replace(/\n$/, '')
|
|
70
|
+
const escaped = md.utils.escapeHtml(code)
|
|
71
|
+
return `<div class="mermaid-wrap"><pre class="mermaid">${escaped}</pre></div>\n`
|
|
72
|
+
}
|
|
73
|
+
return defaultFence(tokens, idx, options, env, self)
|
|
74
|
+
}
|
|
75
|
+
|
|
61
76
|
return {
|
|
62
77
|
render(content) {
|
|
63
78
|
const tokens = md.parse(content, {})
|
package/src/serve.js
CHANGED
|
@@ -40,24 +40,47 @@ async function resolveFile(dir, urlPath) {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Start a static server for `dir`. If the requested port is busy, automatically tries the
|
|
45
|
+
* next ones (up to maxAttempts) instead of crashing. Resolves with the server and the
|
|
46
|
+
* actual port it bound to.
|
|
47
|
+
*/
|
|
48
|
+
export function serve({ dir, port = 8788, maxAttempts = 25 }) {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const server = http.createServer(async (req, res) => {
|
|
51
|
+
try {
|
|
52
|
+
const file = await resolveFile(dir, req.url || '/')
|
|
53
|
+
if (file) {
|
|
54
|
+
res.writeHead(200, { 'content-type': TYPES[path.extname(file)] || 'application/octet-stream' })
|
|
55
|
+
createReadStream(file).pipe(res)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
const notFound = path.join(dir, '404.html')
|
|
59
|
+
try {
|
|
60
|
+
const body = await fs.readFile(notFound)
|
|
61
|
+
res.writeHead(404, { 'content-type': 'text/html; charset=utf-8' })
|
|
62
|
+
res.end(body)
|
|
63
|
+
} catch {
|
|
64
|
+
res.writeHead(404, { 'content-type': 'text/plain' })
|
|
65
|
+
res.end('Not found')
|
|
66
|
+
}
|
|
67
|
+
} catch (err) {
|
|
68
|
+
// A single bad request should never take down the dev server.
|
|
69
|
+
res.writeHead(500, { 'content-type': 'text/plain' })
|
|
70
|
+
res.end('Internal error: ' + err.message)
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
let attempt = 0
|
|
75
|
+
server.on('error', (err) => {
|
|
76
|
+
if (err.code === 'EADDRINUSE' && attempt < maxAttempts) {
|
|
77
|
+
attempt++
|
|
78
|
+
server.listen(port + attempt)
|
|
79
|
+
} else {
|
|
80
|
+
reject(err)
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
server.once('listening', () => resolve({ server, port: server.address().port }))
|
|
84
|
+
server.listen(port)
|
|
60
85
|
})
|
|
61
|
-
server.listen(port)
|
|
62
|
-
return server
|
|
63
86
|
}
|