@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 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 calcRelativeHref = (currentHref, targetHref) => targetHref ? (path.relative(path.dirname(currentHref), targetHref.replace(/\.md$/, '.html')) || './') : '#'
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) => (!assetPath || assetPath.startsWith('http') || assetPath.startsWith('<svg') || assetPath.startsWith('data:')) ? assetPath : calcRelativeHref(currentHref, assetPath.replace(/^\.\//, ''))
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
- ${getSearchScript()}
107
+ <script>${getSearchScript()}</script>
92
108
  </body></html>`
93
109
  }
94
110
 
95
- module.exports = { renderPageLayout, calcRelativeHref }
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 ensureIndexHtml = (outputDir, generatedPages) => {
25
- if (generatedPages.some((p) => p === 'index.md')) return
26
- const readmePath = path.join(outputDir, 'README.html'), indexPath = path.join(outputDir, 'index.html')
27
- if (fs.existsSync(readmePath)) fs.copyFileSync(readmePath, indexPath)
28
- else if (generatedPages.length > 0) {
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), toc = extractToc(ast), rawHtml = markdownToHtml(rawMd)
44
- const contentHtml = rawHtml.replace(/href="([^":#]+)\.md(#.*?)?"/g, 'href="$1.html$2"')
45
- const relHtmlPath = file.relPath.replace(/\.md$/, '.html'), outHtmlPath = path.join(config.output, relHtmlPath)
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 pageTitle = config.title ? `${file.relPath.replace(/\.md$/, '')} - ${config.title}` : file.relPath.replace(/\.md$/, '')
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, siteTitle: config.title,
53
- navTree, toc, contentHtml, currentHref: file.relPath,
54
- logo: config.logo, favicon: config.favicon, links: config.links, theme: config.theme,
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: file.relPath.replace(/\.md$/, ''), href: relHtmlPath, content: plainText.slice(0, 300) })
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 title = entry.name.replace(/\.md$/, '').replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
53
- items.push({ title, href: relPath, children: [] })
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sullux/markdown-docs",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "A zero-dependency, local-first static documentation site generator compiling GitBook-style Markdown docs.",
5
5
  "main": "./index.js",
6
6
  "bin": {