@sullux/markdown-docs 1.0.1 → 1.0.3
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/lib/layout.js +30 -14
- package/lib/site.js +39 -16
- package/lib/summary.js +13 -4
- package/package.json +1 -1
package/lib/layout.js
CHANGED
|
@@ -3,7 +3,26 @@ const { getCss } = require('./theme')
|
|
|
3
3
|
const { getSearchScript } = require('./search')
|
|
4
4
|
const { SVGS } = require('./icons')
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const normalizeTargetHref = (href) => {
|
|
7
|
+
if (!href) return '#'
|
|
8
|
+
if (href.startsWith('http://') || href.startsWith('https://') || href.startsWith('/') || href.startsWith('#') || href.startsWith('mailto:')) {
|
|
9
|
+
return href
|
|
10
|
+
}
|
|
11
|
+
return href
|
|
12
|
+
.replace(/(?:^|\/)README\.(?:md|html)(#.*)?$/i, (match) => match.replace(/README\.(?:md|html)/i, 'index.html'))
|
|
13
|
+
.replace(/\.md(#.*)?$/, (match) => match.replace(/\.md/, '.html'))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const calcRelativeHref = (currentHref, targetHref) => {
|
|
17
|
+
if (!targetHref) return '#'
|
|
18
|
+
const normalizedTarget = normalizeTargetHref(targetHref)
|
|
19
|
+
if (normalizedTarget.startsWith('http://') || normalizedTarget.startsWith('https://') || normalizedTarget.startsWith('/') || normalizedTarget.startsWith('#') || normalizedTarget.startsWith('mailto:')) {
|
|
20
|
+
return normalizedTarget
|
|
21
|
+
}
|
|
22
|
+
const normalizedCurrent = normalizeTargetHref(currentHref)
|
|
23
|
+
const rel = path.relative(path.dirname(normalizedCurrent), normalizedTarget)
|
|
24
|
+
return rel || './'
|
|
25
|
+
}
|
|
7
26
|
|
|
8
27
|
const renderNavTree = (items, currentHref) => {
|
|
9
28
|
if (!items?.length) return ''
|
|
@@ -11,7 +30,7 @@ const renderNavTree = (items, currentHref) => {
|
|
|
11
30
|
const flushGroup = () => {
|
|
12
31
|
if (!group.length) return ''
|
|
13
32
|
const list = `<ul class="nav-list">\n` + group.map((item) => {
|
|
14
|
-
const active = item.href === currentHref ? ' active' : ''
|
|
33
|
+
const active = normalizeTargetHref(item.href) === normalizeTargetHref(currentHref) ? ' active' : ''
|
|
15
34
|
const href = calcRelativeHref(currentHref, item.href)
|
|
16
35
|
const sub = item.children?.length ? renderNavTree(item.children, currentHref) : ''
|
|
17
36
|
return `<li class="nav-item"><a href="${href}" class="nav-link${active}">${item.title}</a>${sub ? `<div class="nav-sub">${sub}</div>` : ''}</li>\n`
|
|
@@ -30,7 +49,12 @@ const renderNavTree = (items, currentHref) => {
|
|
|
30
49
|
|
|
31
50
|
const renderTocList = (toc) => toc?.length ? `<div class="toc-title">On this page</div>\n<ul class="toc-list">\n` + toc.map((i) => `<li class="toc-item level-${i.level}"><a href="#${i.id}" class="toc-link" onclick="closeAllDrawers()">${i.title}</a></li>`).join('') + `</ul>\n` : ''
|
|
32
51
|
|
|
33
|
-
const resolveAssetHref = (currentHref, assetPath) =>
|
|
52
|
+
const resolveAssetHref = (currentHref, assetPath) => {
|
|
53
|
+
if (!assetPath || assetPath.startsWith('http') || assetPath.startsWith('<svg') || assetPath.startsWith('data:') || assetPath.startsWith('/')) {
|
|
54
|
+
return assetPath
|
|
55
|
+
}
|
|
56
|
+
return calcRelativeHref(currentHref, assetPath.replace(/^\.\//, ''))
|
|
57
|
+
}
|
|
34
58
|
|
|
35
59
|
const renderSingleLogo = (logo, modeClass, title, currentHref) => logo ? (typeof logo === 'string' && logo.startsWith('<svg') ? logo : `<img src="${resolveAssetHref(currentHref, logo)}" alt="${title || 'Logo'}" class="${modeClass ? `brand-logo ${modeClass}` : 'brand-logo'}" />`) : ''
|
|
36
60
|
|
|
@@ -77,19 +101,11 @@ const renderPageLayout = ({ title, siteTitle, navTree, toc, contentHtml, current
|
|
|
77
101
|
<div class="app-container">
|
|
78
102
|
<aside id="sidebar-drawer" class="app-sidebar"><div class="drawer-header"><span class="drawer-title">Navigation</span><button class="drawer-close" onclick="closeAllDrawers()">${SVGS.close}</button></div><nav class="app-nav">${sidebarNav}</nav></aside>
|
|
79
103
|
<main class="app-main"><article class="app-article">${contentHtml}</article></main>
|
|
80
|
-
<aside id="toc-drawer" class="app-toc">
|
|
81
|
-
<div class="drawer-header"><span class="drawer-title">Page Outline</span><button class="drawer-close" onclick="closeAllDrawers()">${SVGS.close}</button></div>
|
|
82
|
-
${renderTocList(toc)}
|
|
83
|
-
<div class="theme-picker">
|
|
84
|
-
<button class="theme-opt" data-mode="light" onclick="setThemeMode('light')" title="Light">${SVGS.sun}</button>
|
|
85
|
-
<button class="theme-opt" data-mode="system" onclick="setThemeMode('system')" title="System">${SVGS.system}</button>
|
|
86
|
-
<button class="theme-opt" data-mode="dark" onclick="setThemeMode('dark')" title="Dark">${SVGS.moon}</button>
|
|
87
|
-
</div>
|
|
88
|
-
</aside>
|
|
104
|
+
<aside id="toc-drawer" class="app-toc"><div class="toc-container">${renderTocList(toc)}</div></aside>
|
|
89
105
|
</div>
|
|
90
106
|
<div id="drawer-backdrop" class="drawer-backdrop" onclick="closeAllDrawers()"></div>
|
|
91
|
-
|
|
107
|
+
<script>${getSearchScript()}</script>
|
|
92
108
|
</body></html>`
|
|
93
109
|
}
|
|
94
110
|
|
|
95
|
-
module.exports = {
|
|
111
|
+
module.exports = { calcRelativeHref, renderPageLayout }
|
package/lib/site.js
CHANGED
|
@@ -21,14 +21,25 @@ const collectMarkdownFiles = (dir, rootDir = dir) => {
|
|
|
21
21
|
return files
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const firstHtmlPath = path.join(outputDir, generatedPages[0].replace(/\.md$/, '.html'))
|
|
30
|
-
if (fs.existsSync(firstHtmlPath)) fs.copyFileSync(firstHtmlPath, indexPath)
|
|
24
|
+
const extractFirstH1 = (ast) => {
|
|
25
|
+
for (const block of ast.blocks || []) {
|
|
26
|
+
if (block.type === 'header' && block.level === 1) {
|
|
27
|
+
return block.children ? block.children.map((c) => c.value || '').join('') : ''
|
|
28
|
+
}
|
|
31
29
|
}
|
|
30
|
+
return ''
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const normalizeHtmlPath = (relPath) => {
|
|
34
|
+
return relPath
|
|
35
|
+
.replace(/(?:^|\/)README\.md$/i, (match) => match.replace(/README\.md$/i, 'index.html'))
|
|
36
|
+
.replace(/\.md$/, '.html')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const rewriteMarkdownLinks = (html) => {
|
|
40
|
+
return html
|
|
41
|
+
.replace(/href="([^":#]*?)README\.md(#.*?)?"/gi, 'href="$1index.html$2"')
|
|
42
|
+
.replace(/href="([^":#]+)\.md(#.*?)?"/g, 'href="$1.html$2"')
|
|
32
43
|
}
|
|
33
44
|
|
|
34
45
|
const generateSite = (options = {}) => {
|
|
@@ -40,26 +51,38 @@ const generateSite = (options = {}) => {
|
|
|
40
51
|
|
|
41
52
|
for (const file of mdFiles) {
|
|
42
53
|
const rawMd = fs.readFileSync(file.fullPath, 'utf8')
|
|
43
|
-
const ast = parse(rawMd)
|
|
44
|
-
const
|
|
45
|
-
const
|
|
54
|
+
const ast = parse(rawMd)
|
|
55
|
+
const toc = extractToc(ast)
|
|
56
|
+
const rawHtml = markdownToHtml(rawMd)
|
|
57
|
+
const contentHtml = rewriteMarkdownLinks(rawHtml)
|
|
58
|
+
const relHtmlPath = normalizeHtmlPath(file.relPath)
|
|
59
|
+
const outHtmlPath = path.join(config.output, relHtmlPath)
|
|
46
60
|
|
|
47
61
|
fs.mkdirSync(path.dirname(outHtmlPath), { recursive: true })
|
|
48
62
|
|
|
49
|
-
const
|
|
63
|
+
const h1Title = extractFirstH1(ast)
|
|
64
|
+
const fallbackTitle = file.relPath.replace(/(?:^|\/)README\.md$/i, '').replace(/\.md$/, '') || config.title || 'Home'
|
|
65
|
+
const docTitle = h1Title || fallbackTitle
|
|
66
|
+
const pageTitle = config.title && docTitle !== config.title ? `${docTitle} - ${config.title}` : (docTitle || config.title)
|
|
50
67
|
|
|
51
68
|
const fullHtml = renderPageLayout({
|
|
52
|
-
title: pageTitle,
|
|
53
|
-
|
|
54
|
-
|
|
69
|
+
title: pageTitle,
|
|
70
|
+
siteTitle: config.title,
|
|
71
|
+
navTree,
|
|
72
|
+
toc,
|
|
73
|
+
contentHtml,
|
|
74
|
+
currentHref: relHtmlPath,
|
|
75
|
+
logo: config.logo,
|
|
76
|
+
favicon: config.favicon,
|
|
77
|
+
links: config.links,
|
|
78
|
+
theme: config.theme,
|
|
55
79
|
})
|
|
56
80
|
|
|
57
81
|
fs.writeFileSync(outHtmlPath, fullHtml, 'utf8')
|
|
58
82
|
const plainText = rawMd.replace(/[#*`_\[\]()\-]/g, ' ').replace(/\s+/g, ' ').trim()
|
|
59
|
-
searchIndex.push({ title:
|
|
83
|
+
searchIndex.push({ title: docTitle, href: relHtmlPath, content: plainText.slice(0, 300) })
|
|
60
84
|
}
|
|
61
85
|
|
|
62
|
-
ensureIndexHtml(config.output, mdFiles.map((f) => f.relPath))
|
|
63
86
|
fs.writeFileSync(path.join(config.output, 'search-index.json'), JSON.stringify(searchIndex, null, 2))
|
|
64
87
|
copyAssets(config.input, config.output)
|
|
65
88
|
|
package/lib/summary.js
CHANGED
|
@@ -2,6 +2,14 @@ const fs = require('node:fs')
|
|
|
2
2
|
const path = require('node:path')
|
|
3
3
|
const { parse } = require('@sullux/markdown-compiler')
|
|
4
4
|
|
|
5
|
+
const normalizeHref = (url) => {
|
|
6
|
+
if (!url) return '#'
|
|
7
|
+
if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('/') || url.startsWith('#')) return url
|
|
8
|
+
return url
|
|
9
|
+
.replace(/(?:^|\/)README\.md(#.*)?$/i, (match) => match.replace(/README\.md/i, 'index.html'))
|
|
10
|
+
.replace(/\.md(#.*)?$/, (match) => match.replace(/\.md/, '.html'))
|
|
11
|
+
}
|
|
12
|
+
|
|
5
13
|
const parseSummaryMd = (content) => {
|
|
6
14
|
const ast = parse(content)
|
|
7
15
|
const nav = []
|
|
@@ -18,7 +26,7 @@ const parseSummaryMd = (content) => {
|
|
|
18
26
|
for (const node of itemNodes) {
|
|
19
27
|
if (node.type === 'link') {
|
|
20
28
|
const title = node.children ? node.children.map((c) => c.value || '').join('') : ''
|
|
21
|
-
listItems.push({ title, href: node.url, children: [] })
|
|
29
|
+
listItems.push({ title, href: normalizeHref(node.url), children: [] })
|
|
22
30
|
}
|
|
23
31
|
}
|
|
24
32
|
}
|
|
@@ -49,8 +57,9 @@ const scanDir = (dir, rootDir = dir) => {
|
|
|
49
57
|
items.push({ type: 'section', title, items: children })
|
|
50
58
|
}
|
|
51
59
|
} else if (entry.isFile() && entry.name.endsWith('.md') && entry.name !== 'SUMMARY.md') {
|
|
52
|
-
const
|
|
53
|
-
|
|
60
|
+
const isReadme = /^README\.md$/i.test(entry.name)
|
|
61
|
+
const title = isReadme ? 'Overview' : entry.name.replace(/\.md$/, '').replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
|
|
62
|
+
items.push({ title, href: normalizeHref(relPath), children: [] })
|
|
54
63
|
}
|
|
55
64
|
}
|
|
56
65
|
|
|
@@ -65,4 +74,4 @@ const getNavigationTree = (inputDir) => {
|
|
|
65
74
|
return scanDir(inputDir)
|
|
66
75
|
}
|
|
67
76
|
|
|
68
|
-
module.exports = { parseSummaryMd, scanDir, getNavigationTree }
|
|
77
|
+
module.exports = { normalizeHref, parseSummaryMd, scanDir, getNavigationTree }
|
package/package.json
CHANGED