From 7648c1e5351b482d252fb061b045b9321e412db9 Mon Sep 17 00:00:00 2001 From: wanglu Date: Mon, 5 Sep 2022 22:51:54 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E8=87=AA=E5=B7=B1=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E7=9B=AE=E5=BD=95=E5=AF=BC=E8=88=AA=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=20#29?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../website/components/Markdown/heading.tsx | 14 +++ .../website/components/Markdown/index.tsx | 7 ++ .../components/MarkdownTocBar/core.tsx | 101 ++++++++++++++++++ .../components/MarkdownTocBar/index.tsx | 11 ++ .../components/MarkdownTocBar/tools.ts | 84 +++++++++++++++ packages/website/components/Toc/index.tsx | 49 ++++----- packages/website/package.json | 2 +- packages/website/pages/_app.tsx | 3 +- packages/website/styles/toc-dark.css | 25 ----- packages/website/styles/toc.css | 101 ++++++++++++++++++ packages/website/yarn.lock | 30 ++---- 11 files changed, 351 insertions(+), 76 deletions(-) create mode 100644 packages/website/components/Markdown/heading.tsx create mode 100644 packages/website/components/MarkdownTocBar/core.tsx create mode 100644 packages/website/components/MarkdownTocBar/index.tsx create mode 100644 packages/website/components/MarkdownTocBar/tools.ts delete mode 100644 packages/website/styles/toc-dark.css create mode 100644 packages/website/styles/toc.css diff --git a/packages/website/components/Markdown/heading.tsx b/packages/website/components/Markdown/heading.tsx new file mode 100644 index 000000000..2f71d834a --- /dev/null +++ b/packages/website/components/Markdown/heading.tsx @@ -0,0 +1,14 @@ +import React from "react"; +import { HeadingProps } from "react-markdown/lib/ast-to-react"; + +export const HeadingRender = (props: HeadingProps) => { + const { node, children } = props; + let text = ""; + try { + text = children[0] as string; + } catch (err) { + text = ""; + } + + return React.createElement(node.tagName, { ["data-id"]: text }, text); +}; diff --git a/packages/website/components/Markdown/index.tsx b/packages/website/components/Markdown/index.tsx index 9cc52aaa8..115ecd352 100644 --- a/packages/website/components/Markdown/index.tsx +++ b/packages/website/components/Markdown/index.tsx @@ -10,6 +10,7 @@ import rehypeKatex from "rehype-katex"; import "katex/dist/katex.min.css"; import ImageBox from "../ImageBox"; import dynamic from "next/dynamic"; +import { HeadingRender } from "./heading"; export default function (props: { content: string }) { return ( <> @@ -83,6 +84,12 @@ export default function (props: { content: string }) { ); }, + h1: HeadingRender, + h2: HeadingRender, + h3: HeadingRender, + h4: HeadingRender, + h5: HeadingRender, + h6: HeadingRender, img(props) { return ( { + ev.stopPropagation(); + ev.preventDefault(); + + let top: any = null; + let topEl: any = null; + let lastMin = 9999999999; + for (const each of items) { + const el: any = document.querySelector(`[data-id="${each.text}"]`); + + if (!topEl) { + top = each; + topEl = el; + } + if (el) { + const scrollTop = + window.pageYOffset || + document.documentElement.scrollTop || + document.body.scrollTop || + 0; + const v = Math.abs(scrollTop + props.headingOffset - el.offsetTop); + if (v <= lastMin) { + lastMin = v; + top = each; + topEl = el; + } + } + } + setCurrIndex(top.index); + + // updateHash(top.text); + }, 100); + useEffect(() => { + const el = document.querySelector(".markdown-navigation div.active"); + if (el) { + let to = (el as any)?.offsetTop; + if (to <= props.headingOffset) { + to = 0; + } + scroll.animateScroll.scrollTo(to, { + containerId: "toc-container", + smooth: true, + delay: 0, + spyThrottle: 0, + }); + } + }, [currIndex, props.headingOffset]); + //TODO 逻辑完善的 hash 更新 + useEffect(() => { + window.addEventListener("scroll", handleScroll); + return () => { + window.removeEventListener("scroll", handleScroll); + }; + }); + const res = []; + for (const each of items) { + const cls = `title-anchor title-level${each.level} ${ + currIndex == each.index ? "active" : "" + }`; + res.push( +
{ + const el: any = document.querySelector(`[data-id="${each.text}"]`); + + if (el) { + let to = el.offsetTop - props.headingOffset; + if (to <= 100) { + to = 0; + } + scroll.animateScroll.scrollTo(to); + } + }} + > + {each.text} +
+ ); + } + return ( + <> +
{ + scroll.animateScroll.scrollToTop(); + }} + > + 目录 +
+
{res}
+ + ); +} diff --git a/packages/website/components/MarkdownTocBar/index.tsx b/packages/website/components/MarkdownTocBar/index.tsx new file mode 100644 index 000000000..adf5c53c2 --- /dev/null +++ b/packages/website/components/MarkdownTocBar/index.tsx @@ -0,0 +1,11 @@ +import { useMemo } from "react"; +import Core from "./core"; +import { parseNavStructure } from "./tools"; + +export default function (props: { content: string; headingOffset?: number }) { + const navData = useMemo(() => { + return parseNavStructure(props.content.replace(/`#/g, "")); + }, [props]); + + return ; +} diff --git a/packages/website/components/MarkdownTocBar/tools.ts b/packages/website/components/MarkdownTocBar/tools.ts new file mode 100644 index 000000000..f5a8f2ff3 --- /dev/null +++ b/packages/website/components/MarkdownTocBar/tools.ts @@ -0,0 +1,84 @@ +export interface NavItem { + index: number; + level: number; + listNo: string; + text: string; +} +export const parseNavStructure = (source: string): NavItem[] => { + const contentWithoutCode = source + .replace(/^[^#]+\n/g, "") + .replace(/(?:[^\n#]+)#+\s([^#\n]+)\n*/g, "") // 匹配行内出现 # 号的情况 + .replace(/^#\s[^#\n]*\n+/, "") + .replace(/```[^`\n]*\n+[^```]+```\n+/g, "") + .replace(/`([^`\n]+)`/g, "$1") + .replace(/\*\*?([^*\n]+)\*\*?/g, "$1") + .replace(/__?([^_\n]+)__?/g, "$1") + .trim(); + + const pattOfTitle = /#+\s([^#\n]+)\n*/g; + const matchResult = contentWithoutCode.match(pattOfTitle); + + if (!matchResult) { + return []; + } + + const navData = matchResult.map((r, i) => ({ + index: i, + //@ts-ignore + level: r.match(/^#+/g)[0].length, + text: r.replace(pattOfTitle, "$1"), + })); + + let maxLevel = 0; + navData.forEach((t) => { + if (t.level > maxLevel) { + maxLevel = t.level; + } + }); + let matchStack = []; + // 此部分重构,原有方法会出现次级标题后再次出现高级标题时,listNo重复的bug + for (let i = 0; i < navData.length; i++) { + const t: any = navData[i]; + const { level } = t; + while ( + matchStack.length && + matchStack[matchStack.length - 1].level > level + ) { + matchStack.pop(); + } + if (matchStack.length === 0) { + const arr = new Array(maxLevel).fill(0); + arr[level - 1] += 1; + matchStack.push({ + level, + arr, + }); + t.listNo = trimArrZero(arr).join("."); + continue; + } + const { arr } = matchStack[matchStack.length - 1] as any; + const newArr = arr.slice(); + newArr[level - 1] += 1; + matchStack.push({ + level, + arr: newArr, + }); + t.listNo = trimArrZero(newArr).join("."); + } + return navData as NavItem[]; +}; + +const trimArrZero = (arr: any) => { + let start, end; + for (start = 0; start < arr.length; start++) { + if (arr[start]) { + break; + } + } + for (end = arr.length - 1; end >= 0; end--) { + if (arr[end]) { + break; + } + } + return arr.slice(start, end + 1); +}; diff --git a/packages/website/components/Toc/index.tsx b/packages/website/components/Toc/index.tsx index 2ce08f5c8..d6cebf59d 100644 --- a/packages/website/components/Toc/index.tsx +++ b/packages/website/components/Toc/index.tsx @@ -1,28 +1,31 @@ -import MarkdownNavbar from "markdown-navbar"; -import { useEffect } from "react"; +import { useEffect, useRef } from "react"; import Headroom from "headroom.js"; -import scroll from "react-scroll"; +import MarkdownTocBar from "../MarkdownTocBar"; export default function (props: { content: string; showSubMenu: "true" | "false"; }) { + const { current } = useRef({ hasInit: false }); useEffect(() => { - const el = document.querySelector("#toc-card"); - if (el) { - const headroom = new Headroom(el, { - classes: { - initial: `side-bar${ - props.showSubMenu == "true" ? "" : " no-submenu" - }`, - pinned: "side-bar-pinned", - unpinned: "side-bar-unpinned", - top: "side-bar-top", - notTop: "side-bar-not-top", - }, - }); - headroom.init(); + if (!current.hasInit) { + const el = document.querySelector("#toc-card"); + if (el) { + current.hasInit = true; + const headroom = new Headroom(el, { + classes: { + initial: `side-bar${ + props.showSubMenu == "true" ? "" : " no-submenu" + }`, + pinned: "side-bar-pinned", + unpinned: "side-bar-unpinned", + top: "side-bar-top", + notTop: "side-bar-not-top", + }, + }); + headroom.init(); + } } - }); + }, [current]); return (
-
- 目录 -
- + {/* { + onHashChange={(newHash: string, oldHash: string) => { // 判断一下当前激活的元素 const el = document.querySelector( ".markdown-navigation div.active" @@ -61,7 +62,7 @@ export default function (props: { } // console.log(newHash, oldHash, el); }} - /> + /> */}
); diff --git a/packages/website/package.json b/packages/website/package.json index 037c5ef44..d27f43325 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -8,13 +8,13 @@ }, "dependencies": { "@next/bundle-analyzer": "^12.2.5", + "@types/lodash": "^4.14.184", "@types/markdown-navbar": "^1.4.0", "@types/mermaid": "^8.2.9", "@waline/client": "^2.6.1", "dayjs": "^1.11.3", "headroom.js": "^0.12.0", "js-base64": "^3.7.2", - "markdown-navbar": "^1.4.3", "mermaid": "^9.1.6", "next": "latest", "react": "18.1.0", diff --git a/packages/website/pages/_app.tsx b/packages/website/pages/_app.tsx index 6bdeab63a..c39bd43b0 100644 --- a/packages/website/pages/_app.tsx +++ b/packages/website/pages/_app.tsx @@ -1,7 +1,6 @@ import "../styles/globals.css"; -import "markdown-navbar/dist/navbar.css"; import "../styles/side-bar.css"; -import "../styles/toc-dark.css"; +import "../styles/toc.css"; import "../styles/var.css"; import "../styles/github-markdown.css"; import "../styles/tip-card.css"; diff --git a/packages/website/styles/toc-dark.css b/packages/website/styles/toc-dark.css deleted file mode 100644 index 4a072204b..000000000 --- a/packages/website/styles/toc-dark.css +++ /dev/null @@ -1,25 +0,0 @@ -.dark .markdown-navigation .title-level1 { - color: rgb(158, 158, 158); -} -.dark .markdown-navigation .title-level2 { - color: rgb(148, 148, 148); -} -.dark .markdown-navigation .title-level3 { - color: rgb(138, 138, 138); -} -.dark .markdown-navigation .title-level4 { - color: rgb(128, 128, 128); -} -.dark .markdown-navigation .title-level5 { - color: rgb(118, 118, 118); -} -.dark .markdown-navigation .title-level6 { - color: rgb(108, 108, 108); -} -.dark .markdown-navigation .title-anchor.active { - color: #4f79be; -} -.dark .markdown-navigation .title-anchor:hover, -.dark .markdown-navigation .title-anchor.active { - background-color: #3a3c41; -} diff --git a/packages/website/styles/toc.css b/packages/website/styles/toc.css new file mode 100644 index 000000000..b3fc672f0 --- /dev/null +++ b/packages/website/styles/toc.css @@ -0,0 +1,101 @@ +.dark .markdown-navigation .title-level1 { + color: rgb(158, 158, 158); +} +.dark .markdown-navigation .title-level2 { + color: rgb(148, 148, 148); +} +.dark .markdown-navigation .title-level3 { + color: rgb(138, 138, 138); +} +.dark .markdown-navigation .title-level4 { + color: rgb(128, 128, 128); +} +.dark .markdown-navigation .title-level5 { + color: rgb(118, 118, 118); +} +.dark .markdown-navigation .title-level6 { + color: rgb(108, 108, 108); +} +.dark .markdown-navigation .title-anchor.active { + color: #4f79be; +} +.dark .markdown-navigation .title-anchor:hover, +.dark .markdown-navigation .title-anchor.active { + background-color: #3a3c41; +} +.markdown-navigation { + font-size: 14px; + font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Helvetica", "Arial", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif; + width: 100%; + overflow-x: hidden; + overflow-y: scroll; + scrollbar-width: 0; + width:200px +} +.markdown-navigation::-webkit-scrollbar{ + width:0; +} + +.markdown-navigation .title-anchor { + display: block; + color: #bbb; + transition: all 0.2s; + margin: 0.8em 0; + font-weight: lighter; + line-height: 2em; + padding-right: 1.8em; + cursor: pointer; +} + +.markdown-navigation .title-anchor:hover, +.markdown-navigation .title-anchor.active { + background-color: #f8f8f8; + text-decoration: inherit; +} + +.markdown-navigation .title-anchor.active { + color: #007fff; +} + +.markdown-navigation .title-anchor small { + margin: 0 0.8em; +} + +.markdown-navigation .title-level1 { + color: #000; + font-size: 1.2em; + padding-left: 1em; + font-weight: normal; +} + +.markdown-navigation .title-level2 { + color: #333; + font-size: 1em; + padding-left: 1em; + font-weight: normal; +} + +.markdown-navigation .title-level3 { + color: #666; + font-size: 0.8em; + padding-left: 3em; + font-weight: normal; +} + +.markdown-navigation .title-level4 { + color: #999; + font-size: 0.72em; + padding-left: 5em; +} + +.markdown-navigation .title-level5 { + color: #aaa; + font-size: 0.72em; + padding-left: 7em; +} + +.markdown-navigation .title-level6 { + color: #bbb; + font-size: 0.72em; + padding-left: 9em; +} diff --git a/packages/website/yarn.lock b/packages/website/yarn.lock index 36ce7ae3f..a74905e82 100644 --- a/packages/website/yarn.lock +++ b/packages/website/yarn.lock @@ -158,6 +158,11 @@ resolved "https://registry.yarnpkg.com/@types/katex/-/katex-0.11.1.tgz#34de04477dcf79e2ef6c8d23b41a3d81f9ebeaf5" integrity sha512-DUlIj2nk0YnJdlWgsFuVKcX27MLW0KbKmGVoUHmFr+74FYYNUDAaj9ZqTADvsbE8rfxuVmSFc7KczYn5Y09ozg== +"@types/lodash@^4.14.184": + version "4.14.184" + resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.184.tgz#23f96cd2a21a28e106dc24d825d4aa966de7a9fe" + integrity sha512-RoZphVtHbxPZizt4IcILciSWiC6dcn+eZ8oX9IWEYfDMcocdd42f7NPI6fQj+6zI8y4E0L7gu2pcZKLGTRaV9Q== + "@types/markdown-navbar@^1.4.0": version "1.4.0" resolved "https://registry.yarnpkg.com/@types/markdown-navbar/-/markdown-navbar-1.4.0.tgz#5ddbb2e30e0cabfdc3df856d3288f69f8332c0ab" @@ -687,11 +692,6 @@ copy-to-clipboard@^3.3.1: dependencies: toggle-selection "^1.0.6" -core-js@3: - version "3.24.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.24.1.tgz#cf7724d41724154010a6576b7b57d94c5d66e64f" - integrity sha512-0QTBSYSUZ6Gq21utGzkfITDylE8jWC9Ne1D2MrhvlsZBI1x39OdDIVbzSqtgMndIy6BlHxBXpMGqzZmnztg2rg== - cross-env@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" @@ -1878,15 +1878,6 @@ magic-string@^0.25.7: dependencies: sourcemap-codec "^1.4.8" -markdown-navbar@^1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/markdown-navbar/-/markdown-navbar-1.4.3.tgz#f92e7ec5cf58b994e90188794497a1b597621b7d" - integrity sha512-esZQ9fHbjnOq/YnmKRrHTRhIO5+UmU3g5BS2tly7RwWrWCjOWv0Nq2A8r1GF9IBZKvrwM/rzaMgjjsQlkbh0tA== - dependencies: - core-js "3" - prop-types "^15.7.2" - react "^16.12.0" - markdown-table@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.2.tgz#9b59eb2c1b22fe71954a65ff512887065a7bb57c" @@ -2626,7 +2617,7 @@ prismjs@~1.27.0: resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.27.0.tgz#bb6ee3138a0b438a3653dd4d6ce0cc6510a45057" integrity sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA== -prop-types@^15.0.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@^15.0.0, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -2801,15 +2792,6 @@ react@18.1.0: dependencies: loose-envify "^1.1.0" -react@^16.12.0: - version "16.14.0" - resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d" - integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.2" - read-cache@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774"