Skip to content

Commit dd8a3cb

Browse files
committed
feat: add tocs feature & fix page element generation issue (#9).
1 parent 6d0801a commit dd8a3cb

9 files changed

+196
-28
lines changed

docs/bash.md

+1-3
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,9 @@ NAME = "John" # => Error (关于空间)
3434

3535
### 注释
3636

37-
```bash
37+
```shell
3838
# 这是一个内联 Bash 注释。
39-
```
4039

41-
```shell
4240
: '
4341
这是一个
4442
非常整洁的评论

scripts/assets/menu.svg

+3
Loading

scripts/create.mjs

+39-17
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import markdown from '@wcj/markdown-to-html';
22
import rehypeDocument from 'rehype-document';
33
import remarkGemoji from 'remark-gemoji';
4+
import rehypeRaw from 'rehype-raw';
5+
import rehypeAttrs from 'rehype-attr';
6+
import rehypeKatex from 'rehype-katex';
47
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
58
import rehypeSlug from 'rehype-slug';
69
import { htmlTagAddAttri } from './nodes/htmlTagAddAttri.mjs';
@@ -9,7 +12,7 @@ import { header } from './nodes/header.mjs';
912
import { rehypeUrls } from './utils/rehypeUrls.mjs';
1013
import { tooltips } from './utils/tooltips.mjs';
1114
import { homeCardIcons } from './utils/homeCardIcons.mjs';
12-
import { getTocsTree } from './utils/getTocsTree.mjs';
15+
import { getTocsTree, getTocsTitleNode, getTocsTitleNodeWarpper, addTocsInWarp } from './utils/getTocsTree.mjs';
1316
import { rehypeTitle } from './utils/rehypeTitle.mjs';
1417
import { anchorPoint } from './utils/anchorPoint.mjs';
1518
import { rehypePreviewHTML } from './utils/rehypePreviewHTML.mjs';
@@ -29,30 +32,49 @@ export function create(str = '', options = {}) {
2932
rehypePlugins: [
3033
rehypeSlug,
3134
rehypeAutolinkHeadings,
32-
[rehypeDocument, {
33-
title: `${title ? `${title} & ` : ''} ${subTitle} Quick Reference`,
34-
css: [ ...options.css ],
35-
link: [
36-
{rel: 'icon', href: favicon, type: 'image/svg+xml'}
37-
],
38-
meta: [
39-
{ description: `${description}为开发人员分享快速参考备忘单。` },
40-
{ keywords: `Quick,Reference,cheatsheet,${!options.isHome && options.filename || ''}` }
41-
]
42-
}],
35+
[rehypeDocument, {
36+
title: `${title ? `${title} & ` : ''} ${subTitle} Quick Reference`,
37+
css: [ ...options.css ],
38+
link: [
39+
{rel: 'icon', href: favicon, type: 'image/svg+xml'}
40+
],
41+
meta: [
42+
{ description: `${description}为开发人员分享快速参考备忘单。` },
43+
{ keywords: `Quick,Reference,cheatsheet,${!options.isHome && options.filename || ''}` }
44+
]
45+
}]
4346
],
47+
filterPlugins: (type, plugins = []) => {
48+
if (type === 'rehype') {
49+
const dt = plugins.filter(plug => {
50+
return /(rehypeRaw)/.test(plug.name) ? false : true;
51+
});
52+
// 放在 rehypeDocument 前面
53+
dt.unshift(rehypeRaw)
54+
return dt;
55+
}
56+
return plugins
57+
},
4458
rewrite: (node, index, parent) => {
4559
rehypePreviewHTML(node, parent);
4660
rehypeTitle(node, options.filename);
4761
homeCardIcons(node, parent, options.isHome);
4862
tooltips(node, index, parent);
4963
htmlTagAddAttri(node, options);
5064
rehypeUrls(node);
51-
if (node.type === 'element' && node.tagName === 'body') {
52-
node.children = getTocsTree([ ...node.children ]);
53-
node.children.unshift(header(options));
54-
node.children.push(footer());
55-
node.children.push(anchorPoint());
65+
if (node.children) {
66+
if (node.type === 'element' && node.tagName === 'body') {
67+
const tocsData = getTocsTree([ ...node.children ]);
68+
if (!options.isHome) {
69+
const tocsMenus = getTocsTitleNode([...tocsData]);
70+
node.children = addTocsInWarp([...tocsData], getTocsTitleNodeWarpper(tocsMenus))
71+
} else {
72+
node.children = tocsData;
73+
}
74+
node.children.unshift(header(options));
75+
node.children.push(footer());
76+
node.children.push(anchorPoint());
77+
}
5678
}
5779
}
5880
}

scripts/style.css

+60
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ body {
6666
--color-accent-emphasis: #0969da;
6767
--color-attention-subtle: #fff8c5;
6868
--color-danger-fg: #cf222e;
69+
--box-shadow: 109 109 109;
6970
}
7071

7172
[data-color-mode*='dark'], [data-color-mode*='dark'] body {
@@ -112,6 +113,7 @@ body {
112113
--color-accent-emphasis: #1f6feb;
113114
--color-attention-subtle: rgba(187,128,9,0.15);
114115
--color-danger-fg: #f85149;
116+
--box-shadow: 0 0 0;
115117
}
116118

117119
body {
@@ -474,6 +476,64 @@ a.text-grey {
474476
display: flex;
475477
flex-direction: column;
476478
gap: 3rem;
479+
position: relative;
480+
}
481+
.menu-tocs {
482+
position: sticky;
483+
top: 0;
484+
z-index: 88;
485+
display: inline-flex;
486+
}
487+
.menu-tocs:hover > .menu-modal {
488+
display: block;
489+
border-radius: 0.5rem;
490+
padding: 0.3rem;
491+
max-height: 100vh;
492+
overflow: auto;
493+
background-color: var(--color-canvas-subtle);
494+
box-shadow: 0 8px 24px rgba(var(--box-shadow)/0.2);
495+
}
496+
.menu-tocs > .menu-btn {
497+
border: 1px solid var(--color-border-default);
498+
display: flex;
499+
border-radius: 0.3rem;
500+
padding: 0.3rem 0.4rem;
501+
font-size: 1.3rem;
502+
margin-left: -3rem;
503+
margin-top: 0.3rem;
504+
position: absolute;
505+
}
506+
.menu-tocs > .menu-modal {
507+
width: 260px;
508+
position:absolute;
509+
display: none;
510+
margin-left: -1rem;
511+
}
512+
.menu-tocs > .menu-modal a + a {
513+
margin-bottom: 0.2rem;
514+
}
515+
.menu-tocs > .menu-modal a:hover {
516+
background-color: var(--color-neutral-muted);
517+
}
518+
.menu-tocs > .menu-modal a.is-active-link {
519+
background-color: var(--color-border-muted);
520+
text-decoration-color: #10b981;
521+
}
522+
.menu-tocs > .menu-modal a {
523+
display: block;
524+
overflow: hidden;
525+
padding: 0.3rem 0.5rem;
526+
}
527+
528+
.menu-tocs > .menu-modal a.leve2 {
529+
font-weight: bold;
530+
}
531+
532+
.menu-tocs > .menu-modal a.leve3 {
533+
padding-left: 1.2rem;
534+
}
535+
.menu-tocs > .menu-modal a.leve4, .menu-tocs > .menu-modal a.leve5, .menu-tocs > .menu-modal a.leve6 {
536+
padding-left: 2.1rem;
477537
}
478538

479539
.wrap-header.h2wrap > h2 {

scripts/utils/anchorPoint.mjs

+21
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const scripts = `
33
if(('onhashchange' in window) && ((typeof document.documentMode==='undefined') || document.documentMode==8)) {
44
window.onhashchange = function () {
55
anchorPoint()
6+
updateAnchor()
67
};
78
}
89
function anchorPoint() {
@@ -16,6 +17,26 @@ function anchorPoint() {
1617
}
1718
}
1819
anchorPoint();
20+
21+
function updateAnchor(element) {
22+
const anchorContainer = document.querySelectorAll('.menu-tocs .menu-modal a.tocs-link');
23+
anchorContainer.forEach((tocanchor) => {
24+
tocanchor.classList.remove('is-active-link');
25+
});
26+
const anchor = element || document.querySelector(\`a.tocs-link[href='\${decodeURIComponent(window.location.hash)}']\`);
27+
console.log('anchor', anchor)
28+
if (anchor) {
29+
anchor.classList.add('is-active-link');
30+
}
31+
}
32+
// toc 定位
33+
updateAnchor()
34+
const anchor = document.querySelectorAll('.menu-tocs .menu-modal a.tocs-link');
35+
anchor.forEach((item) => {
36+
item.addEventListener('click', (e) => {
37+
updateAnchor()
38+
})
39+
})
1940
`;
2041

2142
export function anchorPoint() {

scripts/utils/getSVGNode.mjs

+3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import fs from 'fs-extra';
2+
import path from 'path';
23
import rehypeParse from 'rehype-parse';
34
import {unified} from 'unified';
45
import { VFile } from 'vfile';
56

7+
export const ICONS_PATH = path.resolve(process.cwd(), 'scripts/assets')
8+
69
export function getSVGNode(iconPath, space = 'svg') {
710
const svgStr = fs.readFileSync(iconPath);
811
const processor = unified().use(rehypeParse,{ fragment: true, space })

scripts/utils/getTocsTree.mjs

+67-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,71 @@
1+
import path from 'path';
12
import { panelAddNumber } from './panelAddNumber.mjs';
23
import { getChilds, getHeader } from './childs.mjs';
4+
import { ICONS_PATH, getSVGNode } from './getSVGNode.mjs';
5+
6+
export const titleNum = (tagName = '') => Number(tagName.replace(/^h/, ''));
7+
8+
export function getTocsTitleNode(arr = [], result = []) {
9+
arr.forEach(({ tagName, type, properties, children }) => {
10+
if (/^h[23456]/.test(tagName)) {
11+
const num = titleNum(tagName)
12+
const props = { 'aria-hidden': "true", class: `leve${num} tocs-link`, href: '#' + (properties.id || '') }
13+
result.push({ tagName: 'a', type, properties: props, children: (children || []).filter(m => m.type === 'text') })
14+
} else if (children?.length > 0) {
15+
result = result.concat(getTocsTitleNode(children))
16+
}
17+
});
18+
return result
19+
}
20+
21+
export function addTocsInWarp(tocsData = [], menuData, isDone = false) {
22+
const childs = tocsData.map((item) => {
23+
if (item.properties?.class?.includes('h1wrap-body')) {
24+
isDone = true;
25+
}
26+
if (!isDone && item.children) {
27+
item.children = addTocsInWarp([...item.children], menuData, isDone)
28+
}
29+
return item
30+
});
31+
if (isDone) {
32+
childs.splice(1, 0, menuData);
33+
}
34+
return childs
35+
}
36+
37+
export const getTocsTitleNodeWarpper = (children = []) => {
38+
const iconPath = path.resolve(ICONS_PATH, `menu.svg`);
39+
const svgNode = getSVGNode(iconPath);
40+
return {
41+
type: 'element',
42+
tagName: 'div',
43+
properties: {
44+
class: 'menu-tocs',
45+
},
46+
children: [
47+
{
48+
type: 'element',
49+
tagName: 'div',
50+
properties: {
51+
class: 'menu-btn',
52+
},
53+
children: [
54+
// { type: 'text', value: 'menu' }
55+
...svgNode
56+
]
57+
},
58+
{
59+
type: 'element',
60+
tagName: 'div',
61+
properties: {
62+
class: 'menu-modal',
63+
},
64+
children: children
65+
}
66+
]
67+
}
68+
}
369

470
/** Markdown 文档转成树形结构 */
571
export function getTocsTree(arr = [], result = []) {
@@ -14,9 +80,7 @@ export function getTocsTree(arr = [], result = []) {
1480
if (level === -1) {
1581
level = toc.number;
1682
}
17-
const titleNum = Number(toc.tagName?.replace(/^h/, ''));
18-
19-
if (toc.number === level && titleNum === level) {
83+
if (toc.number === level && titleNum(toc.tagName) === level) {
2084
const header = getHeader(data.slice(n), level);
2185
const wrapCls = ['wrap'];
2286
const headerCls = ['wrap-header', `h${level}wrap`];

scripts/utils/homeCardIcons.mjs

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import fs from 'fs-extra';
22
import path from 'path';
3-
import { getSVGNode } from './getSVGNode.mjs';
4-
5-
export const ICONS_PATH = path.resolve(process.cwd(), 'scripts/assets')
3+
import { getSVGNode, ICONS_PATH } from './getSVGNode.mjs';
64

75
export function homeCardIcons(node, parent, isHome) {
86
if (isHome && node && node.type === 'element' && node.properties?.class?.includes('home-card')) {

scripts/utils/rehypeTitle.mjs

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import fs from 'fs-extra';
22
import path from 'path';
3-
import { getSVGNode } from './getSVGNode.mjs';
4-
import { ICONS_PATH } from './homeCardIcons.mjs';
3+
import { getSVGNode, ICONS_PATH } from './getSVGNode.mjs';
54

65
export function rehypeTitle(node, iconName) {
76
if (node.type === 'element' && node.tagName === 'h1' && iconName !== 'index') {

0 commit comments

Comments
 (0)