├── .eslintrc.json ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── components ├── DarkSwitch.tsx ├── Footer.tsx ├── HOC │ └── Linkable.tsx ├── Heading.tsx ├── Loading.tsx ├── MDXComponents.tsx ├── MDXWrapper.tsx ├── NavBar.tsx ├── SideBar │ ├── DocType.tsx │ ├── Search.tsx │ ├── SideBarItem.tsx │ ├── SideBarTree.tsx │ └── index.tsx ├── ThemeProvider.tsx ├── Title.tsx └── Toc.tsx ├── constant ├── config.ts ├── sidebar-developer.ts └── sidebar-user.ts ├── docs ├── developer │ ├── api.md │ ├── cdn.md │ ├── component.md │ ├── environment.md │ └── index.mdx └── user │ ├── Q&A.mdx │ ├── changelog.md │ ├── donate.mdx │ ├── features.mdx │ ├── index.mdx │ ├── install.mdx │ ├── migrate-v1.mdx │ └── settings.mdx ├── hooks ├── useMdxPath.ts └── useRouterTree.ts ├── loaders └── replace-remote-content │ ├── __test__ │ ├── Comp.tsx │ ├── __snapshots__ │ │ └── replace-remote-content.test.ts.snap │ ├── changelog.md │ ├── example.md │ ├── example.mdx │ ├── html-comment.md │ ├── image-self-close.md │ └── replace-remote-content.test.ts │ ├── index.js │ └── main.mjs ├── next-env.d.ts ├── next.config.mjs ├── package.json ├── pages ├── _app.tsx ├── _document.tsx ├── docs │ └── [...slug].tsx ├── index.tsx └── ssr.tsx ├── plugins ├── recma-lift-up-props.mjs ├── rehype-highlight.mjs ├── rehype-static-props.mjs ├── remark-export-heading.mjs ├── remark-path-to-repo.mjs └── remark-yaml.js ├── pnpm-lock.yaml ├── postcss.config.js ├── prettier.config.js ├── public ├── bilibili-evolved-wide-color.svg ├── bilibili-evolved.svg ├── docs │ └── images │ │ └── compressed │ │ ├── afdian.jpg │ │ └── wechat.jpg ├── entries │ └── .gitkeep ├── favicon.png ├── fonts │ └── Inter-roman.var.woff2 ├── icon.svg ├── icons │ ├── github-light.png │ └── github.png └── images │ ├── index │ ├── bilibili-dark.png │ ├── fresh-home.png │ ├── image-0.png │ ├── image-dark-0.png │ ├── image-dark.png │ └── image.png │ ├── manage-panel.jpg │ ├── settings-panel.jpg │ └── side-panel.jpg ├── scripts ├── build.mjs └── fetch-entries.mjs ├── styles ├── fonts.css └── globals.css ├── tsconfig.json └── types ├── global-env.d.ts └── type.d.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | 39 | .idea 40 | .vercel 41 | /public/entries/**/* 42 | !/public/entries/.gitkeep 43 | 44 | todo -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Next.js: debug server-side", 6 | "type": "node-terminal", 7 | "request": "launch", 8 | "command": "pnpm run dev" 9 | }, 10 | { 11 | "name": "Next.js: debug client-side", 12 | "type": "pwa-chrome", 13 | "request": "launch", 14 | "url": "http://localhost:3000" 15 | }, 16 | { 17 | "name": "Next.js: debug full stack", 18 | "type": "node-terminal", 19 | "request": "launch", 20 | "command": "pnpm run dev", 21 | "console": "integratedTerminal", 22 | "serverReadyAction": { 23 | "pattern": "started server on .+, url: (https?://.+)", 24 | "uriFormat": "%s", 25 | "action": "debugWithChrome" 26 | } 27 | }, 28 | { 29 | "type": "node", 30 | "request": "launch", 31 | "name": "Debug Current Test File", 32 | "autoAttachChildProcesses": true, 33 | "skipFiles": ["/**", "**/node_modules/**"], 34 | "program": "${workspaceRoot}/node_modules/vitest/vitest.mjs", 35 | "args": ["run", "${relativeFile}"], 36 | "smartStep": true, 37 | "console": "integratedTerminal" 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "bilibili", 4 | "tailwindcss" 5 | ], 6 | "typescript.tsdk": ".\\node_modules\\typescript\\lib", 7 | "typescript.enablePromptUseWorkspaceTsdk": true 8 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 FoundTheWOUT 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bilibili-Evolved 文档 2 | 3 | 使用 [Next.js](https://nextjs.org/) 构建的 [Bilibili-Evolved](https://github.com/the1812/Bilibili-Evolved) 文档 4 | 5 | ### 本地运行 6 | 7 | 1. 克隆项目 8 | 9 | ``` 10 | git clone https://github.com/FoundTheWOUT/bilibili-evolved-doc.git 11 | ``` 12 | 13 | 2. 进入项目目录 14 | ``` 15 | cd bilibili-evolved-doc 16 | ``` 17 | 18 | 3. 安装依赖 19 | 20 | ```bash 21 | pnpm i 22 | ``` 23 | 24 | 4. 启动 25 | ```bash 26 | pnpm dev 27 | ``` 28 | -------------------------------------------------------------------------------- /components/DarkSwitch.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import cn from "classnames"; 3 | import { useTheme } from "./ThemeProvider"; 4 | import { MoonIcon, SunIcon } from "@heroicons/react/solid"; 5 | 6 | const DarkSwitch = ({ className }: { className?: string }) => { 7 | const { setDarkMode, setLightMode } = useTheme(); 8 | 9 | return ( 10 |
16 |
17 | setLightMode()} 20 | /> 21 | setDarkMode()} 24 | /> 25 |
26 |
27 | ); 28 | }; 29 | 30 | export default DarkSwitch; 31 | -------------------------------------------------------------------------------- /components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/solid"; 3 | import type { RouteItem } from "./SideBar"; 4 | import Linkable from "./HOC/Linkable"; 5 | import Image from "next/image"; 6 | import { useMdxPath } from "hooks/useMdxPath"; 7 | 8 | interface FooterProps { 9 | routerTree: RouteItem; 10 | } 11 | 12 | interface RouterMeta { 13 | nextRoute?: RouteItem; 14 | prevRoute?: RouteItem; 15 | route?: RouteItem; 16 | } 17 | 18 | /** 19 | * 深度优先获得当前路由的前后路由 20 | * https://github.com/reactjs/reactjs.org 21 | */ 22 | const searchPreAndNextInRouterTree = ( 23 | tree: RouteItem, 24 | searchPath: string, 25 | ctx: RouterMeta = {} 26 | ): RouterMeta => { 27 | const { routes } = tree; 28 | if (ctx.route && !ctx.nextRoute) { 29 | ctx.nextRoute = tree; 30 | } 31 | 32 | if (tree.path === searchPath) { 33 | ctx.route = tree; 34 | } 35 | 36 | if (!ctx.route && !tree.hide) { 37 | ctx.prevRoute = tree; 38 | } 39 | 40 | if (!routes) { 41 | return ctx; 42 | } 43 | 44 | if (routes) { 45 | routes.forEach((route) => { 46 | searchPreAndNextInRouterTree(route, searchPath, ctx); 47 | }); 48 | } 49 | return ctx; 50 | }; 51 | 52 | export default function Footer({ routerTree }: FooterProps) { 53 | const pathname = useMdxPath(); 54 | const { nextRoute, prevRoute } = searchPreAndNextInRouterTree( 55 | routerTree, 56 | pathname 57 | ); 58 | 59 | return ( 60 | <> 61 |
62 | {prevRoute && ( 63 |
64 | {Linkable( 65 | 66 | 67 | {prevRoute.title} 68 | , 69 | prevRoute 70 | )} 71 |
72 | )} 73 | {nextRoute && ( 74 |
75 | {Linkable( 76 | 77 | {nextRoute.title} 78 | 79 | , 80 | nextRoute 81 | )} 82 |
83 | )} 84 |
85 | {/* footer */} 86 | 111 | 112 | ); 113 | } 114 | -------------------------------------------------------------------------------- /components/HOC/Linkable.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import React, { forwardRef } from "react"; 3 | import { RouteItem } from "components/SideBar"; 4 | 5 | // 检测路由是否有 path 属性,有则使用 Link 包裹,没有则返回原始 JSX 6 | const Linkable = (component: any, route: RouteItem) => { 7 | return route.path ? ( 8 | 9 | {component} 10 | 11 | ) : ( 12 | component 13 | ); 14 | }; 15 | 16 | export default Linkable; 17 | -------------------------------------------------------------------------------- /components/Heading.tsx: -------------------------------------------------------------------------------- 1 | import cn from "classnames"; 2 | import GithubSlugger from "github-slugger"; 3 | import { LinkIcon } from "@heroicons/react/solid"; 4 | 5 | const parseElementToSting = (node: JSX.Element | string): string => { 6 | if (typeof node === "string") return node; 7 | return parseElementToSting(node.props.children); 8 | }; 9 | 10 | const createHeaderLink = (children: JSX.Element) => { 11 | let slugger = new GithubSlugger(); 12 | slugger.reset(); 13 | const { children: content } = children.props; 14 | 15 | let title: string; 16 | if (Array.isArray(content)) { 17 | title = content.map(parseElementToSting).join(" "); 18 | } else { 19 | title = parseElementToSting(content); 20 | } 21 | const id = slugger.slug(title); 22 | 23 | return ( 24 |
25 | {children} 26 | 31 | 32 | 33 |
34 | ); 35 | }; 36 | 37 | export const H1 = ({ className, ...props }: JSX.IntrinsicElements["h1"]) => 38 | createHeaderLink( 39 |

46 | ); 47 | 48 | export const H2 = ({ className, ...props }: JSX.IntrinsicElements["h2"]) => 49 | createHeaderLink( 50 |

57 | ); 58 | 59 | export const H3 = ({ className, ...props }: JSX.IntrinsicElements["h3"]) => 60 | createHeaderLink( 61 |

68 | ); 69 | 70 | export const H4 = ({ className, ...props }: JSX.IntrinsicElements["h4"]) => 71 | createHeaderLink( 72 |

79 | ); 80 | -------------------------------------------------------------------------------- /components/Loading.tsx: -------------------------------------------------------------------------------- 1 | const Loading = ({ className }: { className?: string }) => ( 2 | 8 | 16 | 21 | 22 | ); 23 | export default Loading; 24 | -------------------------------------------------------------------------------- /components/MDXComponents.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | // eslint-disable-next-line jsx-a11y/alt-text 3 | 4 | import * as React from "react"; 5 | import { H1, H2, H3, H4 } from "./Heading"; 6 | import "highlight.js/styles/github-dark.css"; 7 | import { Components } from "@mdx-js/react/lib"; 8 | import { ClipboardIcon, ClipboardCheckIcon } from "@heroicons/react/solid"; 9 | import Link from "next/link"; 10 | // import SyntaxHighlighter from "react-syntax-highlighter"; 11 | 12 | const P = (p: JSX.IntrinsicElements["p"]) => ( 13 |

14 | ); 15 | 16 | const Strong = (strong: JSX.IntrinsicElements["strong"]) => ( 17 | 18 | ); 19 | 20 | const OL = (p: JSX.IntrinsicElements["ol"]) => ( 21 |

    22 | ); 23 | const LI = (p: JSX.IntrinsicElements["li"]) => ( 24 |
  1. 25 | ); 26 | const UL = (p: JSX.IntrinsicElements["ul"]) => ( 27 |
      28 | ); 29 | 30 | const Divider = () => ( 31 |
      32 | ); 33 | 34 | // const Gotcha = ({ children }: { children: React.ReactNode }) => ( 35 | // {children} 36 | // ); 37 | // const Note = ({ children }: { children: React.ReactNode }) => ( 38 | // {children} 39 | // ); 40 | 41 | const Blockquote = ({ 42 | children, 43 | ...props 44 | }: JSX.IntrinsicElements["blockquote"]) => { 45 | return ( 46 | <> 47 |
      51 | {children} 52 |
      53 | 54 | ); 55 | }; 56 | 57 | const Pre = ({ children }: JSX.IntrinsicElements["pre"]) => { 58 | const plainText = (children as any)?.[1]?.props?.children; 59 | const [checked, setChecked] = React.useState(false); 60 | const handleClipboard = (text: string) => { 61 | navigator.clipboard.writeText(text); 62 | // TODO: notify. 63 | }; 64 | 65 | return ( 66 |
       67 |       {checked ? (
       68 |          {
       71 |             handleClipboard(plainText);
       72 |           }}
       73 |         />
       74 |       ) : (
       75 |          {
       78 |             handleClipboard(plainText);
       79 |             setChecked(true);
       80 |           }}
       81 |         />
       82 |       )}
       83 |       {children}
       84 |     
      85 | ); 86 | }; 87 | 88 | const InlineCode = ({ ...props }: JSX.IntrinsicElements["code"]) => { 89 | return ( 90 | 94 | ); 95 | }; 96 | 97 | export const MDXComponents: Components = { 98 | p: P, 99 | strong: Strong, 100 | blockquote: Blockquote, 101 | ol: OL, 102 | ul: UL, 103 | li: LI, 104 | h1: H1, 105 | h2: H2, 106 | h3: H3, 107 | h4: H4, 108 | hr: Divider, 109 | code: InlineCode, 110 | pre: Pre, 111 | // table 112 | table: ({ ...props }) => ( 113 | 117 | ), 118 | th: ({ ...props }) => ( 119 |
      123 | ), 124 | td: ({ ...props }) => ( 125 | 126 | ), 127 | a: ({ href, ...props }) => { 128 | if (!href?.startsWith("http")) { 129 | return ( 130 | 131 | 132 | 133 | ); 134 | } 135 | return ( 136 | 143 | ); 144 | }, 145 | img: ({ ...props }) => { 146 | return ( 147 | 148 | 149 | 150 | ); 151 | }, 152 | }; 153 | -------------------------------------------------------------------------------- /components/MDXWrapper.tsx: -------------------------------------------------------------------------------- 1 | import Toc from "./Toc"; 2 | import Footer from "./Footer"; 3 | import Head from "next/head"; 4 | import type { TocHeader } from "./Toc"; 5 | import React, { PropsWithChildren } from "react"; 6 | 7 | export interface RemarkHeading { 8 | id: string; 9 | depth: number; 10 | title: string; 11 | type: string; 12 | } 13 | 14 | export interface FrontMatter {} 15 | 16 | const MDXFrontMatter = React.createContext({}); 17 | 18 | export default function MDXWrapper({ 19 | headings, 20 | router, 21 | meta, 22 | children, 23 | }: PropsWithChildren<{ 24 | headings: RemarkHeading[]; 25 | meta: FrontMatter; 26 | router: any; 27 | }>) { 28 | const headers: TocHeader[] = headings.map((header) => ({ 29 | url: `#${header.id}`, 30 | depth: header.depth - 1, 31 | text: header.title, 32 | })); 33 | 34 | return ( 35 | <> 36 | 37 | bilibili-evolved-doc 38 | 39 | 40 | 41 |
      42 | {/* context */} 43 |
      44 |
      45 | {children} 46 |
      47 |
      48 |
      49 | 50 |
      51 |
      52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /components/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { MenuAlt2Icon } from "@heroicons/react/solid"; 3 | import DarkSwitch from "./DarkSwitch"; 4 | import Title from "./Title"; 5 | import { SidebarContext } from "./SideBar"; 6 | 7 | const NavBar = () => { 8 | const { showSidebar, hidden, hideSidebar } = React.useContext(SidebarContext); 9 | return ( 10 |
      11 |
      12 |
      13 | (hidden ? showSidebar() : hideSidebar())} 16 | /> 17 |
      18 | 19 | <div className="flex w-14 justify-center"> 20 | <DarkSwitch /> 21 | </div> 22 | </div> 23 | </div> 24 | ); 25 | }; 26 | 27 | export default NavBar; 28 | -------------------------------------------------------------------------------- /components/SideBar/DocType.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | import cn from "classnames"; 4 | import { useRouter } from "next/router"; 5 | 6 | const DocType = ({ ...props }) => { 7 | const { 8 | query: { slug }, 9 | } = useRouter(); 10 | 11 | const style = (type: string) => { 12 | return cn("z-50 flex-1 text-center rounded-md cursor-pointer", { 13 | "bg-sky-200 dark:bg-sky-700 text-sky-700 dark:text-sky-200 font-bold": 14 | slug && slug.includes(type), 15 | }); 16 | }; 17 | 18 | return ( 19 | <div {...props}> 20 | <Link href="/docs/user" legacyBehavior> 21 | <a className={style("user")}>用户手册</a> 22 | </Link> 23 | <Link href="/docs/developer" legacyBehavior> 24 | <a className={style("developer")}>开发文档</a> 25 | </Link> 26 | </div> 27 | ); 28 | }; 29 | 30 | export default DocType; 31 | -------------------------------------------------------------------------------- /components/SideBar/Search.tsx: -------------------------------------------------------------------------------- 1 | import { SearchIcon } from "@heroicons/react/solid"; 2 | import { useDocSearchKeyboardEvents, DocSearchModal } from "@docsearch/react"; 3 | import { useState, useCallback, useRef } from "react"; 4 | import { createPortal } from "react-dom"; 5 | import config from "constant/config"; 6 | 7 | const Search = ({ ...props }) => { 8 | const [isModelShow, setModelShow] = useState(false); 9 | const searchButtonRef = useRef(null); 10 | 11 | const onOpen = useCallback( 12 | function onOpen() { 13 | setModelShow(true); 14 | }, 15 | [setModelShow] 16 | ); 17 | 18 | const onClose = useCallback( 19 | function onClose() { 20 | setModelShow(false); 21 | }, 22 | [setModelShow] 23 | ); 24 | 25 | useDocSearchKeyboardEvents({ 26 | isOpen: isModelShow, 27 | onClose, 28 | onOpen, 29 | searchButtonRef, 30 | }); 31 | 32 | return ( 33 | <> 34 | <div {...props}> 35 | <button 36 | ref={searchButtonRef} 37 | type="button" 38 | className="flex w-full items-center rounded-sm bg-stone-100 p-2 text-gray-400 ring-sky-200 focus:ring-3 dark:bg-stone-700 dark:text-stone-400 dark:ring-sky-700" 39 | onClick={onOpen} 40 | > 41 | <SearchIcon className="mx-2 h-5 w-5 text-stone-500 dark:text-stone-300" /> 42 | 搜索 43 | <kbd className="DocSearch-Button-Key ml-auto">/</kbd> 44 | </button> 45 | </div> 46 | {isModelShow && 47 | createPortal( 48 | <DocSearchModal 49 | apiKey={config.algolia.apiKey} 50 | appId={config.algolia.appId} 51 | indexName={config.algolia.indexName} 52 | initialScrollY={window.scrollY} 53 | onClose={onClose} 54 | transformItems={(items: any[]) => { 55 | return items.map((item) => { 56 | const url = new URL(item.url); 57 | return { 58 | ...item, 59 | url: item.url.replace(url.origin, "").replace("#__next", ""), 60 | }; 61 | }); 62 | }} 63 | />, 64 | document.body 65 | )} 66 | </> 67 | ); 68 | }; 69 | 70 | export default Search; 71 | -------------------------------------------------------------------------------- /components/SideBar/SideBarItem.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { RouteItem } from "."; 3 | import cn from "classnames"; 4 | import Link, { LinkProps } from "next/link"; 5 | 6 | interface SideBarItemProps { 7 | route: RouteItem; 8 | selected: string; 9 | level: number; 10 | } 11 | 12 | interface ItemProps extends LinkProps { 13 | title: string; 14 | level: number; 15 | selected?: boolean; 16 | selectable?: boolean; 17 | } 18 | 19 | const StyledLink = ({ 20 | title, 21 | level, 22 | selected, 23 | selectable = true, 24 | ...rest 25 | }: ItemProps) => { 26 | return ( 27 | <Link 28 | className={cn("my-1 block rounded-sm px-4 py-2 transition dark:text-white", { 29 | "hover:bg-sky-100 dark:hover:bg-opacity-25": selectable && !selected, 30 | "font-bold": level === 0, 31 | "ml-2": level === 1, 32 | "ml-4": level === 2, 33 | "bg-sky-200 text-sky-700 dark:bg-sky-700 dark:text-sky-200": selected, 34 | })} 35 | {...rest} 36 | > 37 | {title} 38 | </Link> 39 | ); 40 | }; 41 | 42 | const SideBarItem = ({ route, selected, level }: SideBarItemProps) => { 43 | return ( 44 | <> 45 | {route.path && ( 46 | <StyledLink 47 | key={route.title} 48 | href={route.path} 49 | title={route.title} 50 | level={level} 51 | selected={selected === route.path} 52 | /> 53 | )} 54 | </> 55 | ); 56 | // return Linkable( 57 | // <Item 58 | // title={route.title} 59 | // level={level} 60 | // selected={selected === route.path} 61 | // />, 62 | // route 63 | // ); 64 | }; 65 | 66 | export default SideBarItem; 67 | -------------------------------------------------------------------------------- /components/SideBar/SideBarTree.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { RouteItem } from "."; 3 | import SideBarItem from "./SideBarItem"; 4 | 5 | type SideBarTreeProps = JSX.IntrinsicElements["ul"] & { 6 | routerTree: RouteItem; 7 | curPath: string; 8 | level: number; 9 | }; 10 | 11 | const SideBarTree = ({ 12 | routerTree, 13 | curPath, 14 | level, 15 | ...props 16 | }: SideBarTreeProps) => ( 17 | <ul {...props}> 18 | {routerTree.routes && 19 | routerTree.routes.map((route) => { 20 | const { title } = route; 21 | 22 | if (route.routes) { 23 | return ( 24 | <li key={title}> 25 | <SideBarItem route={route} selected={curPath} level={level} /> 26 | <SideBarTree 27 | routerTree={route} 28 | curPath={curPath} 29 | level={level + 1} 30 | /> 31 | </li> 32 | ); 33 | } 34 | return ( 35 | <li key={title}> 36 | <SideBarItem route={route} selected={curPath} level={level} /> 37 | </li> 38 | ); 39 | })} 40 | </ul> 41 | ); 42 | 43 | export default SideBarTree; 44 | -------------------------------------------------------------------------------- /components/SideBar/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { PropsWithChildren, useState, useEffect } from "react"; 2 | import SideBarTree from "./SideBarTree"; 3 | import DocType from "./DocType"; 4 | import DarkSwitch from "components/DarkSwitch"; 5 | import Title from "components/Title"; 6 | import cn from "classnames"; 7 | import Search from "./Search"; 8 | import { useMdxPath } from "hooks/useMdxPath"; 9 | import { useRouter } from "next/router"; 10 | 11 | export interface RouteItem { 12 | title: string; 13 | path?: string; 14 | routes?: RouteItem[]; 15 | hide?: boolean; 16 | } 17 | 18 | interface SideBarProps { 19 | routerTree: RouteItem; 20 | } 21 | 22 | export const SidebarContext = React.createContext({ 23 | hidden: false, 24 | hideSidebar: () => {}, 25 | showSidebar: () => {}, 26 | }); 27 | 28 | export const SideBar = ({ routerTree }: SideBarProps) => { 29 | const curPath = useMdxPath(); 30 | const { hidden, hideSidebar } = React.useContext(SidebarContext); 31 | 32 | return ( 33 | <div> 34 | <div 35 | className={cn( 36 | "fixed left-0 right-0 z-30 h-screen backdrop-blur-sm lg:hidden", 37 | { 38 | hidden, 39 | } 40 | )} 41 | onClick={() => hideSidebar()} 42 | ></div> 43 | 44 | <aside 45 | className={cn( 46 | "fixed top-0 z-40 flex h-screen w-80 transform flex-col border-r bg-white transition-all dark:bg-stone-900 lg:visible lg:border-none lg:bg-transparent dark:lg:bg-transparent", 47 | { 48 | "-translate-x-full opacity-0 lg:-translate-x-0 lg:opacity-100": 49 | hidden, 50 | } 51 | )} 52 | > 53 | <Title className="invisible flex items-center p-4 lg:visible" /> 54 | 55 | <div className="flex h-8 items-center px-4 dark:text-white"> 56 | <DocType className="relative flex flex-1 rounded-md bg-stone-100 p-1 dark:bg-stone-700" /> 57 | <DarkSwitch className="hidden lg:flex" /> 58 | </div> 59 | 60 | <Search className="p-4" /> 61 | 62 | {/* route */} 63 | <SideBarTree 64 | className="flex flex-col px-4" 65 | routerTree={routerTree} 66 | curPath={curPath} 67 | level={0} 68 | /> 69 | </aside> 70 | </div> 71 | ); 72 | }; 73 | 74 | export const SidebarProvider = ({ children }: PropsWithChildren<{}>) => { 75 | const router = useRouter(); 76 | const [hidden, setHidden] = useState(true); 77 | useEffect(() => { 78 | setHidden(true); 79 | }, [router.query.slug]); 80 | return ( 81 | <SidebarContext.Provider 82 | value={{ 83 | hidden: hidden, 84 | hideSidebar: () => setHidden(true), 85 | showSidebar: () => setHidden(false), 86 | }} 87 | > 88 | {children} 89 | </SidebarContext.Provider> 90 | ); 91 | }; 92 | -------------------------------------------------------------------------------- /components/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { 4 | PropsWithChildren, 5 | useState, 6 | useContext, 7 | useEffect, 8 | } from "react"; 9 | 10 | type ThemeProps = PropsWithChildren<{ 11 | className?: string; 12 | }>; 13 | 14 | declare global { 15 | interface Window { 16 | __set_theme_dark: () => void; 17 | __set_theme_light: () => void; 18 | } 19 | } 20 | 21 | export enum Themes { 22 | DARK = "dark", 23 | LIGHT = "light", 24 | } 25 | 26 | export const ThemeContext = React.createContext({ 27 | setDarkMode: () => {}, 28 | setLightMode: () => {}, 29 | theme: Themes.LIGHT, 30 | }); 31 | 32 | const STORAGE_THEME_KEY = "bv-doc-theme"; 33 | 34 | export const ThemeProvider = ({ children }: ThemeProps) => { 35 | const [theme, setTheme] = useState(() => { 36 | return (process as any).browser 37 | ? (localStorage.getItem(STORAGE_THEME_KEY) as Themes) 38 | : Themes.LIGHT; 39 | }); 40 | 41 | function setHtmlMode(theme: Themes) { 42 | Object.values(Themes) 43 | .filter((v) => v !== theme) 44 | .forEach((v) => document.documentElement.classList.remove(v)); 45 | 46 | document.documentElement.classList.add(theme); 47 | } 48 | 49 | function setThemeMode(theme: Themes) { 50 | setTheme(theme); 51 | setHtmlMode(theme); 52 | localStorage.setItem(STORAGE_THEME_KEY, theme); 53 | } 54 | 55 | useEffect(() => { 56 | window.__set_theme_dark = () => setThemeMode(Themes.DARK); 57 | window.__set_theme_light = () => setThemeMode(Themes.LIGHT); 58 | }); 59 | 60 | return ( 61 | <ThemeContext.Provider 62 | value={{ 63 | theme, 64 | setDarkMode: () => setThemeMode(Themes.DARK), 65 | setLightMode: () => setThemeMode(Themes.LIGHT), 66 | }} 67 | > 68 | {children} 69 | </ThemeContext.Provider> 70 | ); 71 | }; 72 | 73 | export const useTheme = () => { 74 | const context = useContext(ThemeContext); 75 | if (context === undefined) { 76 | throw new Error("useTheme must be used within a ThemeProvider"); 77 | } 78 | 79 | return context; 80 | }; 81 | -------------------------------------------------------------------------------- /components/Title.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | import Image from "next/image"; 4 | 5 | const Title = ({ ...props }) => { 6 | return ( 7 | <div {...props}> 8 | <Link href="/" legacyBehavior> 9 | <a className="flex items-center justify-center"> 10 | <Image 11 | className="cursor-pointer" 12 | src="/icon.svg" 13 | alt="Bilibili-Evolved" 14 | width={30} 15 | height={30} 16 | /> 17 | </a> 18 | </Link> 19 | <span className="font-bold dark:text-white">BiliBili-Evolved 指南</span> 20 | </div> 21 | ); 22 | }; 23 | 24 | export default Title; 25 | -------------------------------------------------------------------------------- /components/Toc.tsx: -------------------------------------------------------------------------------- 1 | import classNames from "classnames"; 2 | import React, { useEffect, useState } from "react"; 3 | import GithubSlugger from "github-slugger"; 4 | 5 | export type TocHeader = { 6 | url: string; 7 | depth: number; 8 | text: string; 9 | }; 10 | 11 | interface TocProps { 12 | headers: TocHeader[]; 13 | } 14 | 15 | export default function Toc({ headers }: TocProps) { 16 | const [activeHeader, setActiveHeader] = useState(0); 17 | 18 | useEffect(() => { 19 | const handleDocumentScroll = () => { 20 | const pageHeight = document.body.scrollHeight; 21 | const scrollPosition = window.scrollY + window.innerHeight; 22 | 23 | if (scrollPosition >= 0 && pageHeight - scrollPosition <= 75) { 24 | // Scrolled to bottom of page. 25 | setActiveHeader(headers.length - 1); 26 | return; 27 | } 28 | 29 | const headersAnchors = Array.from( 30 | document.getElementsByClassName("anchor") 31 | ); 32 | 33 | let index = -1; 34 | while (index < headersAnchors.length - 1) { 35 | const headerAnchor = headersAnchors[index + 1]; 36 | const { top } = headerAnchor.getBoundingClientRect(); 37 | 38 | if (top >= 50) { 39 | break; 40 | } 41 | index += 1; 42 | } 43 | 44 | setActiveHeader(Math.max(index, 0)); 45 | }; 46 | 47 | document.addEventListener("scroll", handleDocumentScroll); 48 | return () => { 49 | document.removeEventListener("scroll", handleDocumentScroll); 50 | }; 51 | }, [headers.length]); 52 | 53 | return ( 54 | <div className="ml-4 hidden text-sm xl:block"> 55 | {/* <div className="fixed right-0 top-0 pt-8 w-72 text-sm hidden lg:block"> */} 56 | <div className="sticky top-8 max-h-[75vh] w-64 overflow-y-auto dark:text-white"> 57 | {headers.map((header, i) => { 58 | let slugger = new GithubSlugger(); 59 | slugger.reset(); 60 | const id = slugger.slug(header.text); 61 | 62 | return ( 63 | <div 64 | className={classNames( 65 | "cursor-pointer px-4 pb-2 pl-1 hover:text-sky-600", 66 | { 67 | "text-sky-600 dark:text-sky-300": activeHeader === i, 68 | } 69 | )} 70 | key={i} 71 | > 72 | <a 73 | href={`#${id}`} 74 | className={ 75 | header.depth >= 2 76 | ? "pl-2" 77 | : header.depth === 1 78 | ? "pl-1" 79 | : "pl-0" 80 | } 81 | > 82 | {header.text} 83 | </a> 84 | </div> 85 | ); 86 | })} 87 | </div> 88 | </div> 89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /constant/config.ts: -------------------------------------------------------------------------------- 1 | const config = { 2 | algolia: { 3 | appId: "F8ZAJXLRJK", 4 | apiKey: "96806be3b37023bb03c0e73e77c03b20", 5 | indexName: "bilibili-evolved-doc", 6 | }, 7 | }; 8 | 9 | export const IS_SERVER = typeof window === "undefined"; 10 | 11 | export default config; 12 | -------------------------------------------------------------------------------- /constant/sidebar-developer.ts: -------------------------------------------------------------------------------- 1 | // import { MDXProvider } from "@mdx-js/react"; 2 | // import dynamic from "next/dynamic"; 3 | 4 | // eslint-disable-next-line import/no-anonymous-default-export 5 | export default { 6 | title: "Developer Manual", 7 | hide: true, 8 | routes: [ 9 | { 10 | title: "开始", 11 | path: "/docs/developer", 12 | // Comp: dynamic(() => import("/docs/developer/index.mdx"), { 13 | // suspense: true, 14 | // }) as typeof MDXProvider, 15 | }, 16 | { 17 | title: "环境搭建", 18 | path: "/docs/developer/environment", 19 | // Comp: dynamic(() => import("/docs/developer/environment.md"), { 20 | // suspense: true, 21 | // }) as typeof MDXProvider, 22 | }, 23 | { 24 | title: "组件(插件)开发", 25 | path: "/docs/developer/component", 26 | // Comp: dynamic(() => import("/docs/developer/component.md"), { 27 | // suspense: true, 28 | // }) as typeof MDXProvider, 29 | }, 30 | { 31 | title: "API", 32 | path: "/docs/developer/api", 33 | // Comp: dynamic(() => import("/docs/developer/api.md"), { 34 | // suspense: true, 35 | // }) as typeof MDXProvider, 36 | }, 37 | { 38 | title: "CDN", 39 | path: "/docs/developer/cdn", 40 | // Comp: dynamic(() => import("/docs/developer/cdn.md"), { 41 | // suspense: true, 42 | // }) as typeof MDXProvider, 43 | }, 44 | ], 45 | }; 46 | -------------------------------------------------------------------------------- /constant/sidebar-user.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: "User Manual", 3 | hide: true, 4 | routes: [ 5 | { 6 | title: "😊 欢迎使用", 7 | path: "/docs/user", 8 | }, 9 | { 10 | title: "📦 安装", 11 | path: "/docs/user/install", 12 | }, 13 | { 14 | title: "⚙ 设置", 15 | path: "/docs/user/settings", 16 | }, 17 | { 18 | title: "📚 组件(插件)", 19 | path: "/docs/user/features", 20 | }, 21 | { 22 | title: "📅 更新日志", 23 | path: "/docs/user/changelog", 24 | }, 25 | { 26 | title: "❓ Q&A", 27 | path: "/docs/user/Q&A", 28 | }, 29 | { 30 | title: "从 v1 迁移", 31 | path: "/docs/user/migrate-v1", 32 | }, 33 | { 34 | title: "🍞 投喂", 35 | path: "/docs/user/donate", 36 | }, 37 | ], 38 | }; 39 | -------------------------------------------------------------------------------- /docs/developer/api.md: -------------------------------------------------------------------------------- 1 | ### 全局 2 | 3 | 全局变量, 无需 `import` 就可以直接使用. (Tampermonkey API 这里不再列出了, 可根据代码提示使用) 4 | 5 | - `Vue`: Vue 库的主对象, 在创建 `.vue` 组件时, 其中的 `<script>` 可以直接使用 `Vue.extend()` 6 | 7 | > 出于历史原因, 项目中用的还是 Vue 2, 由于其糟糕的 TypeScript 支持, 在 VS Code + Vetur 的环境下浏览 `.vue` 文件可能会报各种奇奇怪怪的类型错误, 无视就好. (类型是否正确以 `yarn type` 的结果为准) 8 | 9 | - `lodash`: 包含所有 Lodash 库提供的方法 10 | - `dq` / `dqa`: `document.querySelector` 和 `document.querySelectorAll` 的简写, `dqa` 会返回真实数组 11 | 12 | > 在 `bwp-video` 出现后, 这两个查询函数还会自动将对 `video` 的查询扩展到 `bwp-video` 13 | 14 | - `none`: 什么都不做的空函数 15 | - `componentTags`: 预置的一些组件标签, 实现 `ComponentMetadata.tags` 时常用 16 | 17 | ### 本体 API 18 | 19 | 仅介绍常用 API, 其他可以翻阅源代码了解, 代码中均带有文档. 20 | 21 | - `core/ajax`: 封装 `XMLHttpRequest` 常见操作, 以及 `bilibiliApi` 作为 b 站 API 的响应处理 (`fetch` 可以直接用) 22 | - `core/download`: `DownloadPackage` 类封装了下载文件的逻辑, 可以添加单个或多个文件, 多个文件时自动打包为 `.zip`, 确定是单个文件时也可以直接调用静态方法 `single` 23 | - `core/file-picker`: 打开系统的文件选择对话框 24 | - `core/life-cycle`: 控制在网页的不同生命周期执行代码 25 | - `core/meta`: 获取脚本的自身元数据, 如名称, 版本等/ 26 | - `core/observer`: 封装各种监视器, 包括元素增删, 进入/离开视图, 大小变化, 以及当前页面视频的更换 27 | - `core/spin-query`: 轮询器, 等待页面上异步加载的元素, 也可以自定义查询条件 28 | - `core/runtime-library`: 运行时代码库, 目前支持导入 protobufjs 和 JSZip 使用 29 | - `core/user-info`: 获取当前用户的登录信息 30 | - `core/version`: 版本比较 31 | - `core/settings`: 脚本设置 API, 可监听设置变更, 获取组件设置, 判断组件是否开启等 32 | - `core/toast`: 通知 API, 能够在左下角显示通知 33 | - `core/utils`: 工具集, 包含各种常量, 格式化函数, 排序工具, 标题获取, 日志等 34 | - [`theWorld`](https://zh.moegirl.org.cn/THE_WORLD): ~~并没有什么用的API,~~与 `debugger` 几乎等价,不同的是,它是一个函数,参数是`time: number`,代表`time`毫秒后时停。 35 | 36 | ### 组件 API 37 | 38 | - `components/types`: 组件相关接口定义 39 | - `components/styled-components`: 包含样式的组件 entry 简化包装函数 40 | - `components/user-component`: 用户组件的安装/卸载 API 41 | - `components/description`: 获取组件或插件的描述, 会自动匹配语言并插入作者标记, 支持 HTML, Markdown 和纯文本形式 42 | - `components/feeds/`: 43 | - `api`: 动态相关 API 封装, 获取动态流, 对每一个动态卡片执行操作等 44 | - `BangumiCard.vue`: 番剧卡片 45 | - `VideoCard.vue`: 视频卡片 46 | - `ColumnCard.vue`: 专栏卡片 47 | - `notify`: 获取未读动态数目, 最后阅读的动态 ID 等 48 | - `components/video/`: 49 | - `ass-utils`: ASS 字幕工具函数 50 | - `player-light`: 控制播放器开关灯 51 | - `video-danmaku`: 对每一条视频弹幕执行操作 52 | - `video-info`: 根据 av 号查询视频信息 53 | - `video-quality`: 视频清晰度列表 54 | - `video-context-menu`: 向播放器右键菜单插入内容 55 | - `video-control-bar`: 向播放器控制栏插入内容 56 | - `watchlater`: 稍后再看列表获取, 添加/移除稍后再看等 57 | - `components/live/`: 58 | - `live-control-bar`: 向直播控制栏插入内容 59 | - `live-socket`: 直播间弹幕的 WebSocket 封装 60 | - `components/utils/`: 61 | - `comment-apis`: 对每条评论执行操作 62 | - `categories/data`: 主站分区数据 63 | - `components/i18n/machine-translator/MachineTranslator.vue`: 机器翻译器组件 64 | - `components/switch-options`: SwitchOptions API, 方便创建含有多个独立开闭的子选项的功能 65 | - `components/launch-bar/LaunchBar.vue`: 搜索栏组件 66 | 67 | ### 插件 API 68 | 69 | - `plugins/data`: 数据注入 API 70 | 71 | 持有数据的一方使用特定的 `key` 调用 `registerAndGetData` 注册并获取数据, 在这之前或之后所有的 `addData` 调用都能修改这些数据. 两方通过使用相同的 `key` 交换数据, 可以有效降低代码耦合. 72 | 73 | 例如, 自定义顶栏功能的顶栏元素列表就是合适的数据, 夜间模式的插件可以向其中添加一个快速切换夜间模式的开关, 而不需要修改自定义顶栏的代码. 74 | 75 | > 有时候需要分开数据的注册和获取, 可以分别调用 `registerData` 和 `getData` 76 | 77 | > 从设计上考虑好你的数据是否适合此 API, `addData` 做出的数据修改是单向累加式的, 不能够撤销. 78 | 79 | - `plugins/hook`: 钩子函数 API 80 | 81 | 对于某种特定时机发生的事件, 可以调用 `getHook` 允许其他地方注入钩子提高扩展性, 其他地方可以嗲用 `addHook` 进行注入. 82 | 83 | 例如, 自动更新器在组件管理面板注入了钩子, 在新组件安装时记录安装来源, 实现自动更新. 组件管理面板没有任何更新相关代码. 84 | 85 | - `plugins/style`: 提供简单的自定义样式增删 API (复杂样式请考虑组件或者 Stylus) 86 | 87 | ### UI 88 | 89 | 所有的 UI 组件建议使用 `'@/ui'` 导入, 大部分看名字就知道是什么, 这里只列几个特殊的. 90 | 91 | - `ui/icon/VIcon.vue`: 图标组件, 自带 MDI 图标集, b 站图标集, 支持自定义图标注入 92 | - `ui/_common.scss`: 一些通用的 Sass Mixin, 在代码中可以直接使用 `@import "common"` 导入 93 | - `ui/_markdown.scss`: 提供一个 Markdown Mixin, 导入这个 Mixin 的地方将获得为各种 HTML 元素适配的 Markdown 样式. 在代码中可以直接使用 `@import "markdown"` 导入 94 | - `ui/ScrollTrigger.vue`: 进入视图时触发事件, 通常用于实现无限滚动 95 | - `ui/VEmpty.vue`: 表示无数据, 界面可被插件更改 96 | - `ui/VLoading.vue`: 表示数据加载中, 界面可被插件更改 97 | -------------------------------------------------------------------------------- /docs/developer/cdn.md: -------------------------------------------------------------------------------- 1 | <remote src="https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/doc/cdn.md"/> 2 | -------------------------------------------------------------------------------- /docs/developer/component.md: -------------------------------------------------------------------------------- 1 | <remote src="https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/CONTRIBUTING.md" section="修改"/> 2 | 3 | <remote src="https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/CONTRIBUTING.md" section="新增"/> 4 | -------------------------------------------------------------------------------- /docs/developer/environment.md: -------------------------------------------------------------------------------- 1 | <remote src="https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/CONTRIBUTING.md" section="搭建开发环境"/> 2 | -------------------------------------------------------------------------------- /docs/developer/index.mdx: -------------------------------------------------------------------------------- 1 | # 代码贡献指南 2 | 3 | 请先观看 4 | 5 | 1. [搭建开发环境](/docs/developer/environment) 6 | 2. [组件(插件)开发](/docs/developer/component) 7 | 8 | ## 代码风格检查 9 | 10 | 项目中含有 ESLint, 不通过 ESLint 可能无法发起 Pull Request. 配置基于 `airbnb-base`, `typescript-eslint/recommended`, `vue/recommended` 修改而来, 几个比较特殊的规则如下: 11 | 12 | ### 强制性 13 | 14 | - 除了 Vue 单文件组件, 禁止使用 `export default`, 所有导出必须命名. 15 | - 参数列表, 数组, 对象等的尾随逗号必须添加. 16 | - 如非必要禁止在末尾添加分号. 17 | - 任何控制流语句主体必须添加大括号. 18 | 19 | ### 建议性 20 | 21 | - 一行代码最长 80 字符. 22 | - 不需要使用 `this` 特性的函数, 均使用箭头函数. 23 | 24 | ## 提交 commit 25 | 26 | 仅提交源代码上的修改即可, 不建议把 dist 文件夹里的产物也提交, 否则容易在 PR 时产生冲突. 27 | 28 | commit message 只需写明改动点, 中英文随意, 也不强求类似 [commit-lint](https://github.com/conventional-changelog/commitlint) 的格式. 29 | 30 | ## 发起 PR (合并请求) 31 | 32 | 将你的分支往主仓库的 `preview` 分支合并就行. 33 | 34 | ## 自行保留 35 | 36 | 你可以选择不将功能代码合并到主仓库, 因此也没有 ESLint 的限制. PR 时仅添加指向你的仓库中的组件信息即可, 具体来说, 是在 `registry/lib/docs/third-party.ts` 中, 往对应数组中添加你的功能的相关信息, 当然别忘了把 `owner` 设为你的 GitHub 用户名. 37 | -------------------------------------------------------------------------------- /docs/user/Q&A.mdx: -------------------------------------------------------------------------------- 1 | ### 左侧没有设置等按钮 / 页面没有任何变化 2 | 3 | 检查脚本是否安装完整, 可以在首页中使用不同的安装源重新安装. 4 | 检查浏览器是否支持, 此脚本仅支持高版本的原版 Chrome, Edge (Chromium 内核), Firefox, Safari. 5 | 6 | ### 能不能自动播放视频 7 | 8 | b 站自带这个功能, 在播放器设置里. 9 | 10 | ### 为什么不能给弹幕点赞/举报 11 | 12 | 脚本功能需要修改视频区域的浮层, 弹幕的点赞/举报层会把这些全部挡住, 所以就禁止了弹幕点赞/举报, 详见 #966 13 | 14 | ### 下载 zip 后安装的版本不对 15 | 16 | 本项目从来没有从 zip 安装的说法, 只有在[安装](/docs/user/install)中的安装链接才是正确的. 17 | 18 | ### 下载视频相关 19 | 20 | #### 报错 code -412 21 | 22 | 短时间下载太多被 b 站暂时封 IP 了, 等一段时间就行 23 | 24 | #### 如何使用 IDM 导出得到的 .ef2 文件 25 | 26 | IDM 任务 > 导入 > 从 IDM 导出文件导入 27 | 28 | #### 港澳台番剧没有下载按钮 29 | 30 | 如果以您的账号权限无法观看某些视频(地区限制, 大会员专享等), 就算使用了类似解除 B 站区域限制的脚本也是无法下载的, 除非您有对应节点的梯子. 31 | 32 | #### 番剧没有弹幕 33 | 34 | 部分用户在同时开启此脚本和解除 B 站区域限制脚本后, 会出现番剧没有弹幕的问题, 目前没有解决方法. 35 | 36 | #### 如何得到 MP4 格式 37 | 38 | 下载时选择 DASH 格式(注意只有新一点的视频才支持, 老视频只有 FLV), 得到视频和音频文件后用 [ffmpeg](https://ffmpeg.org/) 合并, 合并方法: 39 | 40 | ```shell 41 | ffmpeg -i [视频] -i [音频] -c copy 合并后视频.mp4 42 | ``` 43 | 44 | 详见 [#183](https://github.com/the1812/Bilibili-Evolved/issues/183) 45 | 46 | FLV 转 MP4 在这里也有很详细的介绍: [#1472](https://github.com/the1812/Bilibili-Evolved/discussions/1472) 47 | 48 | ### 无法跳过番剧承包榜 49 | 50 | 脚本提供的功能叫跳过充电鸣谢, 特指 UP 主视频后面的充电鸣谢榜, 跟番剧里的是两回事. 番剧承包榜目前无法跳过. 51 | 52 | ### 夜间下依然在使用细滚动条 53 | 54 | 即使你关闭里设置中的使用细滚动条, 夜间模式中也会强制启用, 才能显示出符合夜间模式配色的滚动条. 55 | 56 | ### 为啥不兼容 AdGuard 57 | 58 | AdGuard 不支持复杂对象 / 数组存储, 脚本必须要存储这些数据. 59 | 60 | > 另外两个兼容的 monkey 会自动使用 JSON 包装 61 | -------------------------------------------------------------------------------- /docs/user/changelog.md: -------------------------------------------------------------------------------- 1 | <remote src="https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/CHANGELOG.md"/> 2 | -------------------------------------------------------------------------------- /docs/user/donate.mdx: -------------------------------------------------------------------------------- 1 | # 投喂区 2 | 3 | 如果您很喜欢这个项目, 欢迎打赏, 金额随意. 您的支持是我们的动力(=・ω・=) 4 | 5 | 如需匿名或使用昵称请在备注中写明哦. 6 | 7 | ## 爱发电 8 | 9 | > 之前的支付宝付款码有点问题, 现已弃用 (不必担心, 转账均已收到), 在爱发电中仍然可以选择支付宝作为付款方式. 10 | 11 | <a href="https://afdian.net/@the1812" target="_blank"> 12 | <img alt="爱发电" src="https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/images/compressed/afdian.jpg?raw=true" width="400" height="593"/> 13 | </a> 14 | 15 | 16 | ## 微信 17 | 18 | <img alt="微信" src="https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/images/compressed/wechat.jpg?raw=true" width="400" height="400"/> 19 | 20 | # 历史 21 | 22 | ## 爱发电 23 | 24 | https://afdian.net/@the1812?tab=sponsor 25 | 26 | <remote src="https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/doc/donate.md" section="微信/支付宝收款" /> 27 | -------------------------------------------------------------------------------- /docs/user/features.mdx: -------------------------------------------------------------------------------- 1 | <remote src="https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/doc/features/pack/pack.md" /> 2 | 3 | <remote src="https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/doc/features/features.md" /> 4 | -------------------------------------------------------------------------------- /docs/user/index.mdx: -------------------------------------------------------------------------------- 1 | <div className="flex justify-center"> 2 | <img 3 | id="Bilibili-Evolved" 4 | width="500" 5 | alt="Bilibili Evolved" 6 | src="/bilibili-evolved-wide-color.svg" 7 | /> 8 | </div> 9 | <div align="center" className="dark:text-white"> 10 | 「 强大的哔哩哔哩增强脚本 」 11 | </div> 12 | 13 | ### 请先观看 14 | 15 | 1. [安装](/docs/user/install) 16 | 2. [设置](/docs/user/settings) 17 | 18 | ## 兼容性 19 | 20 | ### 脚本管理器 21 | 22 | #### [Tampermonkey](https://tampermonkey.net/) / [Violentmonkey](https://violentmonkey.github.io/) 23 | 24 | 兼容, 但在较旧的浏览器中 Violentmonkey 可能无法运行此脚本. 25 | 26 | > Tampermonkey 版本不能是 4.14.6147 / 6148 27 | 28 | #### [Greasemonkey](https://www.greasespot.net/) 29 | 30 | 不兼容. 31 | 32 | #### [AdGuard](https://adguard.com/zh_cn/adguard-windows/overview.html) 33 | 34 | 未测试. 35 | 36 | ### 浏览器 37 | 38 | 支持**最新版** Chrome, Edge (Chromium 内核), Firefox, 不保证脚本能在["套壳类浏览器"](https://www.jianshu.com/p/67d790a8f221)或者较长时间没更新的浏览器中完美运行. 39 | 40 | ### 文案翻译贡献者 41 | 42 | - [PleiadeSubaru](https://github.com/Etherrrr) 43 | - [Lets-Halloween](https://github.com/Lets-Halloween) 44 | - Joshua ふみひる 45 | 46 | ## 参与项目 47 | 48 | 欢迎参考[代码贡献指南](/docs/developer)来为项目添砖加瓦~ 49 | 50 | ## 隐私声明 51 | 52 | 本脚本以及本仓库中提供的组件/插件, 是完全匿名的. 用户数据的使用均在本地完成, 不会存储到任何服务器, 也不会有所谓的"用户体验改善计划"来收集统计数据. 53 | 54 | 但是, 任何组件/插件都对用户数据有着完全的访问能力, 对于其他来源(非本仓库提供)的组件/插件, 请自行甄别其安全性. 55 | 56 | ## 第三方开源组件 57 | 58 | 👍 感谢这些组件帮助我们极大地提升了开发效率. 59 | 60 | - [Vue.js](https://cn.vuejs.org/index.html) 61 | - [JSZip](https://stuk.github.io/jszip/) 62 | - [bilibili API collect](https://github.com/SocialSisterYi/bilibili-API-collect) 63 | - [popper-core](https://github.com/popperjs/popper-core) 64 | - [Tippy.js](https://github.com/atomiks/tippyjs) 65 | - [Sortable](https://github.com/SortableJS/Sortable) 66 | - [color](https://github.com/Qix-/color) 67 | - [Lodash](https://lodash.com/) 68 | - [marked](https://github.com/markedjs/marked) 69 | - [MDI](https://materialdesignicons.com) 70 | 71 | ## 相关推荐 72 | 73 | 这些脚本/插件同样能够改善您在 B 站的体验, 相同的功能将不会整合到 Bilibili Evolved, 但会尽可能地适配 74 | 75 | ### bilibili 网页端添加 APP 首页推荐 76 | 77 | 作者: [indefined](https://github.com/indefined) 78 | 79 | - [GitHub](https://github.com/indefined/UserScripts/tree/master/bilibiliHome) 80 | - [GreasyFork](https://greasyfork.org/zh-CN/scripts/368446-bilibili%E7%BD%91%E9%A1%B5%E7%AB%AF%E6%B7%BB%E5%8A%A0app%E9%A6%96%E9%A1%B5%E6%8E%A8%E8%8D%90) 81 | 82 | ### pakku.js 哔哩哔哩弹幕过滤器 83 | 84 | 作者: [xmcp](https://github.com/xmcp) 85 | 86 | - [主页](https://s.xmcp.ml/pakkujs/) 87 | - [GitHub](https://github.com/xmcp/pakku.js) 88 | 89 | ### BLTH - Bilibili Live Tasks Helper 90 | 91 | 作者: [andywang425](https://github.com/andywang425) 92 | 93 | - [GitHub](https://github.com/andywang425/BLTH) 94 | - [GreasyFork](https://greasyfork.org/zh-CN/scripts/406048-b%E7%AB%99%E7%9B%B4%E6%92%AD%E9%97%B4%E6%8C%82%E6%9C%BA%E5%8A%A9%E6%89%8B) 95 | 96 | --- 97 | 98 | **喜欢的话就点个 ⭐Star 吧(°∀°)ノ** 99 | 100 | **或者也可以考虑[捐助](/docs/user/donate)支持一下哦(`・ω・´)** 101 | 102 | ## 我(the1812)写的其他一些玩意 103 | 104 | ### [Touhou Tagger](https://github.com/the1812/Touhou-Tagger) 105 | 106 | ☯ 从 [THBWiki](https://thwiki.cc/) 自动填写东方 Project 同人音乐 CD 曲目信息 107 | 108 | ### [Malware Patch](https://github.com/the1812/Malware-Patch) 109 | 110 | 阻止中国流氓软件的管理员授权 111 | 112 | ### [dizzylab auto theme](https://github.com/the1812/dizzylab-auto-theme) 113 | 114 | [dizzylab](https://www.dizzylab.net/) 自适应 Stylus 主题, 跟随系统亮/暗设定 115 | 116 | ### [Steam CSS](https://github.com/the1812/SteamCSS) 117 | 118 | 为 [Steam](https://store.steampowered.com/) 的库和内置浏览器插入一段自定义的 CSS, 用于更换字体等 119 | -------------------------------------------------------------------------------- /docs/user/install.mdx: -------------------------------------------------------------------------------- 1 | ## 安装 2 | 3 | 需要浏览器装有 [Tampermonkey](https://tampermonkey.net/) 或 [Violentmonkey](https://violentmonkey.github.io/) 插件, 下方表格中挑一个链接安装. 4 | 5 | **注意事项** 6 | 7 | - 做好觉悟, 脚本开启后不能使用弹幕点赞和举报, 全景视频不能用鼠标拖拽视角(只能用键盘操作), 对性能也有较大影响. 8 | - 新版本一旦正式发布, 就不再对旧版本做任何技术支持. 9 | - 使用外部网站的链接时(如将下载任务发送到自己的服务器 / 使用链接安装组件等)可能会提示"脚本试图访问跨域资源", 请选择"始终允许". 10 | - 可能无法很好地适应窄屏幕, 请尽量以 1400 x 800 以上的逻辑分辨率使用此脚本. 11 | - **⚠ 网络需要确保能够连接 cdn.jsdelivr.net, 如果使用 GitHub 更新源, 也需要能够连接 raw.githubusercontent.com (这两个目前都有墙** 12 | 13 | | | 更新延迟 | 正式版 | 预览版 | 14 | | ------ | -------- | ------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | 15 | | GitHub | 小于 1h | [安装](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/dist/bilibili-evolved.user.js) | [安装](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/dist/bilibili-evolved.preview.user.js) | 16 | -------------------------------------------------------------------------------- /docs/user/migrate-v1.mdx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | ## 从 v1 (旧版) 迁移设置 4 | 5 | 在 v2 的组件库中, 提供了 `v1 设置迁移` 的功能, 可以将 v1 中使用的功能自动安装到 v2 中来. 6 | 7 | 本文档将说明迁移的步骤, 从还没有安装 v2 的脚本开始: 8 | 9 | ### 1. 导出 v1 设置 10 | 11 | 打开脚本设置面板, 在搜索框旁边的菜单中选择导出设置, 得到文件 `settings.json` 12 | 13 | ![image](https://user-images.githubusercontent.com/26504152/127772561-96410454-45eb-4a08-9223-6a97c1850abb.png) 14 | 15 | ### 2. 安装 v2 脚本 16 | 17 | 删除 v1 脚本, 前往 [安装](/docs/user/install) 安装 v2 脚本. 18 | 19 | ### 3. 安装 v1 设置迁移 20 | 21 | 刷新之前的 b 站页面, v2 脚本应该已经生效, 设置面板依然默认在页面左侧中央. 22 | 23 | 打开设置, 点左下角的组件管理, 然后进在线仓库, 搜索 `v1 设置迁移` 并安装, 安装完成后刷新页面. 24 | 25 | ![image](https://user-images.githubusercontent.com/26504152/139682841-86d045a4-08dd-401e-8726-3d0dea298eec.png) 26 | 27 | ### 4. 开始迁移 28 | 29 | 再次打开设置面板, 进入左下角的 `关于`, 上一步成功的话这里会多出一个 `导入 v1 设置` 的按钮. 30 | 31 | ![image](https://user-images.githubusercontent.com/26504152/127772852-4594d882-9ce7-4059-a34c-366725ce36ff.png) 32 | 33 | 点击这个按钮, 选择一开始导出的 `settings.json` 文件, 脚本将会开始下载所有在 v1 中开启的功能并安装. 34 | 35 | 等待安装完成后, 刷新页面就完成迁移了. 36 | -------------------------------------------------------------------------------- /docs/user/settings.mdx: -------------------------------------------------------------------------------- 1 | ## 设置 2 | 3 | 脚本启用后, 在网页左侧中央会有功能面板和设置面板的入口. 功能面板中包含适用于当前页面的一些功能入口, 设置面板中可以管理组件的开启/关闭, 修改组件选项, 以及安装/卸载组件和插件等. 4 | 5 | ![sidebar](/images/side-panel.jpg) 6 | 7 | ![settings-panel](/images/settings-panel.jpg) 8 | 9 | 全新安装的脚本实际上没有任何功能, 你可以通过多种方式添加功能: 10 | 11 | 打开设置面板, 进入左下的组件/插件/样式管理, 会有批量, 浏览, 和在线三个按钮, 以及下面还有个输入框可以用链接添加. 12 | 13 | - `批量`: 可以粘贴多个功能的在线链接并一次性安装. 14 | - `浏览`: 载入本地的功能文件. 15 | - `在线`: 打开在线仓库窗口. 16 | 17 | ![manage-panel](/images/manage-panel.jpg) 18 | 19 | > 用链接安装需要对应文件的直链, GitHub Raw 或 jsDelivr 都可以. 批量安装时逐行粘贴链接即可. 20 | 21 | 如果你曾经使用过 v1 版, 可以利用 `v1 设置迁移` 组件将旧设置导入到 v2 中, 该工具将自动把里面开启的设置对应的组件下载并安装, 使用方法可以参考[这个文档](/docs/user/migrate-v1) 22 | 23 | 最后, 在关于面板中, 可以查看脚本的详细版本号, 以及进行设置导入 / 导出等快捷操作. 24 | 25 | ## 推荐配置 26 | 27 | - 操作系统: 64-bit Windows 10+ / macOS 10.15+ 28 | - 分辨率: 2K+ / 192ppi 29 | - 浏览器: Chrome 84+ / Firefox 80+ / Edge 84+ / Safari 14.1+ 30 | - 处理器: 8 代 Intel Core i7+ / Zen 3 架构 AMD Ryzen 5+ 31 | - 内存: 8GB 32 | - 脚本管理器: Tampermonkey 4.14 / Violentmonkey 2.13 33 | - 显卡: GeForce GTX 660 / Radeon HD 7870 34 | - 网络: 10MB/s 35 | -------------------------------------------------------------------------------- /hooks/useMdxPath.ts: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | 3 | interface Ops { 4 | endWithIndex: boolean; 5 | } 6 | 7 | export const useMdxPath = (options?: Ops) => { 8 | const { endWithIndex } = Object.assign({ endWithIndex: false }, options); 9 | let { 10 | query: { slug }, 11 | } = useRouter(); 12 | if (!endWithIndex) { 13 | slug = (slug as string[]).filter((v) => v !== "index"); 14 | } 15 | if (!Array.isArray(slug)) return ""; 16 | if (slug.length == 1 && endWithIndex) return slug.concat(["index"]).join("/"); 17 | return "/docs/" + slug.join("/"); 18 | }; 19 | -------------------------------------------------------------------------------- /hooks/useRouterTree.ts: -------------------------------------------------------------------------------- 1 | import sideBarUser from "constant/sidebar-user"; 2 | import sideBarDeveloper from "constant/sidebar-developer"; 3 | import { useRouter } from "next/router"; 4 | 5 | export const useRouterTree = () => { 6 | const { asPath } = useRouter(); 7 | const tree = asPath.startsWith("/docs/user") ? sideBarUser : sideBarDeveloper; 8 | return { 9 | tree, 10 | getNode(path: string) { 11 | for (let route of tree.routes) { 12 | if (route.path === path) { 13 | return route; 14 | } 15 | } 16 | }, 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /loaders/replace-remote-content/__test__/Comp.tsx: -------------------------------------------------------------------------------- 1 | export default function () { 2 | return <div>Hi there</div>; 3 | } 4 | -------------------------------------------------------------------------------- /loaders/replace-remote-content/__test__/__snapshots__/replace-remote-content.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`Unit Test replace RemoteContent > cache hit 1`] = ` 4 | "### Sugaring time begins in February and lasts for two to six weeks 5 | 6 | well into March. Weather permitting, Grandad always started tapping on Washington's Birthday. The sugaring season, as we called it, required thawing days and freezing nights to allow the sap to flow. Rain spoiled the sap and wind dried up the tap holes so it was a fair-weather industry. Grandad hand-drilled his tap holes using a No. 8 wood bit, drilling about 1/2 inch deep into the vein of a sugar maple tree. Into this hole, a wood spile would then be tapped and after drilling another, a pail was hung to collect the sap as it steadily dripped from the spile. The wooden spiles were about eight inches in length and three-quarters of an inch in diameter. Ours were made from elderberry as that scrub has a soft center that could be cleaned out to make a tube. Our maple grove was along the brook and the edge of the field about a have mile up from the house. There were approximately 200 sugar maples large enough to tap on Grandad's farm and the scars on the trunks of many of the older trees indicated the number of years they had been supplying sap. 7 | 8 | ## Foo 9 | 10 | Bar 11 | 12 | ### Sugaring time begins in February and lasts for two to six weeks 13 | 14 | well into March. Weather permitting, Grandad always started tapping on Washington's Birthday. The sugaring season, as we called it, required thawing days and freezing nights to allow the sap to flow. Rain spoiled the sap and wind dried up the tap holes so it was a fair-weather industry. Grandad hand-drilled his tap holes using a No. 8 wood bit, drilling about 1/2 inch deep into the vein of a sugar maple tree. Into this hole, a wood spile would then be tapped and after drilling another, a pail was hung to collect the sap as it steadily dripped from the spile. The wooden spiles were about eight inches in length and three-quarters of an inch in diameter. Ours were made from elderberry as that scrub has a soft center that could be cleaned out to make a tube. Our maple grove was along the brook and the edge of the field about a have mile up from the house. There were approximately 200 sugar maples large enough to tap on Grandad's farm and the scars on the trunks of many of the older trees indicated the number of years they had been supplying sap. 15 | 16 | ### Sugaring time begins in February and lasts for two to six weeks 17 | 18 | well into March. Weather permitting, Grandad always started tapping on Washington's Birthday. The sugaring season, as we called it, required thawing days and freezing nights to allow the sap to flow. Rain spoiled the sap and wind dried up the tap holes so it was a fair-weather industry. Grandad hand-drilled his tap holes using a No. 8 wood bit, drilling about 1/2 inch deep into the vein of a sugar maple tree. Into this hole, a wood spile would then be tapped and after drilling another, a pail was hung to collect the sap as it steadily dripped from the spile. The wooden spiles were about eight inches in length and three-quarters of an inch in diameter. Ours were made from elderberry as that scrub has a soft center that could be cleaned out to make a tube. Our maple grove was along the brook and the edge of the field about a have mile up from the house. There were approximately 200 sugar maples large enough to tap on Grandad's farm and the scars on the trunks of many of the older trees indicated the number of years they had been supplying sap. 19 | " 20 | `; 21 | 22 | exports[`Unit Test replace RemoteContent > can fetch remote content 1`] = ` 23 | "# Sugaring As I Remember It 24 | 25 | ## The end of winter once again brings with it the memories of maple sugaring 26 | 27 | Back when the Highlands area was mostly farms and woods rather than housing developments as it is today, the manufacturing of maple sugar products was an excellent source of extra income for the local farmers. 28 | 29 | ## The tapping of the sugar maple was an important lesson learned by our forefathers from the Native Americans 30 | 31 | as it provided many Colonial families their only source of sugar. Up until a few years ago, generations of the Post family had been making maple sugar on farms originally acquired by Peter and Abram Post in 1815 in the shadow of Bearfort Mountain. This was an art passed down from father to son unto the present generation. With the break up of the farms into home lots, all that remains are memories and, luckily, some of the equipment used through the years. 32 | 33 | ### Sugaring time begins in February and lasts for two to six weeks 34 | 35 | well into March. Weather permitting, Grandad always started tapping on Washington's Birthday. The sugaring season, as we called it, required thawing days and freezing nights to allow the sap to flow. Rain spoiled the sap and wind dried up the tap holes so it was a fair-weather industry. Grandad hand-drilled his tap holes using a No. 8 wood bit, drilling about 1/2 inch deep into the vein of a sugar maple tree. Into this hole, a wood spile would then be tapped and after drilling another, a pail was hung to collect the sap as it steadily dripped from the spile. The wooden spiles were about eight inches in length and three-quarters of an inch in diameter. Ours were made from elderberry as that scrub has a soft center that could be cleaned out to make a tube. Our maple grove was along the brook and the edge of the field about a have mile up from the house. There were approximately 200 sugar maples large enough to tap on Grandad's farm and the scars on the trunks of many of the older trees indicated the number of years they had been supplying sap. 36 | 37 | ## The saphouse was a small 38 | 39 | low building built into the ledge along Clinton Brook. Its walls were of stone and, having caught fire at least once, the building, I remember, had a tin roof so only the door was of wood. At the rear against the stone ledge was the fireplace with a dirt hearth across the whole width of the building. A wooden pole above the hearth rested at each end on the low side walls, blackened by age and smoke. From this, on wrought iron pot hooks, hung three large iron kettles and one brass kettle. The roof above the kettles was hinged like two large doors to let the smoke out and keep the rain from coming in. On calm days these doors could be laid completely open, back on the roof. On windy days they could b regulated so that the smoke did not blow back into the saphouse and into the boiling sap. To prevent sparks from going into the hemlock woods, a screen of tin was placed around the rear on the top of the ledge. Clinton Brook flowing by the door not only provided a picturesque setting but was essential for scouring the kettles, strainers and utensils after each boiling. 40 | 41 | ## Twice a day, and some days more often 42 | 43 | the sap was collected from each tree and carried up to the saphouse. The kettles were filled and, with a good hot fire beneath, thirty pails of sap would boil down to about eight quarts of syrup in four to five hours. As the sap boiled down and thickened it was removed from the fire and strained and then placed in the brass kettle over a low fire for the last bit of boiling down to syrup. The syrup, as it came from the saphouse, was usually not quite thick enough so it was Grandma's job to boil it a little longer on the back of the old wood-burning kitchen stove. For a thicker cream sugar (we called it "slush") and for sugar cakes, additional boiling and beating were required with all hands taking a turn. The beating cooled the heavy syrup and as it cooled it became granular and almost white. Our reward was the pan to "lick" out and hot syrup poured out on the snow for taffy. 44 | 45 | ### My brothers, sisters, and I, as youngsters 46 | 47 | were at the saphouse every chance we got during the sugaring season, playing in the brook and jumping from rock to rock. If you slipped and got wet, the fire was always handy. When we grew older we helped to gather the sap, split wood, and tend the fire. After Grandad's death, I did the whole bit myself for a few years. 48 | 49 | ## I especially remember the late thirties and early forties when the Newark Sunday News would do a picture story on Grandad and the sugaring. 50 | 51 | This brought out the sightseers and every weekend there would be groups of people around asking questions and taking pictures. Some came back each year and many came to buy the maple sugar products. The more adventurous ones would hike with us to the falls to view the old Clinton Iron Furnace. Many a time we would pack a lunch and stay all day at the saphouse, when school was not in session, and the weather was nice. But usually, it was up the hill to the "big house" with Grandad for dinner and the noonday farm chores; then back to the saphouse till supper time. If it was a season with a good run of sap, Grandad would boil some down at night. That was the best time of all, sitting in the saphouse with the fire for warmth and the old man's stories of long ago to pass the hours. 52 | 53 | <div> 54 | 55 | 56 | 57 | 58 | 59 | \`\`\`shell 60 | echo hi 61 | \`\`\` 62 | " 63 | `; 64 | 65 | exports[`Unit Test replace RemoteContent > don't break mdast structure 1`] = ` 66 | "✨新增 67 | 68 | <details> 69 | <summary>正式版获得 v2.3.1 ~ v2.3.3 预览版的功能</summary> 70 | 71 | * \`清爽首页\` 的热门视频支持显示弹幕数量. 72 | * \`极简首页\` 初版已完成. 73 | * 脚本的更新源配置默认值更换为 \`GitHub\`, 并添加了 \`AltCdn\`, 表示开发者自定义的其他 CDN 源. \`jsDelivr\` 将会删除. 74 | * 在 \`GitHub\` 更新源下, MDI 图标库更换使用 GitHub Pages. 75 | * 新增组件 \`UP 主黑名单\` by [snowraincloud](https://github.com/snowraincloud). (PR #3537) 76 | * 在无限滚动的场景下, 点击那个 \`加载中\` 的标识 (ScrollTrigger) 可以手动触发加载下一页. 在遇到没有自动加载下一页的情况时会比较有用. 77 | * \`下载视频\` 支持 \`flac\` 音源. (#3497) 78 | * \`自定义顶栏\` 支持设置顶栏的高度, 设置为 64px 即为原版顶栏的高度. (#3136) 79 | 80 | </details> 81 | 82 | * \`删除视频弹窗\` 支持 3.x 播放器的 \`关联视频\` 和 \`评分\` 弹窗. (#3545) 83 | * \`展开弹幕列表\` 支持设置 \`最大弹幕数量\`, 超过此数量不进行展开, 避免展开时卡死页面. (#2972) 84 | * \`图片批量导出\` 在导出动态的图片时, 可以指定 \`originalUser\` 作为被转发用户名. (#1208) 85 | * 新增插件 \`自定义顶栏 - 频道\`, 为自定义顶栏添加一个频道入口. (#3258) 86 | * \`自定义顶栏\` 的历史弹窗中支持暂停/继续记录历史. (#3303) 87 | 88 | ✨新增 89 | 90 | <details> 91 | <summary>正式版获得 v2.1.9 ~ v2.1.10 预览版的功能</summary> 92 | 93 | * 设置面板移动了搜索框的位置, 添加了检查更新和卸载组件的快捷按钮. (PR #3279 by [FoundTheWOUT](https://github.com/FoundTheWOUT)) 94 | * \`自定义顶栏\` 支持硬核 LV6 会员的图标显示. (#3203) 95 | * \`动态过滤器\` 支持屏蔽发送动态的面板. (#2447) 96 | * 新增插件 \`下载视频 - 手动输入\`, 可以手动输入 av / BV 号来进行下载. (#3227) 97 | * Toast 消息能够显示关闭时间的倒计时进度, 且鼠标进入时停止倒计时. (#3204) 98 | 99 | </details> 100 | " 101 | `; 102 | 103 | exports[`Unit Test replace RemoteContent > fetch remote content, and select two section 1`] = ` 104 | "### Sugaring time begins in February and lasts for two to six weeks 105 | 106 | well into March. Weather permitting, Grandad always started tapping on Washington's Birthday. The sugaring season, as we called it, required thawing days and freezing nights to allow the sap to flow. Rain spoiled the sap and wind dried up the tap holes so it was a fair-weather industry. Grandad hand-drilled his tap holes using a No. 8 wood bit, drilling about 1/2 inch deep into the vein of a sugar maple tree. Into this hole, a wood spile would then be tapped and after drilling another, a pail was hung to collect the sap as it steadily dripped from the spile. The wooden spiles were about eight inches in length and three-quarters of an inch in diameter. Ours were made from elderberry as that scrub has a soft center that could be cleaned out to make a tube. Our maple grove was along the brook and the edge of the field about a have mile up from the house. There were approximately 200 sugar maples large enough to tap on Grandad's farm and the scars on the trunks of many of the older trees indicated the number of years they had been supplying sap. 107 | 108 | ## Twice a day, and some days more often 109 | 110 | the sap was collected from each tree and carried up to the saphouse. The kettles were filled and, with a good hot fire beneath, thirty pails of sap would boil down to about eight quarts of syrup in four to five hours. As the sap boiled down and thickened it was removed from the fire and strained and then placed in the brass kettle over a low fire for the last bit of boiling down to syrup. The syrup, as it came from the saphouse, was usually not quite thick enough so it was Grandma's job to boil it a little longer on the back of the old wood-burning kitchen stove. For a thicker cream sugar (we called it "slush") and for sugar cakes, additional boiling and beating were required with all hands taking a turn. The beating cooled the heavy syrup and as it cooled it became granular and almost white. Our reward was the pan to "lick" out and hot syrup poured out on the snow for taffy. 111 | 112 | ### My brothers, sisters, and I, as youngsters 113 | 114 | were at the saphouse every chance we got during the sugaring season, playing in the brook and jumping from rock to rock. If you slipped and got wet, the fire was always handy. When we grew older we helped to gather the sap, split wood, and tend the fire. After Grandad's death, I did the whole bit myself for a few years. 115 | " 116 | `; 117 | 118 | exports[`Unit Test replace RemoteContent > html image self close 1`] = ` 119 | "<div> 120 | <img height="400" src="https://user-images.githubusercontent.com/26504152/242905984-895cb72c-b344-40c3-91a0-2a6b20d5f783.png" /> 121 | <img height="400" src="https://user-images.githubusercontent.com/26504152/242905640-cbc948f1-734e-46f2-96a7-d57787b7cf47.png" /> 122 | </div> 123 | " 124 | `; 125 | 126 | exports[`Unit Test replace RemoteContent > html tag 1`] = ` 127 | "# 合集包 128 | 129 | 合集包提供了批量的功能安装链接, 方便一次性安装大量功能. 130 | 131 | #### 简洁至上 132 | 133 | 简化各种多余界面元素, 专注于内容本身. 134 | 135 | 包含以下功能: 136 | 删除广告, 删除直播水印, 删除视频弹窗, 禁用特殊弹幕样式, 简化评论区, 简化直播间, 简化首页, 自动收起直播侧栏, 隐藏视频推荐, 隐藏直播推荐, 隐藏视频标题层, 自动隐藏侧栏 137 | 138 | <details> 139 | <summary><strong>jsDelivr Stable</strong></summary> 140 | https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@v2/registry/dist/components/utils/remove-promotions.js 141 | https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@v2/registry/dist/components/live/remove-watermark.js 142 | https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@v2/registry/dist/components/video/player/remove-popup.js 143 | https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@v2/registry/dist/components/style/special-danmaku.js 144 | https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@v2/registry/dist/components/style/simplify/comments.js 145 | https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@v2/registry/dist/components/style/simplify/live.js 146 | https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@v2/registry/dist/components/style/simplify/home.js 147 | https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@v2/registry/dist/components/live/side-bar.js 148 | https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@v2/registry/dist/components/style/hide/video/related-videos.js 149 | https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@v2/registry/dist/components/style/hide/video/recommended-live.js 150 | https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@v2/registry/dist/components/style/hide/video/top-mask.js 151 | https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@v2/registry/dist/components/style/auto-hide-sidebar.js 152 | </details> 153 | 154 | ### Sugaring time begins in February and lasts for two to six weeks 155 | 156 | well into March. Weather permitting, Grandad always started tapping on Washington's Birthday. The sugaring season, as we called it, required thawing days and freezing nights to allow the sap to flow. Rain spoiled the sap and wind dried up the tap holes so it was a fair-weather industry. Grandad hand-drilled his tap holes using a No. 8 wood bit, drilling about 1/2 inch deep into the vein of a sugar maple tree. Into this hole, a wood spile would then be tapped and after drilling another, a pail was hung to collect the sap as it steadily dripped from the spile. The wooden spiles were about eight inches in length and three-quarters of an inch in diameter. Ours were made from elderberry as that scrub has a soft center that could be cleaned out to make a tube. Our maple grove was along the brook and the edge of the field about a have mile up from the house. There were approximately 200 sugar maples large enough to tap on Grandad's farm and the scars on the trunks of many of the older trees indicated the number of years they had been supplying sap. 157 | " 158 | `; 159 | 160 | exports[`Unit Test replace RemoteContent > should return mdx properly 1`] = ` 161 | "import Comp from "./Comp"; 162 | 163 | <Comp /> 164 | 165 | <Comp>123</Comp> 166 | 167 | ## h2 168 | 169 | body 170 | " 171 | `; 172 | -------------------------------------------------------------------------------- /loaders/replace-remote-content/__test__/changelog.md: -------------------------------------------------------------------------------- 1 | ✨新增 2 | 3 | <details> 4 | <summary>正式版获得 v2.3.1 ~ v2.3.3 预览版的功能</summary> 5 | 6 | - `清爽首页` 的热门视频支持显示弹幕数量. 7 | - `极简首页` 初版已完成. 8 | - 脚本的更新源配置默认值更换为 `GitHub`, 并添加了 `AltCdn`, 表示开发者自定义的其他 CDN 源. `jsDelivr` 将会删除. 9 | - 在 `GitHub` 更新源下, MDI 图标库更换使用 GitHub Pages. 10 | - 新增组件 `UP 主黑名单` by [snowraincloud](https://github.com/snowraincloud). (PR #3537) 11 | - 在无限滚动的场景下, 点击那个 `加载中` 的标识 (ScrollTrigger) 可以手动触发加载下一页. 在遇到没有自动加载下一页的情况时会比较有用. 12 | - `下载视频` 支持 `flac` 音源. (#3497) 13 | - `自定义顶栏` 支持设置顶栏的高度, 设置为 64px 即为原版顶栏的高度. (#3136) 14 | 15 | </details> 16 | 17 | - `删除视频弹窗` 支持 3.x 播放器的 `关联视频` 和 `评分` 弹窗. (#3545) 18 | - `展开弹幕列表` 支持设置 `最大弹幕数量`, 超过此数量不进行展开, 避免展开时卡死页面. (#2972) 19 | - `图片批量导出` 在导出动态的图片时, 可以指定 `originalUser` 作为被转发用户名. (#1208) 20 | - 新增插件 `自定义顶栏 - 频道`, 为自定义顶栏添加一个频道入口. (#3258) 21 | - `自定义顶栏` 的历史弹窗中支持暂停/继续记录历史. (#3303) 22 | 23 | ✨新增 24 | <details> 25 | <summary>正式版获得 v2.1.9 ~ v2.1.10 预览版的功能</summary> 26 | 27 | - 设置面板移动了搜索框的位置, 添加了检查更新和卸载组件的快捷按钮. (PR #3279 by [FoundTheWOUT](https://github.com/FoundTheWOUT)) 28 | - `自定义顶栏` 支持硬核 LV6 会员的图标显示. (#3203) 29 | - `动态过滤器` 支持屏蔽发送动态的面板. (#2447) 30 | - 新增插件 `下载视频 - 手动输入`, 可以手动输入 av / BV 号来进行下载. (#3227) 31 | - Toast 消息能够显示关闭时间的倒计时进度, 且鼠标进入时停止倒计时. (#3204) 32 | 33 | </details> -------------------------------------------------------------------------------- /loaders/replace-remote-content/__test__/example.md: -------------------------------------------------------------------------------- 1 | # Sugaring As I Remember It 2 | 3 | ## The end of winter once again brings with it the memories of maple sugaring 4 | 5 | Back when the Highlands area was mostly farms and woods rather than housing developments as it is today, the manufacturing of maple sugar products was an excellent source of extra income for the local farmers. 6 | 7 | ## The tapping of the sugar maple was an important lesson learned by our forefathers from the Native Americans 8 | 9 | as it provided many Colonial families their only source of sugar. Up until a few years ago, generations of the Post family had been making maple sugar on farms originally acquired by Peter and Abram Post in 1815 in the shadow of Bearfort Mountain. This was an art passed down from father to son unto the present generation. With the break up of the farms into home lots, all that remains are memories and, luckily, some of the equipment used through the years. 10 | 11 | ### Sugaring time begins in February and lasts for two to six weeks 12 | 13 | well into March. Weather permitting, Grandad always started tapping on Washington's Birthday. The sugaring season, as we called it, required thawing days and freezing nights to allow the sap to flow. Rain spoiled the sap and wind dried up the tap holes so it was a fair-weather industry. Grandad hand-drilled his tap holes using a No. 8 wood bit, drilling about 1/2 inch deep into the vein of a sugar maple tree. Into this hole, a wood spile would then be tapped and after drilling another, a pail was hung to collect the sap as it steadily dripped from the spile. The wooden spiles were about eight inches in length and three-quarters of an inch in diameter. Ours were made from elderberry as that scrub has a soft center that could be cleaned out to make a tube. Our maple grove was along the brook and the edge of the field about a have mile up from the house. There were approximately 200 sugar maples large enough to tap on Grandad's farm and the scars on the trunks of many of the older trees indicated the number of years they had been supplying sap. 14 | 15 | ## The saphouse was a small 16 | 17 | low building built into the ledge along Clinton Brook. Its walls were of stone and, having caught fire at least once, the building, I remember, had a tin roof so only the door was of wood. At the rear against the stone ledge was the fireplace with a dirt hearth across the whole width of the building. A wooden pole above the hearth rested at each end on the low side walls, blackened by age and smoke. From this, on wrought iron pot hooks, hung three large iron kettles and one brass kettle. The roof above the kettles was hinged like two large doors to let the smoke out and keep the rain from coming in. On calm days these doors could be laid completely open, back on the roof. On windy days they could b regulated so that the smoke did not blow back into the saphouse and into the boiling sap. To prevent sparks from going into the hemlock woods, a screen of tin was placed around the rear on the top of the ledge. Clinton Brook flowing by the door not only provided a picturesque setting but was essential for scouring the kettles, strainers and utensils after each boiling. 18 | 19 | ## Twice a day, and some days more often 20 | 21 | the sap was collected from each tree and carried up to the saphouse. The kettles were filled and, with a good hot fire beneath, thirty pails of sap would boil down to about eight quarts of syrup in four to five hours. As the sap boiled down and thickened it was removed from the fire and strained and then placed in the brass kettle over a low fire for the last bit of boiling down to syrup. The syrup, as it came from the saphouse, was usually not quite thick enough so it was Grandma's job to boil it a little longer on the back of the old wood-burning kitchen stove. For a thicker cream sugar (we called it "slush") and for sugar cakes, additional boiling and beating were required with all hands taking a turn. The beating cooled the heavy syrup and as it cooled it became granular and almost white. Our reward was the pan to "lick" out and hot syrup poured out on the snow for taffy. 22 | 23 | ### My brothers, sisters, and I, as youngsters 24 | 25 | were at the saphouse every chance we got during the sugaring season, playing in the brook and jumping from rock to rock. If you slipped and got wet, the fire was always handy. When we grew older we helped to gather the sap, split wood, and tend the fire. After Grandad's death, I did the whole bit myself for a few years. 26 | 27 | ## I especially remember the late thirties and early forties when the Newark Sunday News would do a picture story on Grandad and the sugaring. 28 | 29 | This brought out the sightseers and every weekend there would be groups of people around asking questions and taking pictures. Some came back each year and many came to buy the maple sugar products. The more adventurous ones would hike with us to the falls to view the old Clinton Iron Furnace. Many a time we would pack a lunch and stay all day at the saphouse, when school was not in session, and the weather was nice. But usually, it was up the hill to the "big house" with Grandad for dinner and the noonday farm chores; then back to the saphouse till supper time. If it was a season with a good run of sap, Grandad would boil some down at night. That was the best time of all, sitting in the saphouse with the fire for warmth and the old man's stories of long ago to pass the hours. 30 | 31 | <div> 32 | 33 | <style></style> 34 | 35 | <!-- hi! --> 36 | 37 | ```powershell 38 | echo hi 39 | ``` 40 | -------------------------------------------------------------------------------- /loaders/replace-remote-content/__test__/example.mdx: -------------------------------------------------------------------------------- 1 | import Comp from "./Comp"; 2 | 3 | <Comp /> 4 | 5 | <Comp>123</Comp> 6 | 7 | ## h2 8 | 9 | body 10 | -------------------------------------------------------------------------------- /loaders/replace-remote-content/__test__/html-comment.md: -------------------------------------------------------------------------------- 1 | <!--Foo--> 2 | <!-- Bar --> 3 | -------------------------------------------------------------------------------- /loaders/replace-remote-content/__test__/image-self-close.md: -------------------------------------------------------------------------------- 1 | <div> 2 | <img height="400" src="https://user-images.githubusercontent.com/26504152/242905984-895cb72c-b344-40c3-91a0-2a6b20d5f783.png"> 3 | <img height="400" src="https://user-images.githubusercontent.com/26504152/242905640-cbc948f1-734e-46f2-96a7-d57787b7cf47.png"> 4 | </div> 5 | -------------------------------------------------------------------------------- /loaders/replace-remote-content/__test__/replace-remote-content.test.ts: -------------------------------------------------------------------------------- 1 | import { loaderPromise as __replaceFun } from "../main.mjs"; 2 | import { describe, it, expect, beforeAll } from "vitest"; 3 | import { readFile } from "fs/promises"; 4 | import handler from "serve-handler"; 5 | import { createServer } from "http"; 6 | 7 | // first, host 8 | const TEST_PORT = 5500; 9 | beforeAll(async () => { 10 | const server = createServer((request, response) => { 11 | return handler(request, response, { 12 | // ! fix the path 13 | rewrites: [ 14 | { 15 | source: "__test__/:name", 16 | destination: "loaders/replace-remote-content/__test__/:name", 17 | }, 18 | ], 19 | }); 20 | }); 21 | server.listen(TEST_PORT, () => { 22 | console.log(`Running at http://localhost:${TEST_PORT}`); 23 | }); 24 | 25 | return async () => { 26 | server.close(); 27 | }; 28 | }); 29 | 30 | describe("Unit Test replace RemoteContent", () => { 31 | it("can fetch remote content", async () => { 32 | const content = '<remote src="http://127.0.0.1:5500/__test__/example.md"/>'; 33 | 34 | const res = await __replaceFun(content); 35 | expect(res).toMatchSnapshot(); 36 | }); 37 | 38 | it("fetch remote content, and select two section", async () => { 39 | const content = 40 | '<remote src="http://127.0.0.1:5500/__test__/example.md" section="Sugaring time begins in February and lasts for two to six weeks" section-two="Twice a day, and some days more often"/>'; 41 | 42 | const res = await __replaceFun(content); 43 | expect(res).toMatchSnapshot(); 44 | }); 45 | 46 | it("cache hit", async () => { 47 | const content = `<remote src="http://127.0.0.1:5500/__test__/example.md" section="Sugaring time begins in February and lasts for two to six weeks"/> 48 | 49 | ## Foo 50 | Bar 51 | 52 | <remote src="http://127.0.0.1:5500/__test__/example.md" section="Sugaring time begins in February and lasts for two to six weeks"/> 53 | 54 | <remote src="http://127.0.0.1:5500/__test__/example.md" section="Sugaring time begins in February and lasts for two to six weeks"/> 55 | `; 56 | 57 | const res = await __replaceFun(content); 58 | expect(res).toMatchSnapshot(); 59 | }); 60 | 61 | it("html tag", async () => { 62 | const template = `# 合集包 63 | 64 | 合集包提供了批量的功能安装链接, 方便一次性安装大量功能. 65 | 66 | #### 简洁至上 67 | 68 | 简化各种多余界面元素, 专注于内容本身. 69 | 70 | 包含以下功能: 71 | 删除广告, 删除直播水印, 删除视频弹窗, 禁用特殊弹幕样式, 简化评论区, 简化直播间, 简化首页, 自动收起直播侧栏, 隐藏视频推荐, 隐藏直播推荐, 隐藏视频标题层, 自动隐藏侧栏 72 | 73 | <details> 74 | <summary><strong>jsDelivr Stable</strong></summary> 75 | https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@v2/registry/dist/components/utils/remove-promotions.js 76 | https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@v2/registry/dist/components/live/remove-watermark.js 77 | https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@v2/registry/dist/components/video/player/remove-popup.js 78 | https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@v2/registry/dist/components/style/special-danmaku.js 79 | https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@v2/registry/dist/components/style/simplify/comments.js 80 | https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@v2/registry/dist/components/style/simplify/live.js 81 | https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@v2/registry/dist/components/style/simplify/home.js 82 | https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@v2/registry/dist/components/live/side-bar.js 83 | https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@v2/registry/dist/components/style/hide/video/related-videos.js 84 | https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@v2/registry/dist/components/style/hide/video/recommended-live.js 85 | https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@v2/registry/dist/components/style/hide/video/top-mask.js 86 | https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@v2/registry/dist/components/style/auto-hide-sidebar.js 87 | </details> 88 | 89 | <remote src="http://127.0.0.1:5500/__test__/example.md" section="Sugaring time begins in February and lasts for two to six weeks"/> 90 | `; 91 | const res = await __replaceFun(template); 92 | expect(res).toMatchSnapshot(); 93 | }); 94 | 95 | it("should return mdx properly", async () => { 96 | const mdx = ( 97 | await readFile("loaders/replace-remote-content/__test__/example.mdx") 98 | ).toString(); 99 | const res = await __replaceFun(mdx); 100 | expect(res).toMatchSnapshot(); 101 | }); 102 | 103 | it("html image self close", async () => { 104 | const template = `<remote src="http://127.0.0.1:5500/__test__/image-self-close.md"/>`; 105 | const res = await __replaceFun(template); 106 | expect(res).toMatchSnapshot(); 107 | }); 108 | 109 | it("should remove html comment", async () => { 110 | const template = `<remote src="http://127.0.0.1:5500/__test__/html-comment.md"/>`; 111 | const res = await __replaceFun(template); 112 | expect(res).eq("\n\n"); 113 | }); 114 | 115 | it("don't break mdast structure", async () => { 116 | const content = 117 | '<remote src="http://127.0.0.1:5500/__test__/changelog.md"/>'; 118 | 119 | const res = (await __replaceFun(content)) as string; 120 | expect(res.replaceAll("\r", "")).toMatchSnapshot(); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /loaders/replace-remote-content/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (code) { 2 | const callback = this.async(); 3 | const options = this.getOptions(); 4 | // Note that `import()` caches, so this should be fast enough. 5 | import("./main.mjs").then((module) => 6 | module.loader.call(this, code, callback, options) 7 | ); 8 | }; 9 | -------------------------------------------------------------------------------- /loaders/replace-remote-content/main.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * 替换 <remote /> 为 src 里内容 3 | * Author: FoundTheWOUT 4 | * */ 5 | 6 | import { remark } from "remark"; 7 | import { visit } from "unist-util-visit"; 8 | import { toHtml } from "hast-util-to-html"; 9 | import { fromHtml } from "hast-util-from-html"; 10 | import { fromMarkdown } from "mdast-util-from-markdown"; 11 | import { remove } from "unist-util-remove"; 12 | import fetch from "node-fetch"; 13 | 14 | // TODO, check valid jsx. 15 | const isJsxNode = (node) => { 16 | // if is tsx component, should not handle it. 17 | if ( 18 | node.value[0] == "<" && 19 | (/[A-Z]/.test(node.value[1]) || /[A-Z]/.test(node.value[2])) 20 | ) { 21 | return true; 22 | } 23 | return false; 24 | }; 25 | 26 | const normalizeHTML = (tree) => { 27 | tree = remove(tree, { tagName: "style" }); 28 | if (!tree) return; 29 | tree = remove(tree, { type: "comment" }); 30 | if (!tree) return; 31 | return toHtml(tree, { 32 | closeSelfClosing: true, 33 | }); 34 | }; 35 | 36 | const normalizeContentPlugin = () => { 37 | return (tree) => { 38 | visit(tree, (node) => { 39 | if (!node.value) return; 40 | // ! only modify img 41 | if ( 42 | node.type == "html" && 43 | (node.value.includes("<img") || 44 | node.value.includes("<!-") || 45 | node.value.includes("<style")) 46 | ) { 47 | if (isJsxNode(node)) return; 48 | const tree = fromHtml(node.value, { fragment: true }) ?? ""; 49 | node.value = normalizeHTML(tree); 50 | } 51 | if (node.type == "code" && node.lang == "powershell") { 52 | node.lang = "shell"; 53 | } 54 | }); 55 | }; 56 | }; 57 | 58 | const handleSectionSelect = (tree, options) => { 59 | if (options.section.length == 0) return tree; 60 | let depth = -1; // depth != -1 meaning should retained the node. 61 | let index = 0; 62 | while (index < tree.children.length) { 63 | // select the current node 64 | const node = tree.children[index]; 65 | switch (node.type) { 66 | case "heading": 67 | /** 68 | * for example, a md: 69 | * ### Foo <- selected section 70 | * Bar 71 | * ## Jack <- drop content from here 72 | * 19 73 | */ 74 | if (depth !== -1 && node.depth <= depth) { 75 | depth = -1; 76 | } 77 | for (let child of node.children) { 78 | if (options.section.includes(child.value)) { 79 | depth = node.depth; // record the depth 80 | index++; 81 | } else if (depth !== -1) { 82 | index++; 83 | } else { 84 | tree.children.splice(index, 1); 85 | } 86 | } 87 | break; 88 | default: 89 | if (depth == -1) { 90 | // if not collecting item, remove it from children directly. 91 | tree.children.splice(index, 1); 92 | } else { 93 | index++; 94 | } 95 | break; 96 | } 97 | } 98 | return tree; 99 | }; 100 | // TODO: write to local. 101 | const cache = new Map(); 102 | const fetchContentPlugin = (options = {}) => { 103 | const { section, dry } = options; 104 | return async (tree) => { 105 | if (dry && section) { 106 | handleSectionSelect(tree, options); 107 | return; 108 | } 109 | const promises = []; 110 | let preFragmentLength = 0; 111 | // 1. select mdxJsxFlowElement type, and get content from url. 112 | // mdast -> hast for later use 113 | visit(tree, "html", (node, offset, parent) => { 114 | if (isJsxNode(node)) return; 115 | const hast = fromHtml(node.value, { fragment: true }); 116 | if ( 117 | hast.children && 118 | hast.children[0] && 119 | hast.children[0].tagName == "remote" 120 | ) { 121 | const hastNode = hast.children[0]; 122 | let url = null; 123 | let sections = []; 124 | for (let [attrName, value] of Object.entries(hastNode.properties)) { 125 | if (attrName == "src") { 126 | url = value; 127 | } 128 | if (/section/g.test(attrName)) { 129 | sections.push(value); 130 | } 131 | } 132 | if (url) { 133 | promises.push(() => { 134 | console.log("fetching:", url); 135 | const encodedUrl = encodeURI(url); 136 | const cacheContent = cache.get(encodedUrl); 137 | let _p = null; 138 | if (cacheContent) { 139 | console.log("url:", url, "hit cache!"); 140 | _p = Promise.resolve(cacheContent); 141 | } else { 142 | _p = fetch(url) 143 | .then((res) => res.text()) 144 | .then((text) => { 145 | cache.set(encodedUrl, text); 146 | return text; 147 | }); 148 | } 149 | 150 | return _p 151 | .then((text) => { 152 | console.log("compiling markdown to mdast..."); 153 | return fromMarkdown(text); 154 | }) 155 | .then((mdast) => { 156 | console.log("selecting section..."); 157 | return handleSectionSelect(mdast, { section: sections }); 158 | }) 159 | .then((mdast) => { 160 | parent.children.splice( 161 | offset + preFragmentLength, 162 | 1, 163 | ...mdast.children 164 | ); 165 | preFragmentLength += mdast.children.length - 1; 166 | }) 167 | .catch((error) => { 168 | console.log("fetch content error:", error); 169 | }); 170 | }); 171 | } 172 | } 173 | }); 174 | 175 | for (let p of promises) { 176 | try { 177 | await p(); 178 | } catch (error) { 179 | switch (process.env.NODE_ENV) { 180 | case "development": 181 | console.error(error); 182 | default: { 183 | throw Error(error); 184 | process.exit(0); 185 | } 186 | } 187 | } 188 | } 189 | }; 190 | }; 191 | 192 | /** 193 | * 194 | * @param {string} content - .mdx or .md file 195 | * @param {(_:undefined,value:string)=>void} callback 196 | * @param {{dry:boolean}|undefined}options - when passing the dry options, the loader would not find <remote>, but treat it like a file which after fetching content from url. 197 | * @returns 198 | */ 199 | export async function loader(content, callback, options) { 200 | remark() 201 | .use(fetchContentPlugin, options) 202 | .use(normalizeContentPlugin) 203 | .process(content) 204 | .then((res) => { 205 | callback(null, res.toString()); 206 | }); 207 | } 208 | 209 | export const loaderPromise = (template, options) => 210 | new Promise((resolve, reject) => { 211 | loader( 212 | template, 213 | (_, value) => { 214 | resolve(value); 215 | }, 216 | options 217 | ); 218 | }); 219 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// <reference types="next" /> 2 | /// <reference types="next/image-types/global" /> 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | // import rehypeStaticProps from "./plugins/rehype-static-props.mjs"; 2 | 3 | /** @type {import('next').NextConfig} */ 4 | const config = { 5 | pageExtensions: ["tsx", "md", "mdx"], 6 | env: { 7 | buildAt: new Date().toISOString(), 8 | }, 9 | // reactStrictMode: true, 10 | images: { 11 | dangerouslyAllowSVG: true, 12 | remotePatterns: [ 13 | { 14 | protocol: "https", 15 | hostname: "contrib.rocks", 16 | }, 17 | ], 18 | domains: [ 19 | "user-images.githubusercontent.com", 20 | "cdn.jsdelivr.net", 21 | "avatars.githubusercontent.com", 22 | ], 23 | }, 24 | }; 25 | 26 | export default config; 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bilibili-evolved-doc", 3 | "private": true, 4 | "scripts": { 5 | "dev": "next dev", 6 | "raw-build": "next build", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "test": "vitest", 11 | "format": "prettier --write \"{pages,components,styles}/**/*.{tsx,css}\"" 12 | }, 13 | "dependencies": { 14 | "@algolia/client-search": "^4.12.0", 15 | "@docsearch/css": "^3.2.1", 16 | "@docsearch/react": "^3.2.1", 17 | "@headlessui/react": "^1.7.4", 18 | "@heroicons/react": "^1.0.5", 19 | "@mdx-js/loader": "^2.1.4", 20 | "@mdx-js/react": "^2.1.4", 21 | "classnames": "^2.3.1", 22 | "github-slugger": "^1.4.0", 23 | "highlight.js": "^11.4.0", 24 | "js-yaml": "^4.1.0", 25 | "next": "13.1.6", 26 | "next-mdx-remote": "^4.1.0", 27 | "react": "18.2.0", 28 | "react-dom": "18.2.0", 29 | "webpack": "^5.72.0" 30 | }, 31 | "devDependencies": { 32 | "@tailwindcss/postcss": "^4.1.7", 33 | "@types/github-slugger": "^1.3.0", 34 | "@types/mdx-js__react": "^1.5.5", 35 | "@types/node": "16.11.12", 36 | "@types/react": "17.0.37", 37 | "@types/react-dom": "^17.0.11", 38 | "esast-util-from-js": "^1.1.0", 39 | "eslint": "8.4.1", 40 | "eslint-config-next": "13.0.1", 41 | "estree-util-value-to-estree": "^1.3.0", 42 | "hast-util-from-html": "^1.0.0", 43 | "hast-util-to-html": "^8.0.3", 44 | "hast-util-to-text": "^3.1.1", 45 | "lowlight": "^2.7.0", 46 | "mdast-util-from-markdown": "^1.2.0", 47 | "node-fetch": "^3.2.10", 48 | "postcss": "^8.4.5", 49 | "prettier": "^3.5.3", 50 | "prettier-plugin-tailwindcss": "^0.6.11", 51 | "rehype-slug": "^5.0.1", 52 | "remark": "^14.0.2", 53 | "remark-frontmatter": "^4.0.1", 54 | "remark-gfm": "^3.0.1", 55 | "serve-handler": "^6.1.3", 56 | "tailwindcss": "^4.1.7", 57 | "typescript": "4.8.4", 58 | "unified": "^10.1.2", 59 | "unist-util-remove": "^3.1.0", 60 | "unist-util-visit": "^4.1.1", 61 | "vitest": "^3.1.3" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "@docsearch/css"; 2 | import "../styles/fonts.css"; 3 | import "../styles/globals.css"; 4 | import type { AppProps } from "next/app"; 5 | import Head from "next/head"; 6 | import { ThemeProvider } from "components/ThemeProvider"; 7 | 8 | function App({ Component, pageProps }: AppProps) { 9 | return ( 10 | <> 11 | <Head> 12 | <link rel="icon" href="/favicon.png" /> 13 | </Head> 14 | <ThemeProvider> 15 | <div className="min-h-screen dark:bg-stone-900"> 16 | <Component {...pageProps} /> 17 | </div> 18 | </ThemeProvider> 19 | </> 20 | ); 21 | } 22 | 23 | export default App; 24 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Document, { Html, Head, Main, NextScript } from "next/document"; 3 | 4 | class MyDocument extends Document { 5 | render() { 6 | return ( 7 | <Html lang="zh-cn"> 8 | <Head> 9 | <script 10 | dangerouslySetInnerHTML={{ 11 | __html: ` 12 | try { 13 | if (localStorage['bv-doc-theme'] === 'dark' || (!('bv-doc-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) { 14 | document.documentElement.classList.add('dark') 15 | localStorage.setItem('bv-doc-theme','dark') 16 | } else { 17 | document.documentElement.classList.remove('dark') 18 | } 19 | } catch (_) {} 20 | `, 21 | }} 22 | /> 23 | </Head> 24 | <body> 25 | <Main /> 26 | <NextScript /> 27 | </body> 28 | </Html> 29 | ); 30 | } 31 | } 32 | 33 | export default MyDocument; 34 | -------------------------------------------------------------------------------- /pages/docs/[...slug].tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { MDXComponents } from "components/MDXComponents"; 3 | import { SideBar, SidebarProvider } from "components/SideBar"; 4 | import NavBar from "components/NavBar"; 5 | import { GetStaticPaths, GetStaticProps } from "next"; 6 | import sideBarUser from "constant/sidebar-user"; 7 | import sideBarDeveloper from "constant/sidebar-developer"; 8 | import path from "path"; 9 | import { accessSync, constants } from "fs"; 10 | import { serialize } from "next-mdx-remote/serialize"; 11 | import { MDXRemote } from "next-mdx-remote"; 12 | import { readFile } from "fs/promises"; 13 | import remarkGfm from "remark-gfm"; 14 | import remarkExportHeading from "plugins/remark-export-heading.mjs"; 15 | import remarkPathToRepo from "plugins/remark-path-to-repo.mjs"; 16 | import rehypeHighlight from "plugins/rehype-highlight.mjs"; 17 | import rehypeSlug from "rehype-slug"; 18 | import { loaderPromise as remoteContentLoader } from "loaders/replace-remote-content/main.mjs"; 19 | import MDXWrapper from "components/MDXWrapper"; 20 | 21 | const MDXPage = ({ source, router, headings }: any) => { 22 | return ( 23 | <SidebarProvider> 24 | <NavBar /> 25 | <SideBar routerTree={router} /> 26 | <div className="px-6 lg:ml-80 lg:flex"> 27 | <MDXWrapper router={router} headings={headings} meta={{}}> 28 | <MDXRemote {...source} components={MDXComponents} /> 29 | </MDXWrapper> 30 | </div> 31 | </SidebarProvider> 32 | ); 33 | }; 34 | 35 | export const getStaticProps: GetStaticProps = async (context) => { 36 | const slug = context.params!.slug as string[]; 37 | if (slug.length == 1) slug.push("index"); 38 | let filePath = path.resolve(path.join("docs", slug.join("/"))); 39 | try { 40 | accessSync(`${filePath}.mdx`, constants.R_OK); 41 | filePath = `${filePath}.mdx`; 42 | } catch (err) { 43 | filePath = `${filePath}.md`; 44 | } 45 | 46 | let file = ""; 47 | try { 48 | const mdxFile = await readFile(filePath); 49 | file = mdxFile.toString(); 50 | file = await remoteContentLoader(file); 51 | } catch (error) { 52 | console.error(error); 53 | } 54 | 55 | const headings: any[] = []; 56 | const mdxSource = await serialize(file, { 57 | mdxOptions: { 58 | remarkPlugins: [ 59 | remarkGfm, 60 | [remarkExportHeading, { headings }], 61 | remarkPathToRepo, 62 | ], 63 | rehypePlugins: [ 64 | rehypeHighlight, 65 | rehypeSlug, 66 | // [rehypeStaticProps, `{headings}`], 67 | ], 68 | }, 69 | }); 70 | return { 71 | props: { 72 | router: slug[0] === "user" ? sideBarUser : sideBarDeveloper, 73 | source: mdxSource, 74 | headings, 75 | }, // will be passed to the page component as props 76 | }; 77 | }; 78 | 79 | export const getStaticPaths: GetStaticPaths = async () => { 80 | const paths = [] as any[]; 81 | sideBarUser.routes.forEach((route) => paths.push(route.path)); 82 | sideBarDeveloper.routes.forEach((route) => paths.push(route.path)); 83 | return { 84 | paths, 85 | fallback: false, 86 | }; 87 | }; 88 | export default MDXPage; 89 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import Head from "next/head"; 3 | import Link from "next/link"; 4 | import Image from "next/image"; 5 | import { PropsWithChildren, useState } from "react"; 6 | import cn from "classnames"; 7 | import { Themes, useTheme } from "components/ThemeProvider"; 8 | import { Transition } from "@headlessui/react"; 9 | import freshHome from "../public/images/index/fresh-home.png"; 10 | 11 | // deploy 12 | const tabs = [ 13 | { 14 | title: "自由组合", 15 | content: "近百种组件,总有适合你的,下载它们组合出只属于你的应用吧", 16 | img: { 17 | light: "/images/index/image-0.png", 18 | dark: "/images/index/image-dark-0.png", 19 | }, 20 | }, 21 | { 22 | title: "自由开关", 23 | content: "每个组件随时随地,开谁关谁你说了算。", 24 | img: { 25 | light: "/images/index/image.png", 26 | dark: "/images/index/image-dark.png", 27 | }, 28 | }, 29 | { 30 | title: "清爽首页", 31 | content: "使用重新设计的清爽风格首页替换原本的首页", 32 | }, 33 | { 34 | title: "黑暗主题", 35 | content: "我们提供啊B定制化黑暗主题,保护夜里在B站冲浪的你", 36 | }, 37 | ]; 38 | 39 | const StyledTransition = ({ 40 | children, 41 | show, 42 | className = "", 43 | }: PropsWithChildren<{ show: boolean; className?: string }>) => ( 44 | <Transition 45 | className={cn("absolute", className)} 46 | show={show} 47 | enterFrom="opacity-0 -translate-x-10" 48 | enterTo="opacity-100 translate-x-0" 49 | enter="transition duration-300" 50 | leaveFrom="opacity-100 translate-x-0" 51 | leaveTo="opacity-0 -translate-x-10" 52 | leave="transition duration-300" 53 | appear={true} 54 | unmount={false} 55 | > 56 | {children} 57 | </Transition> 58 | ); 59 | 60 | const Home: NextPage<{ contributors: any[] }> = ({ contributors }) => { 61 | const [activeTab, setTab] = useState(0); 62 | const { theme, setDarkMode, setLightMode } = useTheme(); 63 | return ( 64 | <div className="px-5"> 65 | <Head> 66 | <title>欢迎o(* ̄▽ ̄*)ブ 67 | 68 | 69 |
      70 | {/* left */} 71 |
      72 | {/* light */} 73 | 74 | img 81 | 88 | img 95 | 96 | 97 | 98 | 104 | 108 | 了解更多 109 | 110 | 111 |
      112 | 113 | {/* right */} 114 |
      115 | {/* page1 */} 116 |
      117 |
      118 | 119 | BiliBili-Evolved 120 | 121 | 122 | 给你足够多,足够强大的功能 123 | 124 | 128 | 立刻尝试 129 | 130 |
      131 |
      132 | 133 | {/* page2 */} 134 |
      135 | 我们有 136 |
      137 |
      138 | {tabs.map((item, idx) => ( 139 |
      { 151 | if (idx === 3) { 152 | theme == Themes.DARK ? setLightMode() : setDarkMode(); 153 | } else { 154 | setTab(idx); 155 | } 156 | }} 157 | > 158 | 163 | {item.title} 164 | 165 |

      166 | {item.content} 167 |

      168 |
      169 | ))} 170 |
      171 |
      172 |
      173 |
      174 | 175 | {/* footer */} 176 |
      177 |
      178 | 社区支持 179 | 185 | 参与开发 186 | 187 |
      188 | {contributors.map((contributor, idx) => ( 189 | 195 | 202 |
      203 | 204 | {contributor.login} 205 | 206 | 207 | 贡献 {contributor.contributions} 208 | 209 |
      210 | 211 | ))} 212 |
      213 |
      214 |
      215 |
      216 | ); 217 | }; 218 | 219 | export async function getStaticProps() { 220 | let contributors = []; 221 | try { 222 | contributors = await fetch( 223 | "https://api.github.com/repos/the1812/Bilibili-Evolved/contributors" 224 | ).then((res) => res.json()); 225 | } catch (error) { 226 | console.log(error); 227 | } 228 | return { 229 | props: { 230 | contributors, 231 | }, // will be passed to the page component as props 232 | }; 233 | } 234 | 235 | export default Home; 236 | -------------------------------------------------------------------------------- /pages/ssr.tsx: -------------------------------------------------------------------------------- 1 | const Page = () => { 2 | return

      Hi

      ; 3 | }; 4 | 5 | export async function getServerSideProps() { 6 | console.log("server here"); 7 | return { 8 | props: {}, 9 | }; 10 | } 11 | 12 | export default Page; 13 | -------------------------------------------------------------------------------- /plugins/recma-lift-up-props.mjs: -------------------------------------------------------------------------------- 1 | import { fromJs } from "esast-util-from-js"; 2 | 3 | /** 4 | * 5 | * @param {[]string} propsName 6 | * @returns 7 | */ 8 | export default function liftUpProps(propsName = []) { 9 | /** 10 | * global.[prop] = prop 11 | */ 12 | const template = (props) => `global.${props} = ${props}`; 13 | return (ast) => { 14 | propsName.forEach((prop) => { 15 | ast.body.push(fromJs(template(prop))); 16 | }); 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /plugins/rehype-highlight.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Source:https://github.com/rehypejs/rehype-highlight 3 | */ 4 | 5 | 6 | /** 7 | * @typedef {import('lowlight').Root} LowlightRoot 8 | * @typedef {import('lowlight/lib/core.js').HighlightSyntax} HighlightSyntax 9 | * @typedef {import('hast').Root} Root 10 | * @typedef {import('hast').Element} Element 11 | * @typedef {Root|Root['children'][number]} Node 12 | * 13 | * @typedef Options 14 | * Configuration (optional). 15 | * @property {string} [prefix='hljs-'] 16 | * Prefix to use before classes. 17 | * @property {boolean|Array} [subset] 18 | * Scope of languages to check when auto-detecting (default: all languages). 19 | * Pass `false` to not highlight code without language classes. 20 | * @property {boolean} [ignoreMissing=false] 21 | * Swallow errors for missing languages. 22 | * By default, unregistered syntaxes throw an error when they are used. 23 | * Pass `true` to swallow those errors and thus ignore code with unknown code 24 | * languages. 25 | * @property {Array} [plainText=[]] 26 | * List of plain-text languages. 27 | * Pass any languages you would like to be kept as plain-text instead of 28 | * getting highlighted. 29 | * @property {Record>} [aliases={}] 30 | * Register more aliases. 31 | * Passed to `lowlight.registerAlias`. 32 | * @property {Record} [languages={}] 33 | * Register more languages. 34 | * Each key/value pair passed as arguments to `lowlight.registerLanguage`. 35 | */ 36 | 37 | import {lowlight} from 'lowlight' 38 | import {toText} from 'hast-util-to-text' 39 | import {visit} from 'unist-util-visit' 40 | 41 | const own = {}.hasOwnProperty 42 | 43 | /** 44 | * Plugin to highlight the syntax of code with lowlight (`highlight.js`). 45 | * 46 | * @type {import('unified').Plugin<[Options?] | Array, Root>} 47 | */ 48 | export default function rehypeHighlight(options = {}) { 49 | const {aliases, languages, prefix, plainText, ignoreMissing, subset} = options 50 | let name = 'hljs' 51 | 52 | if (aliases) { 53 | lowlight.registerAlias(aliases) 54 | } 55 | 56 | if (languages) { 57 | /** @type {string} */ 58 | let key 59 | 60 | for (key in languages) { 61 | if (own.call(languages, key)) { 62 | lowlight.registerLanguage(key, languages[key]) 63 | } 64 | } 65 | } 66 | 67 | if (prefix) { 68 | const pos = prefix.indexOf('-') 69 | name = pos > -1 ? prefix.slice(0, pos) : prefix 70 | } 71 | 72 | return (tree) => { 73 | // eslint-disable-next-line complexity 74 | visit(tree, 'element', (node, _, givenParent) => { 75 | const parent = /** @type {Node?} */ (givenParent) 76 | 77 | if ( 78 | !parent || 79 | !('tagName' in parent) || 80 | parent.tagName !== 'pre' || 81 | node.tagName !== 'code' || 82 | !node.properties 83 | ) { 84 | return 85 | } 86 | 87 | const lang = language(node) 88 | 89 | if ( 90 | lang === false || 91 | (!lang && subset === false) || 92 | (lang && plainText && plainText.includes(lang)) 93 | ) { 94 | return 95 | } 96 | 97 | if (!Array.isArray(node.properties.className)) { 98 | node.properties.className = [] 99 | } 100 | 101 | if (!node.properties.className.includes(name)) { 102 | node.properties.className.unshift(name) 103 | } 104 | 105 | /** @type {LowlightRoot} */ 106 | let result 107 | 108 | try { 109 | result = lang 110 | ? lowlight.highlight(lang, toText(parent), {prefix}) 111 | : // @ts-expect-error: we checked that `subset` is not a boolean. 112 | lowlight.highlightAuto(toText(parent), {prefix, subset}) 113 | } catch (error) { 114 | const exception = /** @type {Error} */ (error) 115 | if (!ignoreMissing || !/Unknown language/.test(exception.message)) { 116 | throw error 117 | } 118 | 119 | return 120 | } 121 | 122 | if (!lang && result.data.language) { 123 | node.properties.className.push('language-' + result.data.language) 124 | } 125 | 126 | if (Array.isArray(result.children) && result.children.length > 0) { 127 | node.children = result.children 128 | } 129 | 130 | parent.children.push({ 131 | tagName: 'div', 132 | properties: { 133 | id: 'plaint-text', 134 | style: 'display:none' 135 | }, 136 | children: [ 137 | { 138 | value: toText(parent), 139 | type: 'text' 140 | } 141 | ], 142 | type: 'element' 143 | }) 144 | }) 145 | } 146 | } 147 | 148 | /** 149 | * Get the programming language of `node`. 150 | * 151 | * @param {Element} node 152 | * @returns {false|string|undefined} 153 | */ 154 | function language(node) { 155 | const className = node.properties && node.properties.className 156 | let index = -1 157 | 158 | if (!Array.isArray(className)) { 159 | return 160 | } 161 | 162 | while (++index < className.length) { 163 | const value = String(className[index]) 164 | 165 | if (value === 'no-highlight' || value === 'nohighlight') { 166 | return false 167 | } 168 | 169 | if (value.slice(0, 5) === 'lang-') { 170 | return value.slice(5) 171 | } 172 | 173 | if (value.slice(0, 9) === 'language-') { 174 | return value.slice(9) 175 | } 176 | } 177 | } -------------------------------------------------------------------------------- /plugins/rehype-static-props.mjs: -------------------------------------------------------------------------------- 1 | import { fromJs } from "esast-util-from-js"; 2 | 3 | /** 4 | * AST transformer adds `getStaticProps` to the tree based on provided mapping. 5 | * @param {string} propsObject 6 | */ 7 | export default function createNextStaticProps(propsObject) { 8 | const template = ` 9 | export const getStaticProps = async () => { 10 | return { 11 | props: ${propsObject}, 12 | } 13 | }`; 14 | return function transformer(tree) { 15 | tree.children.push({ 16 | type: "mdxjsEsm", 17 | data: { 18 | estree: fromJs(template, { module: true }), 19 | }, 20 | }); 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /plugins/remark-export-heading.mjs: -------------------------------------------------------------------------------- 1 | import { visit } from "unist-util-visit"; 2 | import Slugger from "github-slugger"; 3 | import { valueToEstree } from "estree-util-value-to-estree"; 4 | const slugger = new Slugger(); 5 | 6 | /** 7 | * @typedef {import('mdast').Root} Root - https://github.com/syntax-tree/mdast#root 8 | * @typedef {import('mdast').Heading} Heading - https://github.com/syntax-tree/mdast#heading 9 | */ 10 | 11 | /** 12 | * @author https://github.com/expo/expo 13 | */ 14 | 15 | export default function plugin({ headings }) { 16 | slugger.reset(); 17 | /** @param {Root} tree */ 18 | return (tree) => { 19 | // const headings = []; 20 | 21 | /** @param {Heading} node - */ 22 | const parseNodeToSting = (node) => { 23 | // node is text 24 | if (node.type === "text") { 25 | return node.value; 26 | } 27 | // node is link 28 | return node.children 29 | .map((child) => { 30 | if (child.type === "text") { 31 | return child.value; 32 | } else { 33 | throw Error("node type is not text"); 34 | } 35 | }) 36 | .join(" "); 37 | }; 38 | 39 | /** @param {Heading} node - */ 40 | const visitor = (node) => { 41 | if (node.children.length > 0) { 42 | // parse text and link 43 | const title = node.children.map(parseNodeToSting).join(" "); 44 | headings.push({ 45 | id: node.data?.id || slugger.slug(title), 46 | depth: node.depth, 47 | type: 48 | node.children.find((node) => node.type !== "text")?.type || "text", 49 | title, 50 | }); 51 | } 52 | }; 53 | 54 | visit(tree, "heading", visitor); 55 | 56 | /** 57 | * equivalence to 58 | * 59 | * export const headings = [...] 60 | */ 61 | // const tocExport = { 62 | // type: "mdxjsEsm", 63 | // data: { 64 | // estree: { 65 | // type: "Program", 66 | // sourceType: "module", 67 | // body: [ 68 | // { 69 | // type: "ExportNamedDeclaration", 70 | // specifiers: [], 71 | // source: null, 72 | // declaration: { 73 | // type: "VariableDeclaration", 74 | // kind: "const", 75 | // declarations: [ 76 | // { 77 | // type: "VariableDeclarator", 78 | // id: { 79 | // type: "Identifier", 80 | // name: "headings", 81 | // }, 82 | // init: valueToEstree(headings), 83 | // }, 84 | // ], 85 | // }, 86 | // }, 87 | // ], 88 | // }, 89 | // }, 90 | // }; 91 | // tree.children.push(tocExport); 92 | }; 93 | } 94 | -------------------------------------------------------------------------------- /plugins/remark-path-to-repo.mjs: -------------------------------------------------------------------------------- 1 | import { visit } from "unist-util-visit"; 2 | 3 | /** 4 | * @typedef {import('mdast').Root} Root - https://github.com/syntax-tree/mdast#root 5 | * @typedef {import('mdast').Heading} Heading - https://github.com/syntax-tree/mdast#heading 6 | */ 7 | 8 | const isRelativePath = (url) => url.includes("../../"); 9 | 10 | export default function plugin() { 11 | /** @param {Root} tree */ 12 | return (tree) => { 13 | /** @param {Heading} node - */ 14 | const visitor = (node) => { 15 | if (isRelativePath(node.url)) { 16 | const repoUrl = node.url.replace( 17 | /\.\.\/\.\.\/registry\/dist/g, 18 | "https://github.com/the1812/Bilibili-Evolved/tree/master/registry/lib" 19 | ); 20 | node.url = repoUrl.slice(0, repoUrl.length - 3); 21 | } 22 | }; 23 | 24 | visit(tree, "link", visitor); 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /plugins/remark-yaml.js: -------------------------------------------------------------------------------- 1 | const yaml = require("js-yaml"); 2 | const visit = require("unist-util-visit"); 3 | 4 | /** 5 | * @author https://github.com/expo/expo 6 | */ 7 | // 通过 `remark-frontmatter` 处理后获得的 meta 数据 8 | module.exports = () => { 9 | return (tree) => { 10 | let yamlTransformed = false; 11 | 12 | visit(tree, "yaml", (node) => { 13 | const data = yaml.load(node.value); 14 | node.type = "export"; 15 | node.value = `export const meta = ${JSON.stringify(data)};`; 16 | node.position = undefined; 17 | 18 | yamlTransformed = true; 19 | 20 | return visit.EXIT; 21 | }); 22 | 23 | if (!yamlTransformed) { 24 | tree.children.push({ 25 | type: "export", 26 | value: `export const meta = {};`, 27 | }); 28 | } 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | "@tailwindcss/postcss": {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require("prettier-plugin-tailwindcss")], 3 | }; 4 | -------------------------------------------------------------------------------- /public/bilibili-evolved-wide-color.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/bilibili-evolved.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/docs/images/compressed/afdian.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoundTheWOUT/bilibili-evolved-doc/ceedf1fc74c5d9b2eb61fea0cec94b842cce40ec/public/docs/images/compressed/afdian.jpg -------------------------------------------------------------------------------- /public/docs/images/compressed/wechat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoundTheWOUT/bilibili-evolved-doc/ceedf1fc74c5d9b2eb61fea0cec94b842cce40ec/public/docs/images/compressed/wechat.jpg -------------------------------------------------------------------------------- /public/entries/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoundTheWOUT/bilibili-evolved-doc/ceedf1fc74c5d9b2eb61fea0cec94b842cce40ec/public/entries/.gitkeep -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoundTheWOUT/bilibili-evolved-doc/ceedf1fc74c5d9b2eb61fea0cec94b842cce40ec/public/favicon.png -------------------------------------------------------------------------------- /public/fonts/Inter-roman.var.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoundTheWOUT/bilibili-evolved-doc/ceedf1fc74c5d9b2eb61fea0cec94b842cce40ec/public/fonts/Inter-roman.var.woff2 -------------------------------------------------------------------------------- /public/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/icons/github-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoundTheWOUT/bilibili-evolved-doc/ceedf1fc74c5d9b2eb61fea0cec94b842cce40ec/public/icons/github-light.png -------------------------------------------------------------------------------- /public/icons/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoundTheWOUT/bilibili-evolved-doc/ceedf1fc74c5d9b2eb61fea0cec94b842cce40ec/public/icons/github.png -------------------------------------------------------------------------------- /public/images/index/bilibili-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoundTheWOUT/bilibili-evolved-doc/ceedf1fc74c5d9b2eb61fea0cec94b842cce40ec/public/images/index/bilibili-dark.png -------------------------------------------------------------------------------- /public/images/index/fresh-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoundTheWOUT/bilibili-evolved-doc/ceedf1fc74c5d9b2eb61fea0cec94b842cce40ec/public/images/index/fresh-home.png -------------------------------------------------------------------------------- /public/images/index/image-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoundTheWOUT/bilibili-evolved-doc/ceedf1fc74c5d9b2eb61fea0cec94b842cce40ec/public/images/index/image-0.png -------------------------------------------------------------------------------- /public/images/index/image-dark-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoundTheWOUT/bilibili-evolved-doc/ceedf1fc74c5d9b2eb61fea0cec94b842cce40ec/public/images/index/image-dark-0.png -------------------------------------------------------------------------------- /public/images/index/image-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoundTheWOUT/bilibili-evolved-doc/ceedf1fc74c5d9b2eb61fea0cec94b842cce40ec/public/images/index/image-dark.png -------------------------------------------------------------------------------- /public/images/index/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoundTheWOUT/bilibili-evolved-doc/ceedf1fc74c5d9b2eb61fea0cec94b842cce40ec/public/images/index/image.png -------------------------------------------------------------------------------- /public/images/manage-panel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoundTheWOUT/bilibili-evolved-doc/ceedf1fc74c5d9b2eb61fea0cec94b842cce40ec/public/images/manage-panel.jpg -------------------------------------------------------------------------------- /public/images/settings-panel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoundTheWOUT/bilibili-evolved-doc/ceedf1fc74c5d9b2eb61fea0cec94b842cce40ec/public/images/settings-panel.jpg -------------------------------------------------------------------------------- /public/images/side-panel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoundTheWOUT/bilibili-evolved-doc/ceedf1fc74c5d9b2eb61fea0cec94b842cce40ec/public/images/side-panel.jpg -------------------------------------------------------------------------------- /scripts/build.mjs: -------------------------------------------------------------------------------- 1 | import { execSync } from "child_process"; 2 | import fetchEntries from "./fetch-entries.mjs"; 3 | 4 | (async function main() { 5 | await fetchEntries(); 6 | execSync("next build", { stdio: "inherit" }); 7 | })(); 8 | -------------------------------------------------------------------------------- /scripts/fetch-entries.mjs: -------------------------------------------------------------------------------- 1 | import { access, mkdirSync } from "fs"; 2 | import { writeFile } from "fs/promises"; 3 | import fetch from "node-fetch"; 4 | import path from "path"; 5 | 6 | const BASE_URL = "https://github.com/the1812/Bilibili-Evolved/raw"; 7 | 8 | const writeToPublic = (target, text) => { 9 | const _dir = path.join(path.join(process.cwd(), "./public/entries")); 10 | const file_path = path.join(_dir, target); 11 | access(_dir, (err) => { 12 | if (err) { 13 | mkdirSync(_dir); 14 | } else { 15 | writeFile(file_path, text); 16 | console.log("write to", target); 17 | } 18 | }); 19 | }; 20 | 21 | async function fetchIt() { 22 | try { 23 | await fetch(BASE_URL + "/master/dist/bilibili-evolved.user.js") 24 | .then((res) => res.text()) 25 | .then((text) => 26 | // write to public 27 | writeToPublic("bilibili-evolved.user.js", text) 28 | ); 29 | 30 | await fetch(BASE_URL + "/preview/dist/bilibili-evolved.preview.user.js") 31 | .then((res) => res.text()) 32 | .then( 33 | (text) => writeToPublic("bilibili-evolved.preview.user.js", text) 34 | // write to public 35 | ); 36 | } catch (error) { 37 | throw error 38 | } 39 | } 40 | fetchIt(); 41 | // export default fetchIt; 42 | -------------------------------------------------------------------------------- /styles/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Inter var"; 3 | font-weight: 100 900; 4 | font-display: swap; 5 | font-style: normal; 6 | font-named-instance: "Regular"; 7 | src: url("/fonts/Inter-roman.var.woff2") format("woff2"); 8 | } 9 | 10 | /* @font-face { 11 | font-family: "Sarasa Gothic"; 12 | font-style: normal; 13 | font-weight: 400; 14 | font-display: swap; 15 | src: url("/fonts/sarasa-fixed-sc-regular.ttf") format("truetype"); 16 | } */ 17 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | @custom-variant dark (&:where(.dark, .dark *)); 4 | 5 | /* 6 | The default border color has changed to `currentcolor` in Tailwind CSS v4, 7 | so we've added these compatibility styles to make sure everything still 8 | looks the same as it did with Tailwind CSS v3. 9 | 10 | If we ever want to remove these styles, we need to add an explicit border 11 | color utility to any element that depends on these defaults. 12 | */ 13 | @layer base { 14 | *, 15 | ::after, 16 | ::before, 17 | ::backdrop, 18 | ::file-selector-button { 19 | border-color: var(--color-gray-200, currentcolor); 20 | } 21 | } 22 | 23 | @theme { 24 | --color-main: #1bb2ed; 25 | } 26 | 27 | @utility flex-center { 28 | @apply flex items-center justify-center; 29 | } 30 | 31 | @utility index-card { 32 | @apply mt-6 flex h-60 w-60 cursor-pointer select-none flex-col items-center justify-center rounded-xl 33 | border 34 | bg-white 35 | p-6 36 | text-left 37 | transition-shadow 38 | hover:border-none 39 | hover:bg-sky-50 40 | hover:text-sky-500 41 | hover:shadow-lg 42 | hover:shadow-sky-500/50 43 | focus:text-sky-500 44 | dark:border-stone-500 45 | dark:bg-stone-700 46 | dark:hover:bg-sky-700 47 | dark:hover:text-sky-200 48 | dark:hover:shadow-sky-700/50 49 | lg:h-96 50 | lg:w-96; 51 | } 52 | 53 | @utility btn { 54 | @apply flex-center rounded-xl text-white shadow-lg transition-shadow active:shadow-none bg-main shadow-main/50; 55 | } 56 | 57 | .light-up::after { 58 | content: ""; 59 | position: absolute; 60 | left: -100px; 61 | width: 640px; 62 | height: 1024px; 63 | background: rgba(27, 178, 237, 0.3); 64 | filter: blur(200px); 65 | border-radius: 200px; 66 | z-index: -1; 67 | } 68 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true, 21 | "baseUrl": ".", 22 | "plugins": [ 23 | { 24 | "name": "next" 25 | } 26 | ] 27 | }, 28 | "include": [ 29 | "next-env.d.ts", 30 | "**/*.ts", 31 | "**/*.tsx", 32 | ".next/types/**/*.ts" 33 | ], 34 | "exclude": [ 35 | "node_modules" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /types/global-env.d.ts: -------------------------------------------------------------------------------- 1 | import { RemarkHeading } from "components/MDXWrapper"; 2 | 3 | // NOTE: This file should not be edited 4 | // see https://nextjs.org/docs/basic-features/typescript for more information. 5 | 6 | declare global { 7 | interface Window { 8 | headings: RemarkHeading[] | undefined; 9 | } 10 | } 11 | 12 | export {}; 13 | -------------------------------------------------------------------------------- /types/type.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.mdx"; 2 | declare module "*.md"; 3 | --------------------------------------------------------------------------------