├── .editorconfig
├── .github
└── workflows
│ └── build.yml
├── .gitignore
├── .npmrc
├── .prettierrc
├── .yarnrc
├── LICENSE
├── README.md
├── demo
├── .gitignore
├── CHANGELOG.md
├── content
│ ├── examples
│ │ ├── comment-system.md
│ │ ├── mdx-components
│ │ │ └── FakeClock.tsx
│ │ ├── mdx.mdx
│ │ ├── override-styles.md
│ │ └── syntax-highlight.md
│ ├── features
│ │ ├── graph-view.md
│ │ ├── table-of-contents.md
│ │ ├── using-frontmatter.md
│ │ └── wiki-links.md
│ ├── readme.md
│ ├── showcase.md
│ └── todo.md
├── gatsby-browser.js
├── gatsby-config.js
├── gatsby-ssr.js
├── package.json
└── src
│ ├── gatsby-theme-kb
│ └── components
│ │ └── Topic
│ │ ├── Comment
│ │ └── index.tsx
│ │ └── index.tsx
│ └── styles
│ ├── main.css
│ └── prism-theme.css
├── lerna.json
├── package.json
├── packages
├── gatsby-remark-wiki-link
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ └── index.ts
│ └── tsconfig.json
├── gatsby-theme-kb
│ ├── .gitignore
│ ├── .prettierignore
│ ├── .yarnrc
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── fragments.js
│ ├── gatsby-browser.js
│ ├── gatsby-config.js
│ ├── gatsby-node-utils
│ │ ├── makeSearchPlugins.js
│ │ ├── shouldHandleFile.js
│ │ └── source-nodes.js
│ ├── gatsby-node.js
│ ├── gatsby-ssr.js
│ ├── package.json
│ ├── postcss.config.js
│ ├── src
│ │ ├── components
│ │ │ ├── DarkModeToggle
│ │ │ │ ├── dark-mode-toggle.css
│ │ │ │ └── index.tsx
│ │ │ ├── GraphButton
│ │ │ │ ├── graph-button.css
│ │ │ │ └── index.tsx
│ │ │ ├── GraphView
│ │ │ │ ├── graph-view.css
│ │ │ │ └── index.tsx
│ │ │ ├── InlineTOC
│ │ │ │ ├── index.tsx
│ │ │ │ └── inline-toc.css
│ │ │ ├── LinkReference
│ │ │ │ ├── index.tsx
│ │ │ │ └── link-reference.css
│ │ │ ├── Search
│ │ │ │ ├── index.tsx
│ │ │ │ └── search.css
│ │ │ ├── SiteSidebar
│ │ │ │ ├── index.tsx
│ │ │ │ └── site-sidebar.css
│ │ │ ├── Topic
│ │ │ │ ├── index.tsx
│ │ │ │ └── topic.css
│ │ │ ├── TopicLayout
│ │ │ │ ├── index.tsx
│ │ │ │ └── topic-layout.css
│ │ │ ├── TreeView
│ │ │ │ ├── index.tsx
│ │ │ │ └── tree-view.css
│ │ │ ├── header.css
│ │ │ ├── header.js
│ │ │ ├── icons
│ │ │ │ ├── index.tsx
│ │ │ │ └── svg-icon.css
│ │ │ ├── layout.css
│ │ │ ├── layout.js
│ │ │ ├── mdx-components
│ │ │ │ ├── AnchorTag.tsx
│ │ │ │ ├── MDXRenderer.tsx
│ │ │ │ ├── anchor-tag.css
│ │ │ │ └── header-components.tsx
│ │ │ └── seo.js
│ │ ├── configs
│ │ │ └── hotkeys.ts
│ │ ├── env.ts
│ │ ├── images
│ │ │ ├── gatsby-astronaut.png
│ │ │ └── gatsby-icon.png
│ │ ├── pages
│ │ │ └── 404.js
│ │ ├── styles
│ │ │ ├── base.css
│ │ │ ├── components
│ │ │ │ ├── link.css
│ │ │ │ └── scrollbar.css
│ │ │ ├── global.css
│ │ │ ├── theme.css
│ │ │ ├── tocbot.css
│ │ │ └── vars.css
│ │ ├── templates
│ │ │ └── Topic.js
│ │ ├── type.ts
│ │ ├── use-graph-data.ts
│ │ ├── use-search.tsx
│ │ ├── use-window-size.ts
│ │ └── utils
│ │ │ ├── index.ts
│ │ │ └── toc.ts
│ ├── tailwind.config.js
│ └── tsconfig.json
└── transformer-wiki-references
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── __tests__
│ ├── get-references.spec.ts
│ └── markdown-utils.spec.ts
│ ├── gatsby-node.js
│ ├── jest.config.js
│ ├── package.json
│ ├── src
│ ├── cache.ts
│ ├── compute-inbounds.ts
│ ├── content-tree.ts
│ ├── get-references.ts
│ ├── index.ts
│ ├── markdown-utils.ts
│ ├── on-create-node.ts
│ ├── on-pre-bootstrap.ts
│ ├── options.ts
│ ├── schema-customization.ts
│ ├── type.ts
│ └── util.ts
│ └── tsconfig.json
├── tsconfig.build.json
├── tsconfig.json
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 | max_line_length = 100
12 | quote_type = single
13 |
14 | [*.md]
15 | trim_trailing_whitespace = false
16 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: 'Build gatsby-project-kb site'
2 |
3 | on:
4 | push:
5 | branches:
6 | # - master
7 | - ci
8 | - feature/deploy
9 |
10 | jobs:
11 | build:
12 | name: Build Site
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@master
16 | - uses: actions/setup-node@v2
17 | with:
18 | node-version: "14"
19 | - name: yarn install
20 | uses: jaid/action-npm-install@v1.2.1
21 | - name: build
22 | uses: CultureHQ/actions-yarn@master
23 | env:
24 | KB_BASE_PATH: '/gatsby-project-kb'
25 | with:
26 | args: build
27 | - name: Deploy to GitHub Pages
28 | if: success()
29 | uses: crazy-max/ghaction-github-pages@v2
30 | with:
31 | target_branch: gh-pages
32 | build_dir: demo/public
33 | env:
34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | .DS_Store
3 | node_modules
4 | dist
5 |
6 | .cache/
7 |
8 | node_modules
9 | package-lock.json
10 | yarn.lock
11 | !/yarn.lock
12 |
13 | .yalc
14 | yalc.lock
15 |
16 | packages/*/lib
17 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | registry=https://registry.npmjs.org/
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "singleQuote": true,
4 | "semi": false,
5 | "arrowParens": "always"
6 | }
7 |
--------------------------------------------------------------------------------
/.yarnrc:
--------------------------------------------------------------------------------
1 | registry https://registry.npmjs.org/
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) hikerpig, 2021.
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 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | gatsby-project-kb
2 | ===
3 |
4 | Here is a project developing a gatsby theme for publishing **K**nowledge **B**ase.
5 |
6 | You can check out the demo and [documentation](https://gatsby-project-kb.vercel.app/).
7 |
8 | If you are looking for `gatsby-theme-kb`, go to directory [packages/gatsby-theme-kb](https://github.com/hikerpig/gatsby-project-kb/tree/master/packages/gatsby-theme-kb) for more detailed docs.
9 |
10 | 
11 |
12 | # Development
13 |
14 | File structure:
15 |
16 | ```
17 | ├── demo // the demo site's code
18 | ├── packages
19 | └── gatsby-theme-kb // the Gatsby theme
20 | ├── yarn.lock
21 | ```
22 |
23 | This is a common structure for developing Gatsby theme using yarn workspace. You can check more on Gatsby official tutorial [Building a Theme](https://www.gatsbyjs.com/tutorial/building-a-theme/).
24 |
25 | ## Run it locally
26 |
27 | ```
28 | yarn # install dependencies
29 | yarn dev # start devlopment
30 | ```
31 |
32 | # Support
33 |
34 | Feel free to open issues if you have any problems or suggestions for this project, I will do my best to solve them in my spare time.
35 |
36 | If you find it useful and find yourself in a mood of sunshine, you may support me a little pack of coffee beans.
37 |
38 | I like [Geisha](https://www.wikiwand.com/en/Geisha_\(coffee\)), you will like it, too.
39 |
40 |
41 |
--------------------------------------------------------------------------------
/demo/.gitignore:
--------------------------------------------------------------------------------
1 | public/
2 |
--------------------------------------------------------------------------------
/demo/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to this project will be documented in this file.
4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5 |
6 | ## [1.4.5](https://github.com/hikerpig/gatsby-project-kb/compare/demo@1.4.4...demo@1.4.5) (2022-03-26)
7 |
8 | **Note:** Version bump only for package demo
9 |
10 |
11 |
12 |
13 |
14 | ## [1.4.3](https://github.com/hikerpig/gatsby-project-kb/compare/demo@1.4.2...demo@1.4.3) (2022-03-20)
15 |
16 | **Note:** Version bump only for package demo
17 |
18 |
19 |
20 |
21 |
22 | ## [1.4.2](https://github.com/hikerpig/gatsby-project-kb/compare/demo@1.4.2-alpha.1...demo@1.4.2) (2022-02-06)
23 |
24 | **Note:** Version bump only for package demo
25 |
26 |
27 |
28 |
29 |
30 | # [1.4.0](https://github.com/hikerpig/gatsby-project-kb/compare/demo@1.3.8...demo@1.4.0) (2021-10-24)
31 |
32 |
33 | ### Features
34 |
35 | * **demo:** an example of shadowing by adding a comment, close [#24](https://github.com/hikerpig/gatsby-project-kb/issues/24) ([5628ccf](https://github.com/hikerpig/gatsby-project-kb/commit/5628ccf2c0f57b2621398b0e82b7efefba05e065))
36 |
37 |
38 |
39 |
40 |
41 | # [1.3.0-alpha.3](https://github.com/hikerpig/gatsby-project-kb/compare/demo@1.3.0-alpha.2...demo@1.3.0-alpha.3) (2021-04-28)
42 |
43 | **Note:** Version bump only for package demo
44 |
45 |
46 |
47 |
48 |
49 | # [1.3.0-alpha.2](https://github.com/hikerpig/gatsby-project-kb/compare/demo@1.3.0-alpha.1...demo@1.3.0-alpha.2) (2021-04-19)
50 |
51 | **Note:** Version bump only for package demo
52 |
53 |
54 |
55 |
56 |
57 | # [1.3.0-alpha.1](https://github.com/hikerpig/gatsby-project-kb/compare/demo@1.3.0-alpha.0...demo@1.3.0-alpha.1) (2021-04-07)
58 |
59 | **Note:** Version bump only for package demo
60 |
61 |
62 |
63 |
64 |
65 | # [1.3.0-alpha.0](https://github.com/hikerpig/gatsby-project-kb/compare/demo@1.2.4...demo@1.3.0-alpha.0) (2021-04-02)
66 |
67 |
68 | ### Features
69 |
70 | * Add anchor reference support, related [#8](https://github.com/hikerpig/gatsby-project-kb/issues/8) ([3c0d13a](https://github.com/hikerpig/gatsby-project-kb/commit/3c0d13a78146dc9b6bf1215af367fbd1e3a999d4))
71 | * better wiki references resolving with `contentPath` option ([46b66a9](https://github.com/hikerpig/gatsby-project-kb/commit/46b66a973bbdd702dfadb523e9ab0ab91ed1d417))
72 |
73 |
74 |
75 |
76 |
77 | ## [1.2.4-alpha.0](https://github.com/hikerpig/gatsby-project-kb/compare/demo@1.2.3...demo@1.2.4-alpha.0) (2021-03-04)
78 |
79 | **Note:** Version bump only for package demo
80 |
81 |
82 |
83 |
84 |
85 | ## [1.2.3](https://github.com/hikerpig/gatsby-project-kb/compare/demo@1.2.2...demo@1.2.3) (2021-03-03)
86 |
87 | **Note:** Version bump only for package demo
88 |
89 |
90 |
91 |
92 |
93 | ## [1.2.2](https://github.com/hikerpig/gatsby-project-kb/compare/demo@1.2.1...demo@1.2.2) (2021-02-28)
94 |
95 |
96 | ### Bug Fixes
97 |
98 | * **gatsby-theme-kb:** errors when rendering custom components in mdx mentioned in [#6](https://github.com/hikerpig/gatsby-project-kb/issues/6) ([006b87c](https://github.com/hikerpig/gatsby-project-kb/commit/006b87c3372908ae09f73bb9476171dfef279e05))
99 |
100 |
101 |
102 |
103 |
104 | ## [1.2.1](https://github.com/hikerpig/gatsby-project-kb/compare/demo@1.2.0...demo@1.2.1) (2021-02-26)
105 |
106 |
107 | ### Bug Fixes
108 |
109 | * **gatsby-theme-kb:** graph view error due to lazy render ([fea20ff](https://github.com/hikerpig/gatsby-project-kb/commit/fea20ffbb4262e36d5adf707159f13c088d8842c))
110 |
111 |
112 |
113 |
114 |
115 | # [1.2.0](https://github.com/hikerpig/gatsby-project-kb/compare/demo@1.1.3...demo@1.2.0) (2021-02-20)
116 |
117 |
118 | ### Features
119 |
120 | * add search in graph view and make it interactively highlighting result nodes ([0323db9](https://github.com/hikerpig/gatsby-project-kb/commit/0323db9ca8f8169d001b021724ca49714b5f10e4))
121 |
122 |
123 |
124 |
125 |
126 | ## [1.1.3](https://github.com/hikerpig/gatsby-project-kb/compare/demo@1.1.2...demo@1.1.3) (2021-02-15)
127 |
128 | **Note:** Version bump only for package demo
129 |
130 |
131 |
132 |
133 |
134 | ## [1.1.2](https://github.com/hikerpig/gatsby-project-kb/compare/demo@1.1.1...demo@1.1.2) (2021-02-14)
135 |
136 | **Note:** Version bump only for package demo
137 |
138 |
139 |
140 |
141 |
142 | ## [1.1.1](https://github.com/hikerpig/gatsby-project-kb/compare/demo@1.1.0...demo@1.1.1) (2021-02-14)
143 |
144 |
145 | ### Bug Fixes
146 |
147 | * **gatsby-theme-kb:** misfunction in AnchorTag caused by v0.3.0 ([7739b49](https://github.com/hikerpig/gatsby-project-kb/commit/7739b496866eb6573dad0600fa252cd292aa1348))
148 |
149 |
150 |
151 |
152 |
153 | # [1.1.0](https://github.com/hikerpig/gatsby-project-kb/compare/demo@1.0.3...demo@1.1.0) (2021-02-13)
154 |
155 |
156 | ### Features
157 |
158 | * **gatsby-theme-kb:** show backlink context, related [#1](https://github.com/hikerpig/gatsby-project-kb/issues/1) ([685b92c](https://github.com/hikerpig/gatsby-project-kb/commit/685b92c3970116cc593581f52ecc6e0b66b0c146))
159 |
--------------------------------------------------------------------------------
/demo/content/examples/comment-system.md:
--------------------------------------------------------------------------------
1 | Add a comment system
2 | ===
3 |
4 | The `gatsby-theme-kb` itself **does not** have a comment system.
5 |
6 | But sure, you can see the comments in this demo.
7 |
8 | Here is an example of adding a comment system to your site using Gatsby's Shadowing feature.
9 |
10 | > 📢 Please read the doc [_Shadowing in Gatsby Themes_](https://www.gatsbyjs.com/docs/how-to/plugins-and-themes/shadowing/) first to have a basic understanding of shadowing.
11 |
12 | You can check the [demo code on Github](https://github.com/hikerpig/gatsby-project-kb/blob/master/demo/src/gatsby-theme-kb/components/Topic).
13 |
14 | ## Shadowing component
15 |
16 | Take this demo site for example, we add a component in our own site's source to shadow one in the `gatsby-theme-kb` without touching its code.
17 |
18 | Here is a simplified list of `gatsby-theme-kb/src/components` directory. Technically you can shadow any component, but we will focus on the `Topic` component for now as it is the main content of the page and very fit to hold the comments.
19 |
20 | For all the files those are capable of being shadowed in the `gatsby-theme-kb`, please [refer to its source code](https://github.com/hikerpig/gatsby-project-kb/tree/master/packages/gatsby-theme-kb/src).
21 |
22 | ``` text
23 | components
24 | ├── DarkModeToggle
25 | ├── GraphButton
26 | ├── GraphView
27 | ├── LinkReference
28 | ├── Search
29 | ├── SiteSidebar // topic left sidebar
30 | ├── Topic // topic main content
31 | ├── TopicLayout
32 | ├── TreeView // directory tree view inside the sidebar
33 | ├── mdx-components
34 | └── seo.js
35 | ```
36 |
37 | - We can reuse the shadowed component by importing it from `gatsby-theme-kb/src/components/Topic`.
38 | - Besides that, our main purpose of shadowing is to add a `Comment` component below the main content.
39 |
40 | ```tsx
41 | // your-site/src/gatsby-theme-kb/components/Topic/index.tsx
42 | import React from 'react'
43 | import ThemeTopic, {
44 | Props as ThemeTopicProps,
45 | } from 'gatsby-theme-kb/src/components/Topic'
46 | import Comment from './Comment'
47 |
48 | interface TopicProps extends ThemeTopicProps {}
49 |
50 | const Topic = (props: TopicProps) => {
51 | return (
52 | <>
53 |
54 |
55 |
56 |
57 | >
58 | )
59 | }
60 |
61 | export default Topic
62 | ```
63 |
64 | ## The Comment component
65 |
66 | Here we choose [utterances](https://utteranc.es/) as the comment system. It is a lightweight system that uses GitHub issues. Please refer to its site for more details of setting up it to your github repository.
67 |
68 | This component is based on a tutorial [_How to Add Comments To Your GatsbyJS Blog In Less Than 10 Minutes | ahsanayaz.com_](https://ahsanayaz.com/adding-comments-to-your-gatsbyjs-blog/). With a little extra support of light/dark mode toggle.
69 |
70 | ```tsx
71 | // your-site/src/gatsby-theme-kb/components/Topic/Comment/index.tsx
72 | import React, { useEffect, memo } from 'react'
73 | import useDarkMode from 'use-dark-mode'
74 |
75 | type Props = {
76 | issueTerm: string
77 | }
78 |
79 | // you could choose other themes too
80 | const UTTERANCES_THEME_MAP = {
81 | light: 'github-light',
82 | dark: 'dark-blue',
83 | }
84 |
85 | // please change it to your own and make sure your repo has installed utterances app
86 | const REPO_NAME = 'hikerpig/gatsby-project-kb'
87 |
88 | const Comment: React.FC = memo(({ issueTerm }) => {
89 | const { value: isDark } = useDarkMode(false)
90 | const commentsUUID = `comments_${issueTerm}`
91 |
92 | useEffect(() => {
93 | let anchor
94 | const theme = isDark ? UTTERANCES_THEME_MAP.dark: UTTERANCES_THEME_MAP.light
95 | const script = document.createElement('script')
96 | anchor = document.getElementById(commentsUUID)
97 | script.setAttribute('src', 'https://utteranc.es/client.js')
98 | script.setAttribute('crossorigin', 'anonymous')
99 | script.async = true
100 | script.setAttribute('repo', REPO_NAME)
101 | script.setAttribute('issue-term', issueTerm)
102 | script.setAttribute('theme', theme)
103 | anchor.appendChild(script)
104 | return () => {
105 | anchor.innerHTML = ''
106 | }
107 | })
108 |
109 | return (
110 | <>
111 |
118 | >
119 | )
120 | })
121 |
122 | export default Comment
123 | ```
124 |
--------------------------------------------------------------------------------
/demo/content/examples/mdx-components/FakeClock.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 |
3 | interface DemoProps {}
4 |
5 | const Demo = ({}: DemoProps) => {
6 | const [date, setDate] = useState(new Date())
7 |
8 | useEffect(() => {
9 | const timerId = setInterval(() => {
10 | setDate(new Date())
11 | }, 1000)
12 |
13 | return () => {
14 | window.clearInterval(timerId)
15 | }
16 | })
17 |
18 | return (
19 |
20 | A fake clock:
21 |
22 | {date.getHours()}:{date.getMinutes()}:{date.getSeconds()}
23 |
24 |
25 | )
26 | }
27 |
28 | export default Demo
29 |
--------------------------------------------------------------------------------
/demo/content/examples/mdx.mdx:
--------------------------------------------------------------------------------
1 | # MDX and custom components
2 |
3 | MDX is an extension to Markdown that lets you include JSX in Markdown documents.
4 |
5 | You can find so many posts on the internet to help understand the basic concepts and usage in a Gatsbyjs project.
6 |
7 | Some official tutorials:
8 |
9 | - [MDX | Gatsby](https://www.gatsbyjs.com/docs/glossary/mdx/)
10 | - [Writing Pages in MDX | Gatsby](https://www.gatsbyjs.com/docs/mdx/writing-pages/)
11 |
12 | ## Using with gatsby-theme-kb
13 |
14 | Here is an example of using custom components in this demo. [Check the latest code on github](https://github.com/hikerpig/gatsby-project-kb/tree/master/demo/content/examples).
15 |
16 | File structure:
17 |
18 | ```
19 | demo/examples
20 | ├── mdx-components
21 | │ └── FakeClock.tsx
22 | ├── mdx.mdx
23 | ```
24 |
25 | In your `.mdx` file:
26 |
27 | ```tsx
28 | import FakeClock from "./mdx-components/FakeClock.tsx"
29 |
30 |
31 | ```
32 |
33 | The result:
34 |
35 | import FakeClock from "./mdx-components/FakeClock.tsx"
36 |
37 |
--------------------------------------------------------------------------------
/demo/content/examples/override-styles.md:
--------------------------------------------------------------------------------
1 | Override Styles
2 | ===
3 |
4 | If you want to customize this theme's appearance in your site, there are multiple ways to achieve it.
5 |
6 | ## Include your own styles for overriding
7 |
8 | You can import your own style files in your site's `gatsby-browser.js`. For example, see how this
9 | demo site [overrides and adds its own
10 | styles](https://github.com/hikerpig/gatsby-project-kb/blob/master/demo/gatsby-browser.js).
11 |
12 | ```js
13 | import './src/styles/prism-theme.css'
14 | import './src/styles/main.css'
15 | ```
16 |
17 | ## Override gatsby-theme-kb css variables
18 |
19 | This is similar to the previous approach, but if you just want to change some colors, instead of rewriting all things on your own, it's better to override some of gatsby-theme-kb's builtin CSS variables.
20 |
21 | You can see them in the [latest source code](https://github.com/hikerpig/gatsby-project-kb/blob/master/packages/gatsby-theme-kb/src/styles/vars.css).
22 |
23 | ```css
24 | // obsidian nord
25 | :scope {
26 | --light-1: #F9FAFB;
27 | --light-2: #F3F4F6;
28 | --light-3: #E7E5E4;
29 | --light-4: #ffffff;
30 |
31 | --dark-1: #2e3440;
32 | --dark-2: #3b4252;
33 | --dark-3: #434c5e;
34 | --dark-4: #4c566a;
35 |
36 | --frost1: #8fbcbb;
37 | --frost2: #88c0d0;
38 | --frost3: #a1c4e6;
39 | --frost4: #81a1c1;
40 |
41 | --red: #bf616a;
42 | --orange: #d08770;
43 | --yellow: #ebcb8b;
44 | --green: #a3be8c;
45 | --purple: #b48ead;
46 | }
47 |
48 | /* default theme light */
49 | body {
50 | --kb-link-color: #ff5449;
51 | --kb-tree-cur-color: #e7e5e4;
52 | --kb-font-family: 'Avenir', -apple-system, sans-serif;
53 | --kb-shadow-bg: rgba(0, 0, 0, 0.2);
54 | --kb-text-color: hsl(2deg 20% 15%);
55 | --kb-text-inverse-color: white;
56 | --kb-references-bg: #fafafa;
57 | --kb-search-highlight-bg: #eaeaea;
58 | --kb-note-bg: var(--bg-color-1);
59 | --kb-separator-color: #ddd;
60 | --kb-scrollbar-thumb: #ddd;
61 | --kb-blockquote-bg: #f6f6f6;
62 |
63 | --bg-color-1: #F9FAFB;
64 | --bg-color-2: #F3F4F6;
65 | --code-bg-color: #f0f0f0;
66 | --code-color: #333;
67 | }
68 | ```
69 |
70 | Here is how the dark mode overrides some of the variables:
71 |
72 | ```css
73 | .dark-mode {
74 | --kb-link-color: var(--frost3);
75 | --kb-tree-cur-color: var(--dark-3);
76 | --kb-font-family: 'Avenir', -apple-system, sans-serif;
77 | --kb-shadow-bg: rgba(0, 0, 0, 0.2);
78 | --kb-text-color: #eceff4;
79 | --kb-text-inverse-color: white;
80 | --kb-references-bg: var(--dark-2);
81 | --kb-search-highlight-bg: var(--dark-3);
82 | --kb-separator-color: #666;
83 | --kb-scrollbar-thumb: var(--dark-4);
84 | --kb-blockquote-bg: #353c48;
85 |
86 | --bg-color-1: var(--dark-1);
87 | --bg-color-2: var(--dark-2);
88 | --code-bg-color: var(--dark-2);
89 | --code-color: var(--purple);
90 | }
91 | ```
92 |
93 | ## Override graph view colors
94 |
95 | This theme uses [note-graph](https://github.com/hikerpig/note-graph) to show the relationship of the
96 | notes. note-graph uses a canvas for drawing, so usually it's not straightforward to change how it
97 | looks.
98 |
99 | note-graph do provide some methods to customize the colors.
100 |
101 | ### CSS Variables
102 |
103 | When initializing its theme, note-graph will try to read CSS variables on it's container's scope:
104 |
105 | ```text
106 | --notegraph-background: color of canvas background
107 | --notegraph-note-color-regular: color of normal node
108 | --notegraph-highlighted-foreground-color: border color of highlighted node
109 | --notegraph-link-color-regular: color of normal link
110 | --notegraph-link-color-highlighted: color of highlighted link
111 | ```
112 |
113 | So it's possible to specify some essential colors of the theme by overriding the CSS variables.
114 |
115 | For example, apply the ayu palette to note-graph:
116 |
117 | ```css
118 | body {
119 | --notegraph-background: #fff;
120 | --notegraph-note-color-regular: #fdb05e;
121 | --notegraph-highlighted-foreground-color: #ff9838;
122 | --notegraph-link-color-regular: #ffbdbd;
123 | --notegraph-link-color-highlighted: #ff99bd;
124 | }
125 | ```
126 |
--------------------------------------------------------------------------------
/demo/content/examples/syntax-highlight.md:
--------------------------------------------------------------------------------
1 | Add syntax highlight
2 | ===
3 |
4 | The `gatsby-theme-kb` does not cover syntax highlighting, it's up to your options to choose your syntax highlighter. Luckily, the Gatsby ecosystem has some pretty good plugins, [gatsby-remark-prismjs](https://www.gatsbyjs.com/plugins/gatsby-remark-prismjs/) can be a good start.
5 |
6 | Here is an example of add a remark plugin to `gatsby-theme-kb`. Since in this theme we use `gatsby-plugin-mdx` to process markdown files, **not** the `gatsby-transformer-remark`.
7 |
8 | ```js
9 | // a fragment of gatsby-config.js
10 | const path = require('path');
11 |
12 | module.exports = {
13 | // ...
14 | plugins: [
15 | {
16 | resolve: 'gatsby-theme-kb',
17 | options: {
18 | contentPath: path.resolve(__dirname, 'content'),
19 | getPluginMdx(defaultPluginMdx) {
20 | defaultPluginMdx.options.gatsbyRemarkPlugins.push({
21 | resolve: 'gatsby-remark-prismjs',
22 | options: {
23 | noInlineHighlight: true,
24 | },
25 | })
26 | return defaultPluginMdx
27 | },
28 | },
29 | },
30 | // ...
31 | ],
32 | };
33 | ```
34 |
35 | Choose one PrismJS theme from it's official site, or [prism-themes](https://github.com/PrismJS/prism-themes).
36 |
37 | In this demo site, I use the prism-nord theme, I download it into `src/styles/prism-theme.css`, and import it in `gatsby-browser.js`.
38 |
39 | ```js
40 | // gatsby-browser.js
41 | import './src/styles/prism-theme.css'
42 | ```
43 |
44 | ## Other resources
45 |
46 | - You may try `gatsby-remark-shiki-twoslash` as [this issue mentioned](https://github.com/hikerpig/foam-template-gatsby-kb/issues/5#issuecomment-782902350).
--------------------------------------------------------------------------------
/demo/content/features/graph-view.md:
--------------------------------------------------------------------------------
1 | Graph View
2 | ===
3 |
4 | Click the 'Show Graph Visualisation' button on top-right to show a graph visualising the relationships - which are established by [[wiki-links]] - of your notes. You can even searc your notes and get result nodes highlighted as you search.
5 |
6 | The graph is rendered by [note-graph].
7 |
8 | If you want to customize the colors of the graph, please check [[override-styles]] for instruction.
9 |
10 |
11 | 
12 |
13 | [note-graph]: https://github.com/hikerpig/note-graph
14 |
--------------------------------------------------------------------------------
/demo/content/features/table-of-contents.md:
--------------------------------------------------------------------------------
1 | Show Table Of Contents
2 | ===
3 |
4 | `gatsby-theme-kb` can be configured to have different placements of auto-generated table of contents.
5 |
6 | Suported option type is `false \| Array<'inline' | 'sidebar'>`.
7 |
8 | By default, the value is `['sidebar']`, so if you want to enable the `inline` behavior, please apply the respective option.
9 |
10 | For example:
11 |
12 | ```js
13 | {
14 | resolve: 'gatsby-theme-kb',
15 | options: {
16 | tocTypes: ['inline', 'sidebar'],
17 | },
18 | }
19 | ```
20 |
21 |
22 | ## sidebar
23 |
24 | Position the TOC in the sidebar.
25 |
26 | 
27 |
28 | The TOC sidebar can also be triggered in mobile mode.
29 |
30 | 
31 |
32 | ## inline
33 |
34 | Some people would prefer to see the table of contents inline content on top of the page.
35 |
36 | 
37 |
--------------------------------------------------------------------------------
/demo/content/features/using-frontmatter.md:
--------------------------------------------------------------------------------
1 | Using frontmatter
2 | ===
3 |
4 | ## Some special fields
5 |
6 | ### `title`
7 |
8 | This will set the page title of a topic page.
9 |
10 | ```yaml
11 | ---
12 | title: Zettelkasten
13 | ---
14 | ```
15 |
16 | If you have not specified title in frontmatter, `gatsby-theme-kb` will extract the first `h1` as title of the page.
17 |
18 | ### `private` to hide it from publishing
19 |
20 | You can prevent the topic from being published by setting `private: True` in frontmatter.
21 |
22 | ```markdown
23 | ---
24 | private: True
25 | ---
26 |
27 | ## Some private or temporary stuff
28 | ```
29 |
--------------------------------------------------------------------------------
/demo/content/features/wiki-links.md:
--------------------------------------------------------------------------------
1 | # Wiki Link
2 |
3 | A wiki-link is a link text wrapped inside double brackets `[[]]`.
4 |
5 | ## Some further details
6 |
7 | ### Anchor reference
8 |
9 | > NOTICE: This is non-standard for wiki links, but I found it quite suit to my needs so I implement it in gatsby-project-kb, and Obsidian has this support, too.
10 |
11 | You can refer to a topic's specific section by anchor text, which is after the hashtag `#`.
12 |
13 | For example, for a target topic with the structure as below.
14 |
15 | ```text
16 | # header 1
17 | ## header 2
18 | ```
19 |
20 | It's possible to link to the second header with `[[topic#header 2]]`. The target url will be `topic#header-2`, with the anchor text processed by `slugify`.
21 |
22 | ## About markdown link
23 |
24 | This theme is also capable of [extract plain markdowndown internal links](https://github.com/hikerpig/gatsby-project-kb/issues/53) and show them in backlinks.
25 |
26 | ```md
27 | A paragraph with a [markdown link](../some-other-file)
28 | ```
29 |
30 | Currently only relative link **without** file extension will be recognized.
31 |
32 | ```md
33 | This [won't work](../some-other-file.md)
34 | ```
35 |
--------------------------------------------------------------------------------
/demo/content/readme.md:
--------------------------------------------------------------------------------
1 | # Gatsby knowledge base theme
2 |
3 | This is a Gatsby theme for publishing a knowledge base or personal wiki. Named `gatsby-theme-kb`.
4 |
5 | Create your [Second Brain](https://www.buildingasecondbrain.com/) by writing down your thoughts - or as the term used this theme `topics` - and their relations in markdown.
6 |
7 | Heavily inspired by [gatsby-digital-garden](https://github.com/mathieudutour/gatsby-digital-garden) and [Obsidian](https://publish.obsidian.md/help/Index).
8 |
9 | ## ✨ Features
10 |
11 | - Support bidirectional [[wiki-links]] in double brackets `[[]]`, will show the backlink reference context.
12 | - Hover preview for wiki-links.
13 | - A nice interactive [[graph-view]] visualizing the relationships of your notes.
14 | - Mobile-friendly responsive design.
15 | - Local search.
16 | - Light and dark mode.
17 | - Auto-generated sidebar based on notes directory.
18 | - Auto-generated [[table-of-contents]].
19 | - Configurable `mdx` processing system, with the power of `gatsby-plugin-mdx`.
20 | - Page customization by [[using-frontmatter]].
21 |
22 | This demo site has some extra gatsby config apart from `gatsby-theme-kb` itself. You can [find them on github](https://github.com/hikerpig/gatsby-project-kb/blob/master/demo/gatsby-config.js).
23 |
24 | ## Working with knowledge management tools
25 |
26 | ### Foam
27 |
28 | [Foam](https://foambubble.github.io/foam/) is a personal knowledge management and sharing system inspired by Roam Research, built on Visual Studio Code and GitHub.
29 |
30 | But it doesn't bundle with an official publishing system (yet).
31 |
32 | And `gatsby-theme-kb` is one of the few Foam publishing solutions that support note graph rendering.
33 |
34 | With the help of [Foam for VSCode](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode) plugin auto-creating link definitions, your knowledge base can have nested folders.
35 |
36 | I've created a Foam template [foam-template-gatsby-kb](https://github.com/hikerpig/foam-template-gatsby-kb/) to help you start. It can also be used to publish an Obsidian vault. There are some detailed instructions in its readme.
37 |
38 | ### Obsidian
39 |
40 | This theme can be used to publish an Obsidian project.
41 |
42 | Though currently this is far less versatile than Obsidian's publishing system. e.g. lack of tagging system.
43 |
44 | But this is free and open, you can always extend it as you wish.
45 |
46 | ## Extending it in your Gatsby site
47 |
48 | - [Shadowing in Gatsby Themes](https://www.gatsbyjs.com/docs/how-to/plugins-and-themes/shadowing/), Gatsby offical tutorial about extending a theme.
49 | - Extend `gatsby-theme-kb` to [[comment-system]].
50 | - [[override-styles]] shows some approaches to change the appearance of the site.
51 |
52 | ## Any Thoughts to make this better?
53 |
54 | Welcome to open issues and PRs on [github repo](https://github.com/hikerpig/gatsby-project-kb).
55 |
56 | [wiki-links]: ./features/wiki-links.md
57 | [graph-view]: ./features/graph-view.md
58 | [using-frontmatter]: ./features/using-frontmatter.md
59 | [comment-system]: ./examples/comment-system
60 | [override-styles]: ./examples/override-styles
61 |
--------------------------------------------------------------------------------
/demo/content/showcase.md:
--------------------------------------------------------------------------------
1 | # Showcase
2 |
3 | - [My own wiki](http://wiki.hikerpig.cn/), there are some custom gatsby configs, you can check that in [github repo](https://github.com/hikerpig/wiki).
--------------------------------------------------------------------------------
/demo/content/todo.md:
--------------------------------------------------------------------------------
1 | # Todo
2 |
3 | - [X] Setup the theme
4 | - [X] Complete the graph view
5 | - [X] Add local search
6 | - [X] Responsive design
7 | - [X] Collapsable sidebar tree
8 | - [X] Support nested folder without the chore of manually adding link definitions
9 | - [X] Make Mobile TOC view available in mobile
10 | - [X] Better style for graph-view
11 | - [ ] Knobs to config the force graph in graph-view
12 |
--------------------------------------------------------------------------------
/demo/gatsby-browser.js:
--------------------------------------------------------------------------------
1 | import './src/styles/prism-theme.css'
2 | import './src/styles/main.css'
3 |
--------------------------------------------------------------------------------
/demo/gatsby-config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | const pathPrefix = process.env.KB_BASE_PATH || '/'
4 |
5 | module.exports = {
6 | pathPrefix,
7 | siteMetadata: {
8 | title: `gatsby-theme-kb`,
9 | description: `Your personal knowledge base`,
10 | author: `@hikerpig`,
11 | },
12 | plugins: [
13 | {
14 | resolve: 'gatsby-theme-kb',
15 | options: {
16 | contentPath: path.resolve(__dirname, 'content'),
17 | wikiLinkLabelTemplate: '[[{{ title }}]]',
18 | getPluginMdx(defaultPluginMdx) {
19 | defaultPluginMdx.options.gatsbyRemarkPlugins.push({
20 | resolve: 'gatsby-remark-prismjs',
21 | options: {
22 | noInlineHighlight: true,
23 | },
24 | })
25 | return defaultPluginMdx
26 | },
27 | },
28 | },
29 | 'gatsby-plugin-no-sourcemaps',
30 | ],
31 | }
32 |
--------------------------------------------------------------------------------
/demo/gatsby-ssr.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/no-danger */
2 | import React from 'react'
3 |
4 | /**
5 | * Implement Gatsby's SSR (Server Side Rendering) APIs in this file.
6 | *
7 | * See: https://www.gatsbyjs.org/docs/ssr-apis/
8 | */
9 |
10 | export const onPreRenderHTML = ({
11 | getPreBodyComponents,
12 | replacePreBodyComponents,
13 | }) => {
14 | const preBodyComponents = getPreBodyComponents()
15 |
16 | const cornerText = `
17 |
18 | `
19 |
20 | preBodyComponents.push(
)
21 | replacePreBodyComponents(preBodyComponents)
22 | }
23 |
--------------------------------------------------------------------------------
/demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "version": "1.4.6",
4 | "name": "demo",
5 | "license": "MIT",
6 | "scripts": {
7 | "start": "develop",
8 | "build": "gatsby build --prefix-paths",
9 | "develop": "gatsby develop",
10 | "clean": "gatsby clean"
11 | },
12 | "dependencies": {
13 | "gatsby": "^4.4.0",
14 | "gatsby-plugin-no-sourcemaps": "^2.8.0",
15 | "gatsby-remark-prismjs": "^3.13.0",
16 | "gatsby-theme-kb": "^0.9.4",
17 | "prismjs": "^1.23.0",
18 | "react": "^17",
19 | "react-dom": "^17"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/demo/src/gatsby-theme-kb/components/Topic/Comment/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, memo } from 'react'
2 | import useDarkMode from 'use-dark-mode'
3 |
4 | type Props = {
5 | issueTerm: string
6 | }
7 |
8 | // you could choose other themes too
9 | const UTTERANCES_THEME_MAP = {
10 | light: 'github-light',
11 | dark: 'dark-blue',
12 | }
13 |
14 | // please change it to your own and make sure your repo has installed utterances app
15 | const REPO_NAME = 'hikerpig/gatsby-project-kb'
16 |
17 | const Comment: React.FC = memo(({ issueTerm }) => {
18 | const { value: isDark } = useDarkMode(false)
19 | const commentsUUID = `comments_${issueTerm}`
20 |
21 | useEffect(() => {
22 | let anchor
23 | const theme = isDark ? UTTERANCES_THEME_MAP.dark: UTTERANCES_THEME_MAP.light
24 | const script = document.createElement('script')
25 | anchor = document.getElementById(commentsUUID)
26 | script.setAttribute('src', 'https://utteranc.es/client.js')
27 | script.setAttribute('crossorigin', 'anonymous')
28 | script.async = true
29 | script.setAttribute('repo', REPO_NAME)
30 | script.setAttribute('issue-term', issueTerm)
31 | script.setAttribute('theme', theme)
32 | script.setAttribute('label', 'comment')
33 | anchor.appendChild(script)
34 | return () => {
35 | anchor.innerHTML = ''
36 | }
37 | })
38 |
39 | return (
40 | <>
41 |
48 | >
49 | )
50 | })
51 |
52 | export default Comment
53 |
--------------------------------------------------------------------------------
/demo/src/gatsby-theme-kb/components/Topic/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ThemeTopic, {
3 | Props as ThemeTopicProps,
4 | } from 'gatsby-theme-kb/src/components/Topic'
5 | import Comment from './Comment'
6 |
7 | interface TopicProps extends ThemeTopicProps {}
8 |
9 | const Topic = (props: TopicProps) => {
10 | return (
11 | <>
12 |
13 |
14 |
15 |
16 | >
17 | )
18 | }
19 |
20 | export default Topic
21 |
--------------------------------------------------------------------------------
/demo/src/styles/main.css:
--------------------------------------------------------------------------------
1 | /* ---------------- prism override ---------------------- */
2 | pre[class*="language-"] {
3 | margin: 1em 0;
4 | }
5 |
6 | .dark-mode pre[class*="language-"] {
7 | background-color: #252931;
8 | }
9 |
10 | .light-mode pre[class*="language-"] {
11 | background-color: #2b2727;
12 | }
13 |
14 | /* ----------------end prism override ------------------- */
15 |
16 | code {
17 | font-size: 0.9em;
18 | }
19 |
20 |
--------------------------------------------------------------------------------
/demo/src/styles/prism-theme.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Nord Theme Originally by Arctic Ice Studio
3 | * https://nordtheme.com
4 | *
5 | * Ported for PrismJS by Zane Hitchcoxc (@zwhitchcox) and Gabriel Ramos (@gabrieluizramos)
6 | */
7 |
8 | code[class*="language-"],
9 | pre[class*="language-"] {
10 | color: #f8f8f2;
11 | background: none;
12 | font-family: "Fira Code", Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
13 | text-align: left;
14 | white-space: pre;
15 | word-spacing: normal;
16 | word-break: normal;
17 | word-wrap: normal;
18 | line-height: 1.5;
19 | -moz-tab-size: 4;
20 | -o-tab-size: 4;
21 | tab-size: 4;
22 | -webkit-hyphens: none;
23 | -moz-hyphens: none;
24 | -ms-hyphens: none;
25 | hyphens: none;
26 | }
27 |
28 | /* Code blocks */
29 | pre[class*="language-"] {
30 | padding: 1em;
31 | margin: .5em 0;
32 | overflow: auto;
33 | border-radius: 0.3em;
34 | }
35 |
36 | :not(pre) > code[class*="language-"],
37 | pre[class*="language-"] {
38 | background: #2E3440;
39 | }
40 |
41 | /* Inline code */
42 | :not(pre) > code[class*="language-"] {
43 | padding: .1em;
44 | border-radius: .3em;
45 | white-space: normal;
46 | }
47 |
48 | .token.comment,
49 | .token.prolog,
50 | .token.doctype,
51 | .token.cdata {
52 | color: #636f88;
53 | }
54 |
55 | .token.punctuation {
56 | color: #81A1C1;
57 | }
58 |
59 | .namespace {
60 | opacity: .7;
61 | }
62 |
63 | .token.property,
64 | .token.tag,
65 | .token.constant,
66 | .token.symbol,
67 | .token.deleted {
68 | color: #81A1C1;
69 | }
70 |
71 | .token.number {
72 | color: #B48EAD;
73 | }
74 |
75 | .token.boolean {
76 | color: #81A1C1;
77 | }
78 |
79 | .token.selector,
80 | .token.attr-name,
81 | .token.string,
82 | .token.char,
83 | .token.builtin,
84 | .token.inserted {
85 | color: #A3BE8C;
86 | }
87 |
88 | .token.operator,
89 | .token.entity,
90 | .token.url,
91 | .language-css .token.string,
92 | .style .token.string,
93 | .token.variable {
94 | color: #81A1C1;
95 | }
96 |
97 | .token.atrule,
98 | .token.attr-value,
99 | .token.function,
100 | .token.class-name {
101 | color: #88C0D0;
102 | }
103 |
104 | .token.keyword {
105 | color: #81A1C1;
106 | }
107 |
108 | .token.regex,
109 | .token.important {
110 | color: #EBCB8B;
111 | }
112 |
113 | .token.important,
114 | .token.bold {
115 | font-weight: bold;
116 | }
117 |
118 | .token.italic {
119 | font-style: italic;
120 | }
121 |
122 | .token.entity {
123 | cursor: help;
124 | }
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "independent",
3 | "registry": "https://registry.npmjs.org/",
4 | "publishConfig": {
5 | "access": "public"
6 | },
7 | "npmClient": "yarn",
8 | "useWorkspaces": true,
9 | "packages": [
10 | "packages/*",
11 | "demo"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gatsby-project-kb",
3 | "private": true,
4 | "devDependencies": {
5 | "@types/react": "^17.0.0",
6 | "@types/react-dom": "^17.0.0",
7 | "gatsby": "^4.0.0",
8 | "lerna": "^4.0.0",
9 | "react": "^17.0.0",
10 | "react-dom": "^17.0.0",
11 | "tsdx": "^0.14.1",
12 | "typescript": "^4.1.2"
13 | },
14 | "workspaces": [
15 | "packages/*",
16 | "demo"
17 | ],
18 | "scripts": {
19 | "lerna": "lerna",
20 | "demo:clean": "yarn workspace demo clean",
21 | "dev": "yarn workspace demo develop",
22 | "demo:build": "NODE_ENV=production yarn workspace demo build",
23 | "test": "lerna run test --",
24 | "lint": "lerna run lint -- --fix",
25 | "build": "yarn demo:build",
26 | "prepublish": "lerna run prepublish",
27 | "start:app": "yarn run build && yarn --cwd example && yarn --cwd example start"
28 | },
29 | "version": null
30 | }
31 |
--------------------------------------------------------------------------------
/packages/gatsby-remark-wiki-link/.gitignore:
--------------------------------------------------------------------------------
1 | lib/
2 |
--------------------------------------------------------------------------------
/packages/gatsby-remark-wiki-link/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to this project will be documented in this file.
4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5 |
6 | # [0.4.0](https://github.com/hikerpig/gatsby-project-kb/compare/gatsby-remark-wiki-link@0.4.0-alpha.0...gatsby-remark-wiki-link@0.4.0) (2022-02-06)
7 |
8 | **Note:** Version bump only for package gatsby-remark-wiki-link
9 |
10 |
11 |
12 |
13 |
14 | ## [0.3.1](https://github.com/hikerpig/gatsby-project-kb/compare/gatsby-remark-wiki-link@0.3.0...gatsby-remark-wiki-link@0.3.1) (2021-10-24)
15 |
16 | **Note:** Version bump only for package gatsby-remark-wiki-link
17 |
18 |
19 |
20 |
21 |
22 | # [0.3.0-alpha.0](https://github.com/hikerpig/gatsby-project-kb/compare/gatsby-remark-wiki-link@0.2.0...gatsby-remark-wiki-link@0.3.0-alpha.0) (2021-04-02)
23 |
24 |
25 | ### Features
26 |
27 | * Add anchor reference support, related [#8](https://github.com/hikerpig/gatsby-project-kb/issues/8) ([3c0d13a](https://github.com/hikerpig/gatsby-project-kb/commit/3c0d13a78146dc9b6bf1215af367fbd1e3a999d4))
28 |
--------------------------------------------------------------------------------
/packages/gatsby-remark-wiki-link/README.md:
--------------------------------------------------------------------------------
1 | # `gatsby-remark-wiki-link`
2 |
3 | Transform `[[Link to page]]` into `[Link to page](titleToURL('Link to page'))`.
4 |
5 | ## Installation
6 |
7 | ```bash
8 | npm install gatsby-remark-wiki-link
9 | ```
10 |
11 | ## Usage
12 |
13 | Add the plugin to your Gatsby config:
14 |
15 | ```js
16 | {
17 | resolve: `gatsby-plugin-mdx`,
18 | options: {
19 | gatsbyRemarkPlugins: [
20 | {
21 | resolve: `gatsby-remark-wiki-link`,
22 | },
23 | ],
24 | },
25 | }
26 | ```
27 |
--------------------------------------------------------------------------------
/packages/gatsby-remark-wiki-link/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gatsby-remark-wiki-link",
3 | "version": "0.4.1",
4 | "description": "A gatsby remark plugin for parsing wiki-link in markdown",
5 | "author": "hikerpig ",
6 | "homepage": "https://github.com/hikerpig/gatsby-project-kb#readme",
7 | "license": "MIT",
8 | "main": "lib/index.js",
9 | "directories": {
10 | "lib": "lib",
11 | "test": "__tests__"
12 | },
13 | "files": [
14 | "lib"
15 | ],
16 | "repository": {
17 | "type": "git",
18 | "url": "git+https://github.com/hikerpig/gatsby-project-kb.git"
19 | },
20 | "scripts": {
21 | "prepublish": "tsc",
22 | "watch": "tsc --watch",
23 | "build": "tsc"
24 | },
25 | "bugs": {
26 | "url": "https://github.com/hikerpig/gatsby-project-kb/issues"
27 | },
28 | "devDependencies": {
29 | "@types/unist": "^2.0.6",
30 | "typescript": "^4.1.3"
31 | },
32 | "dependencies": {
33 | "slugify": "^1.4.6",
34 | "unist-util-visit": "^2.0.3"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/packages/gatsby-remark-wiki-link/src/index.ts:
--------------------------------------------------------------------------------
1 | import * as visit from 'unist-util-visit'
2 | import { Node } from 'unist'
3 | import { LinkReference, Definition, Link, Text, StaticPhrasingContent } from 'mdast'
4 | import slugify from 'slugify'
5 | import * as path from 'path'
6 |
7 | interface LinkReferenceNode extends LinkReference {
8 | url?: string;
9 | title?: string;
10 | }
11 |
12 | /**
13 | * if title is something like `folder1/folder2/name`,
14 | * will slugify the name, while keeping the folder names
15 | */
16 | const defaultTitleToURLPath = (title: string) => {
17 | const segments = title.split('/')
18 | let titleCandidate = segments.pop() as string
19 | const hashIndex = titleCandidate.indexOf('#')
20 | if (hashIndex > -1) {
21 | titleCandidate = titleCandidate.substring(0, hashIndex)
22 | }
23 | const slugifiedTitle = slugify(titleCandidate)
24 | return `${segments.join('/')}/${slugifiedTitle}`
25 | }
26 |
27 | const processWikiLinks = (
28 | { markdownAST }: { markdownAST: Node },
29 | options?: { titleToURLPath?: string; stripBrackets?: boolean, stripDefinitionExts?: string[] }
30 | ) => {
31 | const { stripDefinitionExts } = options
32 | const titleToURL = options?.titleToURLPath
33 | ? require(options.titleToURLPath)
34 | : defaultTitleToURLPath
35 |
36 | const definitions: { [identifier: string]: Definition } = {}
37 |
38 | const getLinkInfo = (definition: Definition) => {
39 | if (typeof definition.identifier !== 'string') return
40 | let linkUrl = definition.url
41 | const isExternalLink = /\/\//.test(linkUrl)
42 | let shouldReplace = !isExternalLink
43 | if (shouldReplace && stripDefinitionExts) {
44 | const extname = path.extname(definition.url || '')
45 | const matchedExtname = stripDefinitionExts.find((n) => extname === n)
46 | if (matchedExtname) {
47 | linkUrl = linkUrl.slice(0, linkUrl.length - matchedExtname.length)
48 | }
49 | }
50 | return {
51 | linkUrl,
52 | shouldReplace
53 | }
54 | }
55 |
56 | visit(markdownAST, `definition`, (node: Definition) => {
57 | if (!node.identifier || typeof node.identifier !== 'string') {
58 | return
59 | }
60 | definitions[node.identifier] = node
61 | })
62 | visit(markdownAST, `linkReference`, (node: LinkReferenceNode, index, parent) => {
63 | if (node.referenceType !== 'shortcut') {
64 | return
65 | }
66 |
67 | const definition = definitions[node.identifier]
68 | const linkInfo = definition ? getLinkInfo(definition): null
69 | const linkUrl = linkInfo ? linkInfo.linkUrl: definition?.url
70 | if ((linkInfo && !linkInfo.shouldReplace)) {
71 | // console.log('should not replace', definitions, node.identifier)
72 | return
73 | }
74 |
75 | const siblings = parent.children
76 | if (!siblings || !Array.isArray(siblings)) {
77 | return
78 | }
79 | const previous: StaticPhrasingContent = siblings[index - 1] as any
80 | const next: StaticPhrasingContent = siblings[index + 1] as any
81 |
82 | if (!(previous && next)) {
83 | return
84 | }
85 |
86 | if (!('value' in previous && 'value' in next)) {
87 | return
88 | }
89 |
90 | const previousValue = previous.value as string
91 | const nextValue = next.value as string
92 |
93 | if (
94 | previous.type !== 'text' ||
95 | previous.value[previousValue.length - 1] !== '[' ||
96 | next.type !== 'text' ||
97 | next.value[0] !== ']'
98 | ) {
99 | return
100 | }
101 |
102 | previous.value = previousValue.replace(/\[$/, '')
103 | next.value = nextValue.replace(/^\]/, '')
104 |
105 | ;(node as any).type = 'link' // cast it to link
106 | if (definition) {
107 | node.url = linkUrl
108 | } else {
109 | node.url = titleToURL(node.label as string)
110 | }
111 | node.title = node.label
112 | if (!options?.stripBrackets && Array.isArray(node.children)) {
113 | const firstChild = node.children[0];
114 | if (firstChild && 'value' in firstChild) {
115 | firstChild.value = `[[${firstChild.value}]]`
116 | }
117 | }
118 | delete node.label
119 | delete node.referenceType
120 | delete node.identifier
121 | })
122 | }
123 |
124 | // default export may have issue being loaded by gatsby-plugin-mdx
125 | // see https://github.com/gatsbyjs/gatsby/issues/34015
126 | // export default processWikiLinks
127 |
128 | export = processWikiLinks
129 |
--------------------------------------------------------------------------------
/packages/gatsby-remark-wiki-link/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2017",
4 | "outDir": "lib",
5 | "rootDir": "src",
6 | "moduleResolution": "node",
7 | "module": "commonjs",
8 | "declaration": true,
9 | "inlineSourceMap": false,
10 | "lib": ["es5", "dom"],
11 | "types": ["jest", "node"],
12 | "typeRoots": ["node_modules/@types"],
13 | "jsx": "react",
14 | "skipLibCheck": true
15 | },
16 | "include": ["src/**/*.ts"],
17 | "exclude": ["node_modules/**"],
18 | "compileOnSave": false
19 | }
20 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (http://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # Typescript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # dotenv environment variable files
55 | .env*
56 |
57 | # gatsby files
58 | .cache/
59 | public
60 |
61 | # Mac files
62 | .DS_Store
63 |
64 | # Yarn
65 | yarn-error.log
66 | .pnp/
67 | .pnp.js
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/.prettierignore:
--------------------------------------------------------------------------------
1 | .cache
2 | package.json
3 | package-lock.json
4 | public
5 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/.yarnrc:
--------------------------------------------------------------------------------
1 | registry https://registry.npmjs.org/
2 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to this project will be documented in this file.
4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5 |
6 | ## [0.9.3](https://github.com/hikerpig/gatsby-project-kb/compare/gatsby-theme-kb@0.9.2...gatsby-theme-kb@0.9.3) (2022-03-26)
7 |
8 |
9 | ### Bug Fixes
10 |
11 | * **gatsby-theme-kb:** apply options.ignore to transformer-wiki-references plugin ([9190eee](https://github.com/hikerpig/gatsby-project-kb/commit/9190eee3ec23484cabdf368793cadb8b0a3458ec))
12 | * **gatsby-theme-kb:** fix reference resolving bug when they are in nested folder ([0e99099](https://github.com/hikerpig/gatsby-project-kb/commit/0e9909994b9c33c1567e52e7d0eb0720cb937d83))
13 |
14 |
15 |
16 |
17 |
18 | ## [0.9.1](https://github.com/hikerpig/gatsby-project-kb/compare/gatsby-theme-kb@0.9.0...gatsby-theme-kb@0.9.1) (2022-03-20)
19 |
20 |
21 | ### Bug Fixes
22 |
23 | * **gatsby-theme-kb:** page name should be slugified during createPage ([#45](https://github.com/hikerpig/gatsby-project-kb/issues/45)) ([06e2741](https://github.com/hikerpig/gatsby-project-kb/commit/06e2741f748d8fd1dee3f04c37da8b902e9d3f29))
24 |
25 |
26 |
27 |
28 |
29 | # [0.9.0](https://github.com/hikerpig/gatsby-project-kb/compare/gatsby-theme-kb@0.9.0-alpha.4...gatsby-theme-kb@0.9.0) (2022-02-06)
30 |
31 | **Note:** Version bump only for package gatsby-theme-kb
32 |
33 |
34 |
35 |
36 |
37 | # [0.8.0](https://github.com/hikerpig/gatsby-project-kb/compare/gatsby-theme-kb@0.7.8...gatsby-theme-kb@0.8.0) (2021-10-24)
38 |
39 |
40 | ### Features
41 |
42 | * **demo:** an example of shadowing by adding a comment, close [#24](https://github.com/hikerpig/gatsby-project-kb/issues/24) ([5628ccf](https://github.com/hikerpig/gatsby-project-kb/commit/5628ccf2c0f57b2621398b0e82b7efefba05e065))
43 | * **gatsby-theme-kb:** Add toc menu in mobile mode ([a82e9b4](https://github.com/hikerpig/gatsby-project-kb/commit/a82e9b48f78c2ed5fbb42227cd08c40cadb9502c))
44 | * **gatsby-theme-kb:** adjust blockquote style a little ([e125ce4](https://github.com/hikerpig/gatsby-project-kb/commit/e125ce40c0c104e1d0e0a2941f933b11bc0c0c9d))
45 | * **gatsby-theme-kb:** configurable toc, both `inline` and `sidebar` ([e3be6cc](https://github.com/hikerpig/gatsby-project-kb/commit/e3be6cc9fef3e2f94a0971106a5c838c8257cf78))
46 | * **gatsby-theme-kb:** upgrade `gatsby-plugin-purgecss` ([35936a6](https://github.com/hikerpig/gatsby-project-kb/commit/35936a66b74c8421f603c0ed8d8a7d6d24a9427a))
47 |
48 |
49 |
50 |
51 |
52 | # [0.7.0-alpha.3](https://github.com/hikerpig/gatsby-project-kb/compare/gatsby-theme-kb@0.7.0-alpha.2...gatsby-theme-kb@0.7.0-alpha.3) (2021-04-28)
53 |
54 | **Note:** Version bump only for package gatsby-theme-kb
55 |
56 |
57 |
58 |
59 |
60 | # [0.7.0-alpha.2](https://github.com/hikerpig/gatsby-project-kb/compare/gatsby-theme-kb@0.7.0-alpha.1...gatsby-theme-kb@0.7.0-alpha.2) (2021-04-19)
61 |
62 | **Note:** Version bump only for package gatsby-theme-kb
63 |
64 |
65 |
66 |
67 |
68 | # [0.7.0-alpha.1](https://github.com/hikerpig/gatsby-project-kb/compare/gatsby-theme-kb@0.7.0-alpha.0...gatsby-theme-kb@0.7.0-alpha.1) (2021-04-07)
69 |
70 |
71 | ### Bug Fixes
72 |
73 | * should ignore directory in contentTree ([5d14716](https://github.com/hikerpig/gatsby-project-kb/commit/5d14716a9287ac1d12e52f43105535c851c582fb))
74 |
75 |
76 | ### Performance Improvements
77 |
78 | * optimize SiteSidebar and TreeView performance ([42c9102](https://github.com/hikerpig/gatsby-project-kb/commit/42c9102e7d7716545a26a4f5ae4dcb1855f5fbb9))
79 |
80 |
81 |
82 |
83 |
84 | # [0.7.0-alpha.0](https://github.com/hikerpig/gatsby-project-kb/compare/gatsby-theme-kb@0.6.0...gatsby-theme-kb@0.7.0-alpha.0) (2021-04-02)
85 |
86 |
87 | ### Features
88 |
89 | * Add anchor reference support, related [#8](https://github.com/hikerpig/gatsby-project-kb/issues/8) ([3c0d13a](https://github.com/hikerpig/gatsby-project-kb/commit/3c0d13a78146dc9b6bf1215af367fbd1e3a999d4))
90 | * better wiki references resolving with `contentPath` option ([46b66a9](https://github.com/hikerpig/gatsby-project-kb/commit/46b66a973bbdd702dfadb523e9ab0ab91ed1d417))
91 |
92 |
93 |
94 |
95 |
96 | ## [0.6.0](https://github.com/hikerpig/gatsby-project-kb/compare/gatsby-theme-kb@0.5.4-alpha.0...gatsby-theme-kb@0.6.0) (2021-03-05)
97 |
98 | Hello Gatsby V3.
99 |
100 | # BREAKING
101 |
102 | Now works with gatsby@3
103 |
104 | # Improvements
105 | * upgrade gatsby-plugin-mdx@2
106 | * fix(gatsby-theme-kb): sidebar expand bug with file in nested folder
107 | * style: a warmer color for light mode default text
108 |
109 |
110 | ## [0.5.4-alpha.0](https://github.com/hikerpig/gatsby-project-kb/compare/gatsby-theme-kb@0.5.3...gatsby-theme-kb@0.5.4-alpha.0) (2021-03-04)
111 |
112 | **Note:** Version bump only for package gatsby-theme-kb
113 |
114 |
115 |
116 |
117 |
118 | ## [0.5.3](https://github.com/hikerpig/gatsby-project-kb/compare/gatsby-theme-kb@0.5.2...gatsby-theme-kb@0.5.3) (2021-03-03)
119 |
120 |
121 | ### Bug Fixes
122 |
123 | * **gatsby-theme-kb:** the sidebar subdirectory should not collapse when clicking link ([3a97a55](https://github.com/hikerpig/gatsby-project-kb/commit/3a97a55b86df0f2934f311ee6b237dae3ea7b865))
124 |
125 |
126 |
127 |
128 |
129 | ## [0.5.2](https://github.com/hikerpig/gatsby-project-kb/compare/gatsby-theme-kb@0.5.1...gatsby-theme-kb@0.5.2) (2021-02-28)
130 |
131 |
132 | ### Bug Fixes
133 |
134 | * **gatsby-theme-kb:** errors when rendering custom components in mdx mentioned in [#6](https://github.com/hikerpig/gatsby-project-kb/issues/6) ([006b87c](https://github.com/hikerpig/gatsby-project-kb/commit/006b87c3372908ae09f73bb9476171dfef279e05))
135 |
136 |
137 |
138 |
139 |
140 | ## [0.5.1](https://github.com/hikerpig/gatsby-project-kb/compare/gatsby-theme-kb@0.5.0...gatsby-theme-kb@0.5.1) (2021-02-26)
141 |
142 |
143 | ### Bug Fixes
144 |
145 | * codeblock style ([a837db8](https://github.com/hikerpig/gatsby-project-kb/commit/a837db867f55af6ca4133e1eb1fb2235e543a553))
146 | * **gatsby-theme-kb:** graph view error due to lazy render ([fea20ff](https://github.com/hikerpig/gatsby-project-kb/commit/fea20ffbb4262e36d5adf707159f13c088d8842c))
147 |
148 |
149 |
150 |
151 |
152 | # [0.5.0](https://github.com/hikerpig/gatsby-project-kb/compare/gatsby-theme-kb@0.4.3...gatsby-theme-kb@0.5.0) (2021-02-20)
153 |
154 |
155 | ### Features
156 |
157 | * add search in graph view and make it interactively highlighting result nodes ([0323db9](https://github.com/hikerpig/gatsby-project-kb/commit/0323db9ca8f8169d001b021724ca49714b5f10e4))
158 |
159 |
160 |
161 |
162 |
163 | ## [0.4.3](https://github.com/hikerpig/gatsby-project-kb/compare/gatsby-theme-kb@0.4.2...gatsby-theme-kb@0.4.3) (2021-02-15)
164 |
165 |
166 | ### Bug Fixes
167 |
168 | * modify gatsby-plugin-purgecss to make it work in foam-template-gatsby-kb ([eea174a](https://github.com/hikerpig/gatsby-project-kb/commit/eea174afb86a8852e7e70bdfcfe09f0e52cfd700))
169 | * multiple note-graph ini in GraphView when toggling graphState ([51581b7](https://github.com/hikerpig/gatsby-project-kb/commit/51581b7396b2edc00b9cb01a506d726eecc03036))
170 |
171 |
172 |
173 |
174 |
175 | ## [0.4.2](https://github.com/hikerpig/gatsby-project-kb/compare/gatsby-theme-kb@0.4.1...gatsby-theme-kb@0.4.2) (2021-02-14)
176 |
177 |
178 | ### Bug Fixes
179 |
180 | * **gatsby-theme-kb:** sidebar treeview with nested folders ([77c6e2f](https://github.com/hikerpig/gatsby-project-kb/commit/77c6e2f4635010cc5db8037d089a04e346bfcc8d))
181 |
182 |
183 |
184 |
185 |
186 | ## [0.4.1](https://github.com/hikerpig/gatsby-project-kb/compare/gatsby-theme-kb@0.4.0...gatsby-theme-kb@0.4.1) (2021-02-14)
187 |
188 |
189 | ### Bug Fixes
190 |
191 | * **gatsby-theme-kb:** misfunction in AnchorTag caused by v0.3.0 ([7739b49](https://github.com/hikerpig/gatsby-project-kb/commit/7739b496866eb6573dad0600fa252cd292aa1348))
192 | * backlink should show referrer title and link ([8e89b4d](https://github.com/hikerpig/gatsby-project-kb/commit/8e89b4d22f85a2dc3b0f4902f9530a4692e81161))
193 |
194 |
195 |
196 |
197 |
198 | # [0.4.0](https://github.com/hikerpig/gatsby-project-kb/compare/gatsby-theme-kb@0.3.0...gatsby-theme-kb@0.4.0) (2021-02-13)
199 |
200 |
201 | ### Features
202 |
203 | * **gatsby-theme-kb:** show backlink context, related [#1](https://github.com/hikerpig/gatsby-project-kb/issues/1) ([685b92c](https://github.com/hikerpig/gatsby-project-kb/commit/685b92c3970116cc593581f52ecc6e0b66b0c146))
204 |
205 |
206 | ## 0.3.1 (2021-02-13)
207 |
208 | ### Features
209 |
210 | * **gatsby-theme-kb:** initially collapsed directory node ([cd0aae4](https://github.com/hikerpig/gatsby-project-kb/commit/cd0aae468c7c7755da813615c7a24c81431f53cb))
211 | * optimize search result background in dark mode ([9af3ca3](https://github.com/hikerpig/gatsby-project-kb/commit/9af3ca3aa725f25f83303a17c922db8802c007e6))
212 | * **gatsby-remark-wiki-link:** support `options.stripDefinitionExts ([7999c4c](https://github.com/hikerpig/gatsby-project-kb/commit/7999c4ce2ed89e313a9bd922c4582c3b0e457fdf))
213 | * **gatsby-theme-kb:** `options.getPluginMdx` ([c307ae5](https://github.com/hikerpig/gatsby-project-kb/commit/c307ae530806797cac3974a1bcea480e931d730d))
214 | * add custom package transformer-wiki-references and gatsby-remark-wiki-link ([237c94f](https://github.com/hikerpig/gatsby-project-kb/commit/237c94f06b79f14124fbcebca10979bacf758de5))
215 | * new package gatsby-remark-wiki-link ([f47ea2a](https://github.com/hikerpig/gatsby-project-kb/commit/f47ea2acdd9fecf1d758df610a8e2e7726fcbf07))
216 |
217 |
218 | ### Bug Fixes
219 |
220 | * **gatsby-remark-wiki-link:** index error ([8fd4636](https://github.com/hikerpig/gatsby-project-kb/commit/8fd4636654fc9406389c61ba52d602009a3cb700))
221 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/README.md:
--------------------------------------------------------------------------------
1 | gatsby-theme-kb
2 | ===
3 |
4 | A Gatsby theme for publishing **K**nowledge **B**ase.
5 |
6 | See the [demo and documentation](https://gatsby-project-kb.vercel.app/).
7 |
8 | [](https://gatsby-project-kb.vercel.app/)
9 |
10 | # Setup in your Gatsby project
11 |
12 | 1. Install dependency
13 |
14 | ```
15 | yarn add gatsby-theme-kb
16 | ```
17 |
18 | 2. Add these to your gatsby-config.js file:
19 |
20 | ```js
21 | module.exports = {
22 | plugins: [
23 | {
24 | resolve: `gatsby-theme-kb`,
25 | options: {
26 | contentPath: path.resolve(__dirname, 'content'),
27 | rootNote: 'readme',
28 | wikiLinkLabelTemplate: '[[{{ refWord }}]]',
29 | getPluginMdx(defaultPluginMdx) {
30 | // customise pre-configured `gatsby-plugin-mdx`, for example:
31 | // defaultPluginMdx.options.gatsbyRemarkPlugins.push({
32 | // resolve: 'gatsby-remark-prismjs',
33 | // })
34 | return defaultPluginMdx
35 | },
36 | },
37 | },
38 | ],
39 | };
40 | ```
41 |
42 | 3. Add notes to your site by adding `md` or `mdx` files in `content` directory, especially you need a `content/readme.md` file if you are using above configs.
43 |
44 | 4. Start developing your site by running `gatsby develop`. If you are using above configuration, your start url will be 'http://localhost:8000'.
45 |
46 | # Usage
47 |
48 | ## Options
49 |
50 | | Key | Default value | Description |
51 | |:----------------------:|:-------------:|:----------------------------------------------------------------------------:|
52 | | rootNote | `/readme` | Root note's name (without exts)
53 | | contentPath | | Location of local content |
54 | | extensions | `['.md', '.mdx']` | Valid content file exts |
55 | | ignore | `['.git']` | A list of file globs to ignore |
56 | | wikiLinkLabelTemplate | | A template string for specifying wiki link label, see [ options.wikiLinkLabelTemplate](# options.wikiLinkLabelTemplate) |
57 | | getPluginMdx | (defaultPluginMdx) => PluginMdx | Customise pre-configured `gatsby-plugin-mdx`, please do always return a valid gatsby plugin object |
58 | | tocTypes | `['sidebar']` | Customise the toc location, type is `false \| Array<'inline' | 'sidebar'>` |
59 | | slugifyFn | `(name) => require('slugify')(name)` | Customise the url slug of a given file name |
60 |
61 |
62 | ### options.wikiLinkLabelTemplate
63 |
64 | When a wikilink is resolved and rendered as an anchor element, the anchor label is by default `[[reference-word]]`. But some people may prefer some other forms, so here is one option for specifying the link label you want.
65 |
66 | The template string will be processed in a mustache alike manner, the variable inside `{{}}` will be replaced by real value. Currently there are some variables available:
67 |
68 | - `refWord`, the reference word inside the double brackets, usually it's the filename (without exts).
69 | - `title`, the title of the page, may be the frontmatter `title` field value, or h1 of the markdown content.
70 |
71 | For example there is page A, filename is `page-a.md`, page title is `Awesome Themes`.
72 |
73 | And in page B I write the reference as `[[page-a]]`.
74 |
75 | - config `wikiLinkLabelTemplate: '[[ {{refWord}} ]]'`, will generate `[[ page-a ]]` as link label.
76 | - config `wikiLinkLabelTemplate: '{{title}}'`, will generate `Awesome Themes` as link label.
77 |
78 |
79 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/fragments.js:
--------------------------------------------------------------------------------
1 | import { graphql } from "gatsby";
2 |
3 | export const references = graphql`
4 | fragment GatsbyGardenReferences on Mdx {
5 | outboundReferences {
6 | contextLine
7 | targetAnchor
8 | refWord
9 | label
10 | target {
11 | ... on Mdx {
12 | body
13 | parent {
14 | id
15 | ... on File {
16 | fields {
17 | slug
18 | title
19 | }
20 | }
21 | }
22 | }
23 | }
24 | }
25 | inboundReferences {
26 | contextLine
27 | referrer {
28 | ... on Mdx {
29 | parent {
30 | id
31 | ... on File {
32 | fields {
33 | slug
34 | title
35 | }
36 | }
37 | }
38 | }
39 | }
40 | }
41 | }
42 | `;
43 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/gatsby-browser.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Implement Gatsby's Browser APIs in this file.
3 | *
4 | * See: https://www.gatsbyjs.com/docs/browser-apis/
5 | */
6 |
7 | // You can delete this file if you're not using it
8 |
9 | import './src/styles/global.css'
10 |
11 | import './src/styles/base.css'
12 | import './src/styles/theme.css'
13 |
14 | import './src/styles/tocbot.css'
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/gatsby-config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const makeSearchPlugins = require('./gatsby-node-utils/makeSearchPlugins')
3 |
4 | module.exports = function (options) {
5 | const {
6 | contentPath = 'content',
7 | mdxOtherwiseConfigured = false,
8 | ignore = ['.git'],
9 | extensions = [`.md`, `.mdx`],
10 | } = options
11 |
12 | // console.log('options', arguments)
13 | const defaultGetPluginMdx = () => {
14 | return {
15 | resolve: `gatsby-plugin-mdx`,
16 | options: {
17 | extensions,
18 | remarkPlugins: [],
19 | gatsbyRemarkPlugins: [
20 | {
21 | resolve: 'gatsby-remark-wiki-link',
22 | options: {
23 | stripBrackets: false,
24 | stripDefinitionExts: extensions,
25 | },
26 | },
27 | 'gatsby-remark-double-parenthesis-link',
28 | ],
29 | },
30 | }
31 | }
32 |
33 | const pluginMdx = mdxOtherwiseConfigured
34 | ? null
35 | : options.getPluginMdx
36 | ? options.getPluginMdx(defaultGetPluginMdx())
37 | : defaultGetPluginMdx()
38 |
39 | // console.log('plugin mdx', pluginMdx)
40 |
41 | return {
42 | plugins: [
43 | {
44 | resolve: `gatsby-plugin-typescript`,
45 | options: {
46 | isTSX: true, // defaults to false
47 | jsxPragma: `jsx`, // defaults to "React"
48 | allExtensions: true, // defaults to false
49 | },
50 | },
51 | `gatsby-plugin-react-helmet`,
52 | {
53 | resolve: 'gatsby-source-filesystem',
54 | options: {
55 | path: contentPath,
56 | name: contentPath,
57 | ignore,
58 | },
59 | },
60 | pluginMdx,
61 | {
62 | resolve: '@gatsby-project-kb/transformer-wiki-references',
63 | options: {
64 | contentPath: path.resolve(process.cwd(), contentPath),
65 | ignore,
66 | extensions,
67 | },
68 | },
69 | 'gatsby-plugin-postcss',
70 | {
71 | resolve: 'gatsby-plugin-purgecss',
72 | options: {
73 | printRejected: true,
74 | tailwind: true,
75 | purgeOnly: ['src/styles/global.css'],
76 | purgeCSSOptions: {
77 | content: [path.join(__dirname, 'src/**/*.{ts,js,jsx,tsx}')],
78 | }
79 | },
80 | },
81 | {
82 | resolve: 'gatsby-plugin-tocbot',
83 | options: {
84 | tocbotOptions: {
85 | contentSelector: '.topic-layout__content',
86 | collapseDepth: 5,
87 | scrollContainer: '.topic-layout__content',
88 | },
89 | },
90 | },
91 | ...makeSearchPlugins(options),
92 | ],
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/gatsby-node-utils/makeSearchPlugins.js:
--------------------------------------------------------------------------------
1 | const shouldHandleFile = require('./shouldHandleFile')
2 |
3 | module.exports = function makeSearchPlugins (options) {
4 | const { contentPath } = options;
5 |
6 | if (!contentPath) {
7 | return [];
8 | }
9 |
10 | const filesPath = `
11 | {
12 | allFile {
13 | nodes {
14 | id
15 | ext
16 | sourceInstanceName
17 | fields {
18 | title
19 | slug
20 | }
21 | childMdx {
22 | excerpt
23 | rawBody
24 | }
25 | internal {
26 | mediaType
27 | }
28 | }
29 | }
30 | }
31 | `;
32 |
33 | const query = filesPath
34 |
35 | return [
36 | {
37 | resolve: "gatsby-plugin-local-search",
38 | options: {
39 | name: "paths",
40 | engine: "flexsearch",
41 | query,
42 |
43 | index: ["path"],
44 |
45 | store: ["id", "path", "title", "excerpt"],
46 |
47 | normalizer: ({ data }) => {
48 | let result = [];
49 | if (contentPath) {
50 | result = result.concat(
51 | data.allFile.nodes
52 | .filter((node) => shouldHandleFile(node, options))
53 | .map((node) => ({
54 | id: node.id,
55 | path: node.fields.slug,
56 | title: node.fields.title,
57 | // Replace weirdly formatted [ link ] in excerpt to look like wikilinks ([link])
58 | excerpt: node.childMdx.excerpt.replace(
59 | /\[\s([\w'-]+)\s\]/gi,
60 | (_, p1) => `[${p1}]`
61 | ),
62 | }))
63 | );
64 | }
65 | return result;
66 | },
67 | },
68 | },
69 | {
70 | resolve: "gatsby-plugin-local-search",
71 | options: {
72 | name: "titles",
73 | engine: "flexsearch",
74 | query,
75 |
76 | index: ["title"],
77 |
78 | store: [],
79 |
80 | normalizer: ({ data }) => {
81 | let result = [];
82 | if (contentPath) {
83 | result = result.concat(
84 | data.allFile.nodes
85 | .filter((node) => shouldHandleFile(node, options))
86 | .map((node) => ({
87 | id: node.id,
88 | title: node.fields.title,
89 | }))
90 | );
91 | }
92 | return result;
93 | },
94 | },
95 | },
96 | {
97 | resolve: "gatsby-plugin-local-search",
98 | options: {
99 | name: "bodies",
100 | engine: "flexsearch",
101 | query,
102 |
103 | index: ["body"],
104 |
105 | store: [],
106 |
107 | normalizer: ({ data }) => {
108 | let result = [];
109 | if (contentPath) {
110 | result = result.concat(
111 | data.allFile.nodes
112 | .filter((node) => shouldHandleFile(node, options))
113 | .map((node) => ({
114 | id: node.id,
115 | body: node.childMdx.rawBody,
116 | }))
117 | );
118 | }
119 | return result;
120 | },
121 | },
122 | },
123 | ];
124 | };
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/gatsby-node-utils/shouldHandleFile.js:
--------------------------------------------------------------------------------
1 | module.exports = function shouldHandleFile(node, options = {}) {
2 | return (
3 | ((options.extensions || ['.md', '.mdx']).includes(node.ext) ||
4 | (options.mediaTypes || ['text/markdown', 'text/x-markdown']).includes(
5 | node.internal.mediaType
6 | )) &&
7 | node.sourceInstanceName === options.contentPath
8 | )
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/gatsby-node-utils/source-nodes.js:
--------------------------------------------------------------------------------
1 |
2 | exports.sourceNodes = (api, pluginOptions) => {
3 | console.log('sourceNodes', api)
4 | }
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/gatsby-node.js:
--------------------------------------------------------------------------------
1 | //@ts-check
2 | const fs = require(`fs`)
3 | const path = require(`path`)
4 | const { urlResolve } = require(`gatsby-core-utils`)
5 | const shouldHandleFile = require('./gatsby-node-utils/shouldHandleFile')
6 | const slugify = require(`slugify`)
7 | const {
8 | findTopLevelHeading,
9 | } = require(`@gatsby-project-kb/transformer-wiki-references`)
10 |
11 | // These are customizable theme options we only need to check once
12 | let contentPath
13 | let rootNoteSlug
14 | // let extensions
15 | // let mediaTypes
16 | let wikiLinkLabelTemplate
17 | let tocTypes = ['sidebar']
18 | let slugifyFn = defaultSlugifyFn
19 |
20 | function padSlugLeading(str) {
21 | if (typeof str !== 'string') return str
22 | if (!str.startsWith('/')) str = '/' + str
23 | return str
24 | }
25 |
26 | exports.onPreBootstrap = async ({ store }, themeOptions) => {
27 | contentPath = themeOptions.contentPath
28 | rootNoteSlug = padSlugLeading(themeOptions.rootNote) || '/readme'
29 | // extensions = themeOptions.extensions || ['.md', '.mdx']
30 | // mediaTypes = themeOptions.mediaTypes || ['text/markdown', 'text/x-markdown']
31 | wikiLinkLabelTemplate =
32 | themeOptions.wikiLinkLabelTemplate || wikiLinkLabelTemplate
33 |
34 | if (themeOptions.slugifyFn && typeof themeOptions.slugifyFn === 'function') {
35 | slugifyFn = themeOptions.slugifyFn
36 | }
37 |
38 | if ('tocTypes' in themeOptions) {
39 | tocTypes = themeOptions.tocTypes
40 | }
41 | }
42 |
43 | function getTitle(node, content) {
44 | if (
45 | typeof node.frontmatter === 'object' &&
46 | node.frontmatter &&
47 | node.frontmatter['title']
48 | ) {
49 | return node.frontmatter['title']
50 | }
51 | return (
52 | findTopLevelHeading(content) ||
53 | (typeof node.fileAbsolutePath === 'string'
54 | ? path.basename(
55 | node.fileAbsolutePath,
56 | path.extname(node.fileAbsolutePath)
57 | )
58 | : '') ||
59 | (typeof node.absolutePath === 'string'
60 | ? path.basename(node.absolutePath, path.extname(node.absolutePath))
61 | : '')
62 | )
63 | }
64 |
65 | function defaultSlugifyFn(str) {
66 | return slugify.default(str)
67 | }
68 |
69 | exports.createResolvers = ({ createResolvers }) => {
70 | const resolvers = {
71 | MdxFrontmatter: {
72 | private: {
73 | type: `Boolean`,
74 | resolve(source, args, context, info) {
75 | const { private } = source
76 | if (private == null) {
77 | return false
78 | }
79 | return private
80 | },
81 | },
82 | },
83 | }
84 | createResolvers(resolvers)
85 | }
86 |
87 | exports.onCreateNode = async ({ node, actions, loadNodeContent }, options) => {
88 | const { createNodeField } = actions
89 | if (node.internal.type === `File` && shouldHandleFile(node, options)) {
90 | const slugifiedName = slugifyFn(node.name)
91 | const slug = '/' + urlResolve(path.parse(node.relativePath).dir, slugifiedName)
92 | // console.log('slug is', slug, node.relativePath)
93 | createNodeField({
94 | node,
95 | name: `slug`,
96 | value: slug,
97 | })
98 | createNodeField({
99 | node,
100 | name: `title`,
101 | value: getTitle(node, await loadNodeContent(node)),
102 | })
103 | }
104 | }
105 |
106 | exports.createPages = async ({ graphql, actions }, options) => {
107 | const { createPage } = actions
108 |
109 | if (contentPath) {
110 | const result = await graphql(
111 | `
112 | {
113 | allFile {
114 | nodes {
115 | id
116 | sourceInstanceName
117 | ext
118 | internal {
119 | mediaType
120 | }
121 | fields {
122 | slug
123 | }
124 | childMdx {
125 | id
126 | frontmatter {
127 | private
128 | }
129 | outboundReferences {
130 | refWord
131 | label
132 | target {
133 | ... on Mdx {
134 | id
135 | slug
136 | }
137 | }
138 | }
139 | }
140 | }
141 | }
142 | allMdx {
143 | nodes {
144 | id
145 | slug
146 | outboundReferences {
147 | refWord
148 | label
149 | target {
150 | ... on Mdx {
151 | id
152 | slug
153 | }
154 | }
155 | }
156 | parent {
157 | ... on File {
158 | id
159 | }
160 | }
161 | }
162 | }
163 | }
164 | `
165 | )
166 |
167 | if (result.errors) {
168 | console.log(result.errors)
169 | throw new Error(`Could not query notes`, result.errors)
170 | }
171 |
172 | const TopicTemplate = require.resolve(
173 | options.topicTemplate || `./src/templates/Topic`
174 | )
175 |
176 | const mdxNodeMap = new Map()
177 | result.data.allMdx.nodes.forEach((mdxNode) => {
178 | mdxNodeMap.set(mdxNode.id, mdxNode)
179 | })
180 |
181 | const getContextByNode = (n) => {
182 | const refWordMdxSlugDict = {}
183 |
184 | const enrichRefDetails = (mdxNode) => {
185 | if (mdxNode && mdxNode.outboundReferences) {
186 | mdxNode.outboundReferences.forEach((ref) => {
187 | const refMdxNode = mdxNodeMap.get(ref.target.id)
188 | // console.log(
189 | // 'refMdxNode exists: ',
190 | // Boolean(refMdxNode),
191 | // ref.target.id
192 | // )
193 | if (refMdxNode) {
194 | // console.log(`${ref.refWord}: ${refMdxNode.slug}`)
195 | if (refWordMdxSlugDict[ref.refWord]) {
196 | return // prevent cycles
197 | }
198 | refWordMdxSlugDict[ref.refWord] = refMdxNode.slug
199 | enrichRefDetails(refMdxNode)
200 | }
201 | })
202 | }
203 | }
204 |
205 | enrichRefDetails(n.childMdx)
206 |
207 | return {
208 | id: n.id,
209 | wikiLinkLabelTemplate,
210 | refWordMdxSlugDict,
211 | tocTypes,
212 | }
213 | }
214 |
215 | const localFiles = result.data.allFile.nodes
216 | .filter((node) => shouldHandleFile(node, options))
217 | .filter((x) => x.childMdx.frontmatter.private !== true)
218 |
219 | localFiles.forEach((node) => {
220 | createPage({
221 | path: node.fields.slug,
222 | component: TopicTemplate,
223 | context: getContextByNode(node),
224 | })
225 | })
226 |
227 | if (rootNoteSlug) {
228 | const root = localFiles.find((node) => node.fields.slug === rootNoteSlug)
229 | // console.log('root is', root, 'rootNoteSlug', rootNoteSlug)
230 | if (root) {
231 | createPage({
232 | path: '/',
233 | component: TopicTemplate,
234 | context: getContextByNode(root),
235 | })
236 | }
237 | }
238 | }
239 | }
240 |
241 | exports.onCreateWebpackConfig = ({ actions }) => {
242 | actions.setWebpackConfig({
243 | resolve: {
244 | alias: {
245 | path: require.resolve('path-browserify'),
246 | },
247 | fallback: {
248 | fs: false,
249 | },
250 | },
251 | })
252 | }
253 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/gatsby-ssr.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Implement Gatsby's SSR (Server Side Rendering) APIs in this file.
3 | *
4 | * See: https://www.gatsbyjs.com/docs/ssr-apis/
5 | */
6 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gatsby-theme-kb",
3 | "description": "A Gatsby theme for publishing Knowledge Base.",
4 | "version": "0.9.4",
5 | "dependencies": {
6 | "@gatsby-project-kb/transformer-wiki-references": "^0.3.1",
7 | "@mdx-js/mdx": "^1.6.22",
8 | "@mdx-js/react": "^1.6.22",
9 | "@reecelucas/react-use-hotkeys": "^1.3.1",
10 | "@tippyjs/react": "^4.2.6",
11 | "autoprefixer": "^10.4.0",
12 | "classnames": "^2.3.1",
13 | "downshift": "^6.1.7",
14 | "flexsearch": "^0.6.32",
15 | "gatsby-plugin-local-search": "^2.0.1",
16 | "gatsby-plugin-mdx": "^3.4.0",
17 | "gatsby-plugin-postcss": "^5.4.0",
18 | "gatsby-plugin-purgecss": "^6.1.0",
19 | "gatsby-plugin-react-helmet": "^5.4.0",
20 | "gatsby-plugin-tocbot": "^0.1.0",
21 | "gatsby-plugin-typescript": "^4.4.0",
22 | "gatsby-react-router-scroll": "^5.4.0",
23 | "gatsby-remark-double-parenthesis-link": "^0.1.5",
24 | "gatsby-remark-wiki-link": "^0.4.1",
25 | "gatsby-source-filesystem": "^4.4.0",
26 | "note-graph": "^0.3.0",
27 | "path-browserify": "^1.0.1",
28 | "postcss": "^8.4.5",
29 | "prop-types": "^15.7.2",
30 | "react": "^17.0.2",
31 | "react-dom": "^17.0.2",
32 | "react-helmet": "^6.1.0",
33 | "tailwindcss": "^2.2.9",
34 | "use-breakpoint": "^3.0.0",
35 | "use-dark-mode": "^2.3.1"
36 | },
37 | "devDependencies": {
38 | "@types/node": "^17.0.1",
39 | "prettier": "2.5.1",
40 | "typescript": "^4.5.4"
41 | },
42 | "peerDependencies": {
43 | "gatsby": "^4.4.0"
44 | },
45 | "keywords": [
46 | "gatsby",
47 | "gatsby-plugin",
48 | "gatsby-theme",
49 | "knowledge-base",
50 | "bidirectional-link",
51 | "note-graph"
52 | ],
53 | "author": {
54 | "name": "hikerpig",
55 | "url": "https://github.com/hikerpig"
56 | },
57 | "license": "MIT",
58 | "scripts": {
59 | "build": "gatsby build",
60 | "develop": "gatsby develop",
61 | "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"",
62 | "start": "npm run develop",
63 | "serve": "gatsby serve",
64 | "clean": "gatsby clean",
65 | "typecheck": "tsc --noEmit",
66 | "test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1"
67 | },
68 | "repository": {
69 | "type": "git",
70 | "url": "https://github.com/hikerpig/gatsby-project-kb.git"
71 | },
72 | "bugs": {
73 | "url": "https://github.com/hikerpig/gatsby-project-kb/issues"
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/components/DarkModeToggle/dark-mode-toggle.css:
--------------------------------------------------------------------------------
1 | /* This is based off a codepen! Much appreciated to: https://codepen.io/aaroniker/pen/KGpXZo */
2 | .dark-mode-toggle {
3 | cursor: pointer;
4 | display: flex;
5 | }
6 | .dark-mode-toggle input {
7 | display: none;
8 | }
9 | .dark-mode-toggle input + div {
10 | border-radius: 50%;
11 | width: 20px;
12 | height: 20px;
13 | position: relative;
14 | box-shadow: inset 5px -5px 0 0 #fff;
15 | transform: scale(1) rotate(-2deg);
16 | transition: box-shadow 0.5s ease 0s, transform 0.4s ease 0.1s;
17 | }
18 | .dark-mode-toggle input + div:before {
19 | content: "";
20 | width: inherit;
21 | height: inherit;
22 | border-radius: inherit;
23 | position: absolute;
24 | left: 0;
25 | top: 0;
26 | transition: background 0.3s ease;
27 | }
28 | .dark-mode-toggle input + div:after {
29 | content: "";
30 | width: 4px;
31 | height: 4px;
32 | border-radius: 50%;
33 | margin: -2px 0 0 -2px;
34 | position: absolute;
35 | top: 50%;
36 | left: 50%;
37 | box-shadow: 0 -11px 0 var(--kb-text-color), 0 11px 0 var(--kb-text-color), 11px 0 0 var(--kb-text-color),
38 | -11px 0 0 var(--kb-text-color), 8px 8px 0 var(--kb-text-color), -8px 8px 0 var(--kb-text-color),
39 | 8px -8px 0 var(--kb-text-color), -8px -8px 0 var(--kb-text-color);
40 | transform: scale(0);
41 | transition: all 0.3s ease;
42 | }
43 |
44 | .dark-mode-toggle input:checked + div {
45 | box-shadow: inset 32px -32px 0 0 #fff;
46 | transform: scale(0.5) rotate(0deg);
47 | transition: transform 0.3s ease 0.1s, box-shadow 0.2s ease 0s;
48 | }
49 | .dark-mode-toggle input:checked + :before {
50 | background: var(--kb-text-color);
51 | transition: background 0.3s ease 0.1s;
52 | }
53 | .dark-mode-toggle input:checked + :after {
54 | transform: scale(1.5);
55 | transition: transform 0.5s ease 0.15s;
56 | }
57 | .dark-mode-toggle__hint {
58 | margin-left: 5px;
59 | }
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/components/DarkModeToggle/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import useDarkMode from 'use-dark-mode'
3 |
4 | import './dark-mode-toggle.css'
5 |
6 | const DarkModeToggle = (props: { showHint?: boolean}) => {
7 | const { value: isDark, toggle: toggleDarkMode } = useDarkMode(false)
8 | const hint = isDark ? 'Activate light mode' : 'Activate dark mode'
9 |
10 | return (
11 |
16 |
17 |
18 | {props.showHint && {hint} }
19 |
20 | )
21 | }
22 |
23 | export default DarkModeToggle
24 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/components/GraphButton/graph-button.css:
--------------------------------------------------------------------------------
1 | .graph-button {
2 | border: 0;
3 | background: none;
4 | cursor: pointer;
5 | display: flex;
6 | align-items: center;
7 | }
8 |
9 | .graph-button:hover {
10 | fill: var(--kb-link-color);
11 | color: var(--kb-link-color);
12 | }
13 |
14 | .graph-button__hint {
15 | margin-left: 5px;
16 | }
17 |
18 | .graph-button svg {
19 | fill: var(--kb-text-color);
20 | transform-origin: top center;
21 | transform: scale(1.1);
22 | }
23 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/components/GraphButton/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, lazy, Suspense } from 'react'
2 | import type { GraphState } from '../GraphView'
3 | import classNames from 'classnames'
4 |
5 | import './graph-button.css'
6 |
7 | const Graph = lazy(() => import('../GraphView'))
8 |
9 | const svgIconContent = `
10 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | `
27 |
28 | const GraphButton = (props: {
29 | currentFileId: string
30 | showHint?: boolean
31 | className?: string
32 | isMobileMode?: boolean
33 | }) => {
34 | const { currentFileId, showHint, isMobileMode, className } = props
35 | const [graphState, setGraphState] = useState('hidden')
36 | const hint = 'Show Graph Visualisation'
37 |
38 | return (
39 |
40 | {
45 | setGraphState('show')
46 | }}
47 | >
48 |
49 | {showHint && {hint} }
50 |
51 | {typeof window !== 'undefined' ? (
52 |
53 |
59 |
60 | ) : null}
61 |
62 | )
63 | }
64 |
65 | export default GraphButton
66 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/components/GraphView/graph-view.css:
--------------------------------------------------------------------------------
1 | .dark-mode .force-graph-container canvas {
2 | background: #3b4352 !important;
3 | }
4 |
5 | .dark-mode .overlay {
6 | background-color: rgba(100 , 100, 100, 0.3);
7 | }
8 |
9 | .dark-mode .modal-close {
10 | background-color: rgba(0, 0, 0, 0.4);
11 | border-radius: 50%;
12 | }
13 |
14 | .overlay {
15 | z-index: 98;
16 | position: fixed;
17 | top: 0;
18 | right: 0;
19 | bottom: 0;
20 | left: 0;
21 | display: flex;
22 | align-items: center;
23 | justify-content: center;
24 | height: 100%;
25 | width: 100%;
26 | background-color: var(--kb-shadow-bg);
27 | backdrop-filter: blur(4px);
28 | display: none;
29 | }
30 |
31 | .overlay.show {
32 | display: block;
33 | }
34 |
35 | .graph-view__modal {
36 | z-index: 99;
37 | top: 0;
38 | left: 0;
39 | position: fixed;
40 | width: 100%;
41 | height: 100%;
42 | border-radius: 8px;
43 | background-color: var(--main-bg);
44 | box-shadow: -5px -5px 15px -3px var(--kb-shadow-bg),
45 | 0 4px 6px -2px rgba(0, 0, 0, 0.05);
46 |
47 | display: flex;
48 | align-items: center;
49 | justify-content: center;
50 | }
51 |
52 | .graph-view__search-wrap {
53 | margin-right: 10px;
54 | }
55 |
56 | .graph-view__search-wrap input {
57 | width: 300px;
58 | }
59 |
60 | .graph-view__search-results {
61 | z-index: 200;
62 | }
63 |
64 | .graph-view__modal-hint {
65 | color: var(--kb-text-inverse-color);
66 | font-size: 1.5em;
67 | margin-top: 0.5em;
68 | }
69 |
70 | .modal-close {
71 | position: absolute;
72 | top: 10px;
73 | right: 10px;
74 | padding: 5px;
75 | border: 0;
76 | background: var(--kb-text-inverse-color);
77 | opacity: 0.8;
78 | color: var(--kb-text-color);
79 | border-radius: 50%;
80 | cursor: pointer;
81 | }
82 |
83 | .modal-close:hover {
84 | opacity: 1;
85 | }
86 |
87 | .modal-close svg {
88 | width: 18px;
89 | height: 18px;
90 | }
91 |
92 | .modal-close path {
93 | fill: currentColor;
94 | }
95 |
96 | .modal-minimized .modal-close svg {
97 | width: 15px;
98 | height: 15px;
99 | }
100 |
101 | .modal-body {
102 | height: 100%;
103 | width: 100%;
104 | }
105 |
106 | .modal-body .node,
107 | .modal-body .text {
108 | cursor: pointer;
109 | }
110 |
111 | .graph-view__btn--link {
112 | margin-left: 0.5em;
113 | margin-right: 0.5em;
114 | text-decoration: underline;
115 | }
116 |
117 | .graph-view__btn--link:hover {
118 | color: var(--kb-text-color);
119 | }
120 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/components/GraphView/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useEffect } from 'react'
2 | import useHotkeys from '@reecelucas/react-use-hotkeys'
3 | // import { NoteGraphView, NoteGraphModel } from 'note-graph/dist/note-graph.esm'
4 | import { NoteGraphView, NoteGraphModel } from 'note-graph'
5 | import { navigate } from 'gatsby'
6 | import { createPortal } from 'react-dom'
7 | import { useGraphData } from '../..//use-graph-data'
8 | import { useWindowSize } from '../../use-window-size'
9 | import Search, { SearchProps } from '../Search'
10 |
11 | import './graph-view.css'
12 |
13 | export type GraphState = 'show' | 'hidden'
14 |
15 | const RESULTS_WIDTH = 300
16 |
17 | type Props = {
18 | graphState: GraphState
19 | setGraphState: (state: GraphState) => void
20 | currentFileId: string
21 | isMobileMode?: boolean
22 | }
23 |
24 | export default function GraphView({
25 | setGraphState,
26 | graphState,
27 | currentFileId,
28 | isMobileMode,
29 | }: Props) {
30 | const { notesMap, fileNodesMap } = useGraphData()
31 | const windowSize = useWindowSize()
32 | const graphContainer = useRef(null)
33 | const shouldShowGraph = graphState !== 'hidden'
34 |
35 | const modalShrinkSizeX = isMobileMode ? 20: 40
36 | const modalShrinkSizeY = isMobileMode ? 80: 40
37 | const modalSize = {
38 | width: Math.min(windowSize.width - modalShrinkSizeX, 1400),
39 | height: Math.min(windowSize.height - modalShrinkSizeY, 800),
40 | }
41 |
42 | const navigateTo = (p: string) => {
43 | navigate(p)
44 | }
45 |
46 | const notes = Array.from(notesMap.values())
47 | let noteGraphView: NoteGraphView
48 |
49 | useEffect(() => {
50 | if (!graphContainer.current || graphState === 'hidden') {
51 | return
52 | }
53 |
54 | const graphModel = new NoteGraphModel(notes)
55 |
56 | noteGraphView = new NoteGraphView({
57 | container: graphContainer.current,
58 | graphModel,
59 | width: isMobileMode ? modalSize.width : modalSize.width - RESULTS_WIDTH,
60 | height: modalSize.height,
61 | })
62 |
63 | noteGraphView.onInteraction('nodeClick', ({ node }) => {
64 | const fileNode = fileNodesMap.get(node.id)
65 | const slug = fileNode?.fields?.slug
66 | if (slug) {
67 | navigateTo(slug)
68 | }
69 | })
70 |
71 | if (currentFileId) {
72 | const currentNoteInfo = graphModel.getNodeInfoById(currentFileId)
73 | const shouldZoomToFit = currentNoteInfo && Boolean(currentNoteInfo.neighbors?.length)
74 | noteGraphView.setSelectedNodes([currentFileId], { shouldZoomToFit })
75 | }
76 |
77 | return () => {
78 | noteGraphView.dispose()
79 | }
80 | }, [notes, graphState])
81 |
82 | useHotkeys('Escape Escape', () => {
83 | setGraphState('hidden')
84 | })
85 |
86 | const onSearchResults: SearchProps['onResults'] = (results) => {
87 | if (noteGraphView) {
88 | const nodeIds = results.map((o) => o.id).filter((s) => s)
89 | // console.debug('onSearchResults node ids', nodeIds)
90 | // It's better to add another highlight style or specity styles in `setSelectedNodes`,
91 | // I will need to extend note-graph for this.
92 | if (nodeIds.length) {
93 | noteGraphView.setSelectedNodes(nodeIds)
94 | } else {
95 | noteGraphView.setSelectedNodes([currentFileId])
96 | }
97 | }
98 | }
99 |
100 | return createPortal(
101 |
102 |
{
106 | if (!ev.isDefaultPrevented()) {
107 | setGraphState('hidden')
108 | }
109 | }}
110 | />
111 |
112 |
ev.preventDefault()}
115 | style={{ display: shouldShowGraph ? 'flex' : 'none' }}
116 | >
117 |
123 |
{
127 | setGraphState('hidden')
128 | }}
129 | aria-label="Close Graph"
130 | >
131 |
132 |
133 |
134 |
135 |
136 |
137 | {!isMobileMode && (
138 |
139 |
145 |
146 | )}
147 |
148 |
149 |
Press Esc twice to
150 | {
154 | setGraphState('hidden')
155 | }}
156 | aria-label="Close Graph"
157 | >close
158 | this modal.
159 |
160 |
161 |
162 |
163 |
164 |
165 |
,
166 | document.body
167 | )
168 | }
169 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/components/InlineTOC/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { TableOfContents, TOCItem } from '../../type'
3 | import { slugifyTitle } from '../../utils/toc'
4 | import './inline-toc.css'
5 |
6 | interface InlineTOCProps {
7 | tableOfContents: TableOfContents
8 | }
9 |
10 | const InlineTOC = ({ tableOfContents }: InlineTOCProps) => {
11 | return (
12 |
13 |
Table Of Contents
14 | {tableOfContents.items && (
15 |
16 | )}
17 |
18 | )
19 | }
20 |
21 | type TOCItemProps = {
22 | item: TOCItem
23 | className?: string
24 | }
25 |
26 | function TOCItemComponent({ item, className }: TOCItemProps) {
27 | const itemsElement = item.items ? (
28 |
29 | {item.items.map((childItem) => (
30 |
31 | ))}
32 |
33 | ) : null
34 | return item.title ? (
35 |
36 | {item.title}
37 | {itemsElement}
38 |
39 | ) : (
40 | itemsElement
41 | )
42 | }
43 |
44 | export default InlineTOC
45 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/components/InlineTOC/inline-toc.css:
--------------------------------------------------------------------------------
1 | .inline-toc {
2 | margin-bottom: 1em;
3 | }
4 |
5 | .inline-toc ol {
6 | margin-top: 0;
7 | }
8 |
9 | .inline-toc__header {
10 | font-size: 1.2em;
11 | font-weight: bold;
12 | }
13 |
14 | .inline-toc__top-node {
15 | margin-left: 0;
16 | }
17 |
18 | .inline-toc li {
19 | list-style: disc;
20 | }
21 |
22 | .inline-toc__top-node > li {
23 | list-style: none;
24 | }
25 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/components/LinkReference/index.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'gatsby';
2 | import { Reference } from '../../type'
3 | import React from 'react';
4 |
5 | import './link-reference.css'
6 |
7 | const LinkReference = (props: { reference: Reference}) => {
8 | const { reference } = props
9 | const { slug, title } = reference.referrer.parent?.fields!
10 |
11 | return (
12 |
13 |
14 | {title}
15 |
16 |
17 |
18 | );
19 | }
20 |
21 | export default LinkReference;
22 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/components/LinkReference/link-reference.css:
--------------------------------------------------------------------------------
1 | .link-refernce__context {
2 | padding-left: 1em;
3 | }
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/components/Search/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback, useEffect, useRef } from 'react'
2 | import ReactDOM from 'react-dom'
3 | // import useHotkeys from '@reecelucas/react-use-hotkeys'
4 | import { navigate } from 'gatsby'
5 | import Downshift from 'downshift'
6 | import useSearch, { SearchResult, LOADING_ID } from '../../use-search'
7 |
8 | import './search.css'
9 |
10 | const RESULTS_WIDTH = 500
11 |
12 | export type SearchProps = {
13 | isMobileMode?: boolean
14 | searchActivateHotkey?: string
15 | resultsClassName?: string
16 | resultsWidth?: number
17 | position?: 'right'
18 | onResults?(results: SearchResult[]): void
19 | }
20 |
21 | export default function Search(props: SearchProps) {
22 | const { isMobileMode, resultsClassName, resultsWidth, onResults, position } = props
23 | const [query, setQuery] = useState('')
24 | const searchBarRef = useRef
(null)
25 | const searchBarInputRef = useRef(null)
26 |
27 | const results = useSearch(query)
28 |
29 | const handleChange = useCallback((e) => setQuery(e.target.value), [setQuery])
30 |
31 | // if (searchActivateHotkey) {
32 | // useHotkeys(searchActivateHotkey, () => {
33 | // setTimeout(() => {
34 | // if (searchBarInputRef.current) searchBarInputRef.current.focus()
35 | // })
36 | // })
37 | // }
38 |
39 | if (onResults) {
40 | useEffect(() => {
41 | onResults(results.filter((o) => o.id !== LOADING_ID))
42 | }, [results])
43 | }
44 |
45 | return (
46 | navigate(selection.path)}
48 | itemToString={(item) => (item ? item.title : '')}
49 | >
50 | {({
51 | getInputProps,
52 | getItemProps,
53 | getMenuProps,
54 | isOpen,
55 | highlightedIndex,
56 | getRootProps,
57 | }) => {
58 | return (
59 |
63 |
69 | {isOpen && (
70 |
81 | )}
82 |
83 | )
84 | }}
85 |
86 | )
87 | }
88 |
89 | const SearchBar = React.forwardRef((props, ref) => {
90 | const { onChange, getInputProps, inputRef } = props as any
91 | return (
92 |
93 |
100 |
101 |
102 |
110 |
111 | )
112 | }) as any
113 |
114 | function Results({
115 | results,
116 | getItemProps,
117 | getMenuProps,
118 | highlightedIndex,
119 | searchBarRef,
120 | isMobileMode = false,
121 | className = '',
122 | position,
123 | width = 0,
124 | }) {
125 | width = width || RESULTS_WIDTH
126 |
127 | const sRef: React.RefObject = searchBarRef
128 | const styles: React.CSSProperties = sRef.current
129 | ? (function () {
130 | const searchBarBox = sRef.current.getBoundingClientRect()
131 | let left = isMobileMode ? 10 : searchBarBox.left
132 | if (position === 'right') {
133 | left = searchBarBox.right - width
134 | }
135 | return {
136 | top: searchBarBox.top + searchBarBox.height + 10,
137 | left,
138 | width,
139 | }
140 | })()
141 | : {}
142 |
143 | return ReactDOM.createPortal(
144 |
149 | {results.map((r, index) => (
150 |
163 | {r.title}
164 | {r.excerpt}
165 |
166 | ))}
167 | ,
168 | document.body
169 | )
170 | }
171 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/components/Search/search.css:
--------------------------------------------------------------------------------
1 | .searchWrapper {
2 | position: relative;
3 | display: block;
4 | }
5 |
6 | .inputWrapper {
7 | position: relative;
8 | color: var(--kb-text-color);
9 | }
10 |
11 | .inputWrapper .searchIcon {
12 | position: absolute;
13 | left: 2px;
14 | width: 22px;
15 | padding: 2px;
16 | padding-bottom: 0px;
17 | pointer-events: none;
18 | fill: var(--kb-text-color);
19 | }
20 |
21 | .inputWrapper:hover .searchIcon {
22 | fill: var(--kb-link-color);
23 | }
24 |
25 | .inputWrapper input {
26 | color: var(--kb-text-color);
27 | border: 1px solid transparent;
28 | height: 26px;
29 | width: 100%;
30 | outline: none;
31 | padding-left: 28px;
32 | background-color: var(--kb-note-bg);
33 | /* transition: all 0.3s ease-in-out; */
34 | }
35 |
36 | .inputWrapper input:focus {
37 | border-color: inherit;
38 | }
39 |
40 | .results {
41 | padding-inline-start: 0;
42 | position: fixed;
43 | display: block;
44 | right: 0;
45 | top: 0;
46 | width: 500px;
47 | margin-left: 0;
48 | max-width: calc(100vw - 20px);
49 | max-height: 50vh;
50 | background: var(--kb-note-bg);
51 | box-shadow: -5px -5px 15px -3px var(--kb-shadow-bg);
52 | overflow-y: auto;
53 | border-radius: 4px;
54 | }
55 |
56 | .results li {
57 | display: block;
58 | text-decoration: none;
59 | cursor: pointer;
60 | padding: 10px 16px;
61 | border-bottom: 1px solid var(--kb-separator-color);
62 | }
63 |
64 | .results li:hover {
65 | background: var(--kb-search-highlight-bg);
66 | }
67 |
68 | .results li .title {
69 | color: var(--kb-text-color);
70 | font-weight: bold;
71 | }
72 |
73 | .results li .excerpt {
74 | font-size: 0.9em;
75 | }
76 |
77 | .lds-dual-ring:after {
78 | content: " ";
79 | left: calc(50% - 32px);
80 | position: relative;
81 | display: block;
82 | width: 64px;
83 | height: 64px;
84 | margin: 8px;
85 | border-radius: 50%;
86 | border: 6px solid var(--kb-separator-color);
87 | border-color: var(--kb-separator-color) transparent var(--kb-separator-color) transparent;
88 | animation: lds-dual-ring 1.2s ease-in-out infinite;
89 | }
90 | @keyframes lds-dual-ring {
91 | 0% {
92 | transform: rotate(0deg);
93 | }
94 | 100% {
95 | transform: rotate(360deg);
96 | }
97 | }
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/components/SiteSidebar/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useState, useEffect } from 'react'
2 | import { useStaticQuery, graphql, navigate, Link } from 'gatsby'
3 | import TreeView, { TreeNodeRawData, TreeNodeProps } from '../TreeView'
4 | import Search from '../Search'
5 | import { PageContext } from '../../type'
6 | import { recursivelyCallNode } from '../../utils/index'
7 | import { SEARCH_HOTKEY } from '../../configs/hotkeys'
8 | import './site-sidebar.css'
9 |
10 | function getDirectoriesByPath(dir: string) {
11 | const segs = dir.split('/')
12 | const directories: string[] = []
13 | let cur = ''
14 | segs.forEach((seg) => {
15 | const joinedCur = cur ? [cur, seg].join('/') : seg
16 | directories.push(joinedCur)
17 | cur = joinedCur
18 | })
19 | return directories
20 | }
21 |
22 | export interface ISiteSidebarProps {
23 | pageContext: PageContext
24 | title: string
25 | isMobileMode?: boolean
26 | }
27 |
28 | type RemarkNode = {
29 | frontmatter?: {
30 | title: string
31 | }
32 | parent: {
33 | id: string
34 | relativeDirectory: string
35 | relativePath: string
36 | name: string
37 | fields: {
38 | slug: string
39 | title: string
40 | }
41 | }
42 | }
43 |
44 | export default function SiteSidebar(props: ISiteSidebarProps) {
45 | const { pageContext, title, isMobileMode } = props
46 | const data = useStaticQuery(graphql`
47 | query SiteSidebarQuery {
48 | allMdx {
49 | nodes {
50 | frontmatter {
51 | title
52 | }
53 | parent {
54 | id
55 | ... on File {
56 | id
57 | name
58 | relativeDirectory
59 | relativePath
60 | fields {
61 | title
62 | slug
63 | }
64 | }
65 | }
66 | }
67 | }
68 | }
69 | `)
70 |
71 | const [treeNodes, setTreeNodes] = useState([])
72 | const [treeDataMap, setTreeDataMap] = useState<
73 | Record
74 | >({})
75 | const [nodeMap, setNodeMap] = useState>({})
76 |
77 | // initialize
78 | useEffect(() => {
79 | const nodes = data.allMdx!.nodes as RemarkNode[]
80 | const validNodes = nodes.filter((node) => node.parent).sort((a,b) => {
81 | if (a.parent.relativePath > b.parent.relativePath) return 1
82 | if (a.parent.relativePath < b.parent.relativePath) return -1
83 | return 0
84 | })
85 |
86 | function makeDirectoryNodes(inputPath: string) {
87 | const directories = getDirectoriesByPath(inputPath)
88 | let parentDirId
89 | directories.forEach((dir) => {
90 | if (!treeDataMap[dir]) {
91 | const dirLabel = dir.split('/').pop() || dir
92 | const dirNode: TreeNodeRawData = {
93 | id: dir,
94 | label: dirLabel,
95 | parentId: parentDirId,
96 | isLeaf: false,
97 | }
98 | treeDataMap[dir] = dirNode
99 | treeNodes.push(dirNode)
100 | }
101 | parentDirId = dir
102 | })
103 | }
104 |
105 | validNodes.forEach((node) => {
106 | if (node.parent.relativeDirectory) {
107 | makeDirectoryNodes(node.parent.relativeDirectory)
108 | }
109 | })
110 |
111 | const expandedParents: TreeNodeRawData[] = []
112 |
113 | validNodes.forEach((node) => {
114 | if (!node.parent) return
115 | const file = node.parent
116 | const parentNode = file.relativeDirectory
117 | ? treeDataMap[file.relativeDirectory]
118 | : null
119 | const treeNode: TreeNodeRawData = {
120 | id: file.id,
121 | label: node.frontmatter?.title || file.fields.title,
122 | parentId: parentNode ? parentNode.id : null,
123 | isLeaf: true,
124 | }
125 | const isCurrent = file.id === pageContext.id
126 | if (isCurrent) {
127 | treeNode['className'] = 'site-sidebar__link--cur'
128 | if (parentNode) {
129 | expandedParents.push(parentNode)
130 | }
131 | }
132 | treeNodes.push(treeNode)
133 | treeDataMap[file.id] = treeNode
134 | nodeMap[file.id] = node
135 | })
136 |
137 | // expand parent node through the pah
138 | const _getParentNode = (node: TreeNodeRawData) =>
139 | node.parentId ? treeDataMap[node.parentId] : undefined
140 | expandedParents.forEach((node) => {
141 | recursivelyCallNode(node, _getParentNode, (_node) => {
142 | _node.isExpanded = true
143 | })
144 | })
145 |
146 | setTreeNodes(treeNodes.slice())
147 | setNodeMap(nodeMap)
148 | setTreeDataMap(treeDataMap)
149 |
150 | return () => {}
151 | }, [])
152 |
153 | const onNodeSelect = useCallback(
154 | (treeNode) => {
155 | const node = nodeMap[treeNode.id]
156 | if (node) {
157 | navigate(node.parent.fields.slug)
158 | }
159 | },
160 | [nodeMap]
161 | )
162 |
163 | const renderLabel: TreeNodeProps['renderLabel'] = useCallback(
164 | (nodeProps, { labelClassName }) => {
165 | const { data } = nodeProps
166 | const node = nodeMap[data.id]
167 | let slug = ''
168 | if (node) {
169 | slug = node.parent.fields.slug
170 | }
171 | if (data.isLeaf) {
172 | // console.log('slug is', slug, node.parent.fields)
173 | return (
174 |
179 | {data.label}
180 |
181 | )
182 | } else {
183 | return {data.label}
184 | }
185 | },
186 | [onNodeSelect, nodeMap]
187 | )
188 |
189 | const onBranchNodeClick: TreeNodeProps['onBranchNodeClick'] = useCallback(
190 | (nodeProps) => {
191 | const dataNode = treeDataMap[nodeProps.id]
192 | const newDataNode = { ...dataNode, isExpanded: !nodeProps.isExpanded }
193 | treeDataMap[nodeProps.id] = newDataNode
194 |
195 | // make parent nodes expanded otherwise their state will be lost.
196 | // this logic should be in TreeView rather than here, need to separate it.
197 | recursivelyCallNode(
198 | newDataNode,
199 | (_node) => {
200 | const parentNode = _node.parentId ? treeDataMap[_node.parentId] : null
201 | return parentNode as any
202 | },
203 | (_node) => {
204 | if (_node === newDataNode) return
205 | _node.isExpanded = true
206 | }
207 | )
208 |
209 | const newTreeNodes = Object.values(treeDataMap)
210 | setTreeNodes(newTreeNodes)
211 | },
212 | [treeNodes]
213 | )
214 |
215 | return (
216 |
217 |
218 | {title}
219 |
220 |
221 |
225 |
226 |
227 |
233 |
234 |
235 | )
236 | }
237 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/components/SiteSidebar/site-sidebar.css:
--------------------------------------------------------------------------------
1 | .site-sidebar {
2 | display: flex;
3 | flex-direction: column;
4 | width: 100%;
5 | }
6 |
7 | .site-sidebar__title {
8 | font-weight: bold;
9 | margin-bottom: 5px;
10 | font-size: large;
11 | }
12 |
13 | .site-sidebar__title a {
14 | color: var(--kb-text-color);
15 | }
16 |
17 | .site-sidebar__title a:hover {
18 | color: var(--kb-link-color);
19 | }
20 |
21 | .site-sidebar__link {
22 | color: var(--kb-text-color);
23 | display: block;
24 | }
25 |
26 | .site-sidebar__link:hover {
27 | color: var(--kb-link-color);
28 | }
29 |
30 | .site-sidebar__link--cur {
31 | background-color: var(--kb-tree-cur-color);
32 | }
33 |
34 | .site-sidebar__files {
35 | overflow-y: auto;
36 | }
37 |
38 | .site-sidebar__search {
39 | margin: 5px 15px 15px 0;
40 | }
41 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/components/Topic/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import MDXRenderer from '../mdx-components/MDXRenderer'
3 | import { TopicFlie, WikiLinkLabelTemplateFn } from '../../type'
4 | import AnchorTag from '../mdx-components/AnchorTag'
5 | import LinkReference from '../LinkReference'
6 | import * as HEADER_COMPONENTS from '../mdx-components/header-components'
7 | import { MDXProvider } from '@mdx-js/react'
8 | import slugify from 'slugify'
9 | import InlineTOC from '../InlineTOC'
10 | import './topic.css'
11 |
12 | export type Props = {
13 | file: TopicFlie
14 | currentLocation: Location
15 | refWordMdxSlugDict: {[key: string]: string}
16 | wikiLinkLabelTemplateFn?: WikiLinkLabelTemplateFn | null
17 | showInlineTOC?: boolean
18 | }
19 |
20 | const Topic: React.FC = ({ file, currentLocation, refWordMdxSlugDict, wikiLinkLabelTemplateFn, showInlineTOC }: Props) => {
21 | let referenceBlock
22 | const { frontmatter, inboundReferences, outboundReferences, tableOfContents } = file.childMdx
23 | const { title, slug } = file.fields
24 |
25 | // console.log(
26 | // 'outboundReferences',
27 | // outboundReferences,
28 | // 'inboundReferences',
29 | // inboundReferences,
30 | // slug,
31 | // )
32 |
33 | const ProvidedAnchorTag = (anchorProps) => {
34 | // console.log("ProviµdedAnchorTag", anchorProps)
35 | return (
36 |
44 | )
45 | }
46 |
47 | if (inboundReferences) {
48 | const references = inboundReferences.reduce((acc, ref) => {
49 | if (!ref.referrer.parent?.fields) return acc
50 | const { slug } = ref.referrer.parent?.fields!
51 | acc.push( )
52 | return acc
53 | }, [] as JSX.Element[])
54 |
55 | if (references.length > 0) {
56 | referenceBlock = (
57 |
58 |
Backlinks
59 |
{references}
60 |
61 | )
62 | }
63 | }
64 |
65 | const shouldRenderTitle = !!frontmatter.title
66 | const realTitle = frontmatter.title || title
67 |
68 | return (
69 |
70 | {shouldRenderTitle ?
{realTitle} : null}
71 | {showInlineTOC && }
72 |
73 | {file.childMdx.body}
74 |
75 | {referenceBlock}
76 |
77 | )
78 | }
79 |
80 | export default Topic
81 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/components/Topic/topic.css:
--------------------------------------------------------------------------------
1 | .topic ul,
2 | .topic ol {
3 | list-style: inherit;
4 | }
5 |
6 | .topic__references {
7 | padding: 1em;
8 | background-color: var(--kb-blockquote-bg);
9 | }
10 |
11 | .topic a:link:hover {
12 | text-decoration: underline;
13 | }
14 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/components/TopicLayout/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useMemo, useRef, useLayoutEffect, useCallback } from 'react'
2 | import { useScrollRestoration } from 'gatsby-react-router-scroll'
3 | import useBreakpoint from 'use-breakpoint'
4 | import { useStaticQuery, graphql } from 'gatsby'
5 | import classnames from 'classnames'
6 | import './topic-layout.css'
7 |
8 | import { PageContext } from '../../type'
9 | import GraphButton from '../GraphButton'
10 | import SiteSidebar from '../SiteSidebar'
11 | import DarkModeToggle from '../DarkModeToggle'
12 | import { isServer } from '../../env'
13 |
14 | export type Props = React.PropsWithChildren<{
15 | pageContext: PageContext
16 | showSidebarTOC?: boolean
17 | }>
18 |
19 | const BREAKPOINTS = {
20 | sm: 0,
21 | md: 768,
22 | lg: 1024,
23 | xl: 1280,
24 | }
25 | type BreakpointName = keyof typeof BREAKPOINTS
26 |
27 | enum CmpResult {
28 | Greater = 1,
29 | Equal = 0,
30 | Less = -1,
31 | }
32 |
33 | function cmpBreakpoint(p1: BreakpointName, p2: BreakpointName) {
34 | const v1 = BREAKPOINTS[p1]
35 | const v2 = BREAKPOINTS[p2]
36 | if (v1 > v2) return CmpResult.Greater
37 | if (v1 === v2) return CmpResult.Equal
38 | return CmpResult.Less
39 | }
40 |
41 | export default function TopicLayout(props: Props) {
42 | const { children, pageContext, showSidebarTOC } = props
43 | const tocRestoration = useScrollRestoration('toc')
44 | const data = useStaticQuery(graphql`
45 | query TopicLayoutQuery {
46 | site {
47 | siteMetadata {
48 | title
49 | }
50 | }
51 | }
52 | `)
53 |
54 | const [menuOpened, setMenuOpened] = useState(false)
55 | const [rightMenuOpened, setRightMenuOpened] = useState(false)
56 |
57 | const defaultBreakPoint: BreakpointName = isServer
58 | ? 'md'
59 | : window.innerWidth > BREAKPOINTS.md
60 | ? 'md'
61 | : 'sm'
62 | const { breakpoint } = useBreakpoint(BREAKPOINTS, defaultBreakPoint)
63 |
64 | const isMobileMode = useMemo(() => {
65 | return cmpBreakpoint(breakpoint, 'md') === CmpResult.Less
66 | }, [breakpoint])
67 |
68 | const title = data.site.siteMetadata!.title
69 |
70 | const handleMenuClick = useCallback(() => {
71 | setMenuOpened(!menuOpened)
72 | }, [menuOpened])
73 | const expandIcon = (
74 |
90 | )
91 |
92 | const handleTocClick = useCallback(() => {
93 | setRightMenuOpened(!rightMenuOpened)
94 | }, [rightMenuOpened])
95 |
96 | // this ref and useLayoutEffect are kind of hack, but I don't know if
97 | // there is a better solution to tweak the hydration of '.topic-layout__left'
98 | // so I choose to force some manipulation in browser side
99 | const leftEleRef = useRef(null)
100 | const leftClassObject = {
101 | 'topic-layout__left--mobile': isMobileMode,
102 | 'topic-layout__left--show': isMobileMode && menuOpened,
103 | 'shadow-md': isMobileMode,
104 | 'transition-all': isMobileMode,
105 | 'z-10': isMobileMode,
106 | }
107 | useLayoutEffect(() => {
108 | if (!leftEleRef.current) return
109 | for (const [key, value] of Object.entries(leftClassObject)) {
110 | if (value) {
111 | leftEleRef.current.classList.add(key)
112 | } else {
113 | leftEleRef.current.classList.remove(key)
114 | }
115 | }
116 | })
117 | const leftClass = classnames(leftClassObject)
118 |
119 | const rightEleRef = useRef(null)
120 | const rightClassObject = {
121 | 'topic-layout__right': true,
122 | 'topic-layout__right--drawer': isMobileMode,
123 | 'topic-layout__right--drawer-show': isMobileMode && rightMenuOpened,
124 | 'shadow-md': isMobileMode,
125 | 'transition-all': isMobileMode,
126 | 'z-10': isMobileMode,
127 | 'flex-shrink-0': true,
128 | 'p-5': true,
129 | 'hover:shadow-md': true,
130 | }
131 | useLayoutEffect(() => {
132 | if (!rightEleRef.current) return
133 | for (const [key, value] of Object.entries(rightClassObject)) {
134 | if (value) {
135 | rightEleRef.current.classList.add(key)
136 | } else {
137 | rightEleRef.current.classList.remove(key)
138 | }
139 | }
140 | })
141 | const rightClass = classnames(rightClassObject)
142 |
143 | const sideBar = useMemo(() => {
144 | return (
145 |
150 | )
151 | }, [isMobileMode, breakpoint])
152 |
153 | return (
154 |
155 |
156 |
157 | {expandIcon}
158 |
{title}
159 |
160 | {showSidebarTOC && (
161 |
162 |
163 | T
164 |
165 |
166 |
167 |
168 | )}
169 |
170 |
171 |
175 | {sideBar}
176 |
177 |
178 | {children}
179 |
180 |
181 | {isMobileMode ? null : (
182 | <>
183 |
184 |
185 | >
186 | )}
187 |
188 | {showSidebarTOC && (
189 |
190 | {isMobileMode && (
191 |
192 | Table Of Contents
193 |
194 | )}
195 |
196 |
197 | )}
198 |
199 |
200 | {isMobileMode && (menuOpened || rightMenuOpened) ? (
201 |
{
204 | setMenuOpened(false)
205 | setRightMenuOpened(false)
206 | }}
207 | >
208 | ) : null}
209 |
210 | )
211 | }
212 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/components/TopicLayout/topic-layout.css:
--------------------------------------------------------------------------------
1 | .topic-layout__main {
2 | width: 1480px;
3 | max-width: 100%;
4 | }
5 |
6 | .topic-layout__content {
7 | max-width: min(960px, 100vw);
8 | background-color: var(--kb-note-bg);
9 | }
10 |
11 | .topic-layout__left {
12 | width: 280px;
13 | position: relative;
14 | background-color: var(--bg-color-2);
15 | transition: 0.3s box-shadow ease-in-out;
16 | }
17 |
18 | .topic-layout__left--mobile {
19 | top: 0;
20 | left: 0;
21 | position: fixed;
22 | transform: translateX(-280px);
23 | }
24 |
25 | .topic-layout__left--mobile.topic-layout__left--show {
26 | transform: translateX(0px);
27 | }
28 |
29 | .topic-layout__right {
30 | overflow-x: hidden;
31 | width: 280px;
32 | transition: 0.3s box-shadow ease-in-out;
33 | }
34 |
35 | .topic-layout__left,
36 | .topic-layout__right {
37 | height: 100vh;
38 | overflow-y: auto;
39 | }
40 |
41 | .topic-layout__right--drawer {
42 | position: fixed;
43 | right: 0;
44 | top: 0;
45 | background-color: var(--bg-color-2);
46 | display: none;
47 | }
48 |
49 | .topic-layout__right--drawer-show {
50 | display: block;
51 | }
52 |
53 | .topic-layout .tocbot {
54 | margin-top: 20px;
55 | }
56 |
57 | .topic-layout__header {
58 | background-color: var(--bg-color-2);
59 | position: sticky;
60 | top: 0;
61 | }
62 |
63 | .topic-layout__right .dark-mode-toggle {
64 | margin-top: 0.6em;
65 | }
66 |
67 | .topic-layout__mask {
68 | background-color: rgba(0, 0, 0, 0.1);
69 | }
70 |
71 | .top-layout__header-item {
72 | margin-right: .5em;
73 | }
74 |
75 | .toc-layout__toc-icon {
76 | padding-right: .3em;
77 | transform-origin: top center;
78 | transform: scale(1.2) translateY(-1px);
79 | }
80 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/components/TreeView/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import './tree-view.css'
3 | import { IconChevronRight } from '../icons'
4 |
5 | export type TreeNodeRawData = {
6 | id: string
7 | label: string
8 | isLeaf: boolean
9 | isExpanded?: boolean
10 | parentId?: string | null
11 | className?: string
12 | }
13 |
14 | type TreeNodeData = TreeNodeRawData & {
15 | children?: TreeNodeData[]
16 | depth: number
17 | }
18 |
19 | interface TreeCommonProps {
20 | onSelect(node: TreeNodeData): void
21 | onBranchNodeClick?(node: TreeNodeData): void
22 | renderLabel?(props: TreeNodeProps, opts: { labelClassName: string }): JSX.Element
23 | }
24 |
25 | export interface TreeNodeProps extends TreeCommonProps {
26 | data: TreeNodeData
27 | }
28 | export interface ITreeViewProps extends TreeCommonProps {
29 | nodes: TreeNodeRawData[]
30 | }
31 |
32 | export default function TreeView(props: ITreeViewProps) {
33 | const tree = buildTree(props.nodes)
34 | return (
35 |
36 | {tree.rootNode.children!.map((node) => {
37 | return (
38 |
43 | )
44 | })}
45 |
46 | )
47 | }
48 |
49 | function TreeNode(props: TreeNodeProps) {
50 | const { data, onSelect, onBranchNodeClick } = props
51 | const onClick = (e: React.MouseEvent) => {
52 | if (data.isLeaf) onSelect(data)
53 | e.stopPropagation()
54 | }
55 | const labelClassName = 'tree-view__label'
56 | const nodeLabel = props.renderLabel ? (
57 | props.renderLabel(props, { labelClassName })
58 | ) : (
59 | {data.label}
60 | )
61 | const onNodeClick = (e: React.MouseEvent) => {
62 | if (!data.isLeaf && onBranchNodeClick) {
63 | e.stopPropagation()
64 | onBranchNodeClick(data)
65 | }
66 | }
67 | return (
68 |
69 |
70 | {!data.isLeaf && }
71 | {nodeLabel}
72 |
73 | {(data.isLeaf || !data.isExpanded) ? null : (
74 |
75 | {data.children!.map((childNode) => {
76 | return (
77 |
84 | )
85 | })}
86 |
87 | )}
88 |
89 | )
90 | }
91 |
92 | const MemorizedTreeNode = React.memo(TreeNode)
93 |
94 | const MAX_LOOP_COUNT = 200000
95 |
96 | function buildTree(nodes: TreeNodeRawData[]) {
97 | const rootNode: TreeNodeData = {
98 | id: '__root__',
99 | label: '__root__',
100 | isLeaf: false,
101 | children: [],
102 | depth: 0,
103 | }
104 | const treeNodes = [rootNode]
105 | const treeMap = {
106 | [rootNode.id]: rootNode,
107 | }
108 | const allIds = [rootNode.id].concat(nodes.map((node) => node.id))
109 | const nodeQueue = nodes.slice()
110 | let cur = 0
111 | while (nodeQueue.length) {
112 | const node = nodeQueue.shift()!
113 | let parentNode
114 | if (cur++ > MAX_LOOP_COUNT) {
115 | console.error('[TreeView], exceeds MAX_LOOP_COUNT, need to check nodes data')
116 | break
117 | }
118 | if (node.parentId) {
119 | if (!treeMap[node.parentId]) {
120 | if (allIds.includes(node.parentId)) {
121 | nodeQueue.push(node)
122 | continue
123 | } else {
124 | treeMap[node.parentId] = {
125 | id: node.parentId,
126 | label: '',
127 | isLeaf: false,
128 | children: [],
129 | depth: 1,
130 | isExpanded: node.isExpanded,
131 | }
132 | }
133 | }
134 | parentNode = treeMap[node.parentId]
135 | } else {
136 | parentNode = rootNode
137 | }
138 |
139 | const treeNode: TreeNodeData = {
140 | isExpanded: false,
141 | ...node,
142 | depth: parentNode.depth + 1,
143 | children: [],
144 | }
145 | // console.log('tree node', treeNode)
146 | treeMap[node.id] = treeNode
147 | parentNode.children.push(treeNode)
148 | treeNodes.push(treeNode)
149 | }
150 | return { rootNode, treeNodes }
151 | }
152 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/components/TreeView/tree-view.css:
--------------------------------------------------------------------------------
1 | ul.tree-view,
2 | .tree-view ul {
3 | margin: 0;
4 | }
5 |
6 | .tree-view__node {
7 | padding-left: 10px;
8 | margin-left: 0;
9 | margin-bottom: 0;
10 | }
11 |
12 | .tree-view__node-header {
13 | width: 100%;
14 | display: inline-block;
15 | cursor: pointer;
16 | }
17 |
18 | .tree-view__node-header:hover {
19 | color: var(--kb-link-color);
20 | }
21 |
22 | .tree-view__icon {
23 | transition: transform 0.2s ease;
24 | margin-left: -5px;
25 | margin-right: 2px;
26 | position: relative;
27 | top: -1px;
28 | }
29 |
30 | .tree-view__icon--expanded {
31 | transform: rotate(90deg);
32 | }
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/components/header.css:
--------------------------------------------------------------------------------
1 | .header {
2 | display: flex;
3 | justify-content: space-between;
4 | align-items: center;
5 | padding: 10px 15px;
6 | }
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/components/header.js:
--------------------------------------------------------------------------------
1 | import { Link } from 'gatsby'
2 | import PropTypes from 'prop-types'
3 | import React from 'react'
4 |
5 | import './header.css'
6 |
7 | const Header = ({ siteTitle, addons }) => (
8 |
16 | )
17 |
18 | Header.propTypes = {
19 | siteTitle: PropTypes.string,
20 | }
21 |
22 | Header.defaultProps = {
23 | siteTitle: ``,
24 | }
25 |
26 | export default Header
27 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/components/icons/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import classnames from 'classnames'
3 | import './svg-icon.css';
4 |
5 | export const IconChevronRight = (props: { className?: string }) => {
6 | const className = classnames({
7 | [props.className || '']: true,
8 | 'svg-icon': true
9 | })
10 | return
11 |
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/components/icons/svg-icon.css:
--------------------------------------------------------------------------------
1 | .svg-icon {
2 | width: 1em;
3 | display: inline-block;
4 | }
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/components/layout.css:
--------------------------------------------------------------------------------
1 | .layout__content {
2 | margin: auto;
3 | max-width: 960px;
4 | }
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/components/layout.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Layout component that queries for data
3 | * with Gatsby's useStaticQuery component
4 | *
5 | * See: https://www.gatsbyjs.com/docs/use-static-query/
6 | */
7 |
8 | import React from "react"
9 | import PropTypes from "prop-types"
10 | import { useStaticQuery, graphql } from "gatsby"
11 |
12 | import Header from "./header"
13 | import './layout.css'
14 |
15 | const Layout = ({ children, headerAddons }) => {
16 | const data = useStaticQuery(graphql`
17 | query BaseSiteTitleQuery {
18 | site {
19 | siteMetadata {
20 | title
21 | }
22 | }
23 | }
24 | `)
25 |
26 | return (
27 |
28 |
29 |
30 |
{children}
31 |
34 | © {new Date().getFullYear()}, Built with
35 | {` `}
36 | Gatsby
37 |
38 |
39 |
40 | )
41 | }
42 |
43 | Layout.propTypes = {
44 | children: PropTypes.node.isRequired,
45 | }
46 |
47 | export default Layout
48 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/components/mdx-components/AnchorTag.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { withPrefix, Link } from 'gatsby'
3 | import { MDXProvider } from '@mdx-js/react'
4 | import * as path from 'path'
5 | import slugify from 'slugify'
6 | import MDXRenderer from './MDXRenderer'
7 | import Tippy from '@tippyjs/react'
8 | import { Reference, WikiLinkLabelTemplateFn } from '../../type'
9 |
10 | import './anchor-tag.css'
11 |
12 | type Props = React.PropsWithChildren<{
13 | title: string
14 | href: string
15 | withoutLink: boolean
16 | withoutPopup: boolean
17 | references: Reference[]
18 | currentSlug: string
19 | currentLocation: Location
20 | refWordMdxSlugDict: { [key: string]: string }
21 | wikiLinkLabelTemplateFn?: WikiLinkLabelTemplateFn | null
22 | }>
23 |
24 | /**
25 | * Infer the target's slug based on an intuitive method.
26 | * But usually if `contentPath` is passed to config,
27 | * relative ref resolving should be done by the transformer-wiki-references, based on the directory.
28 | * The anchorSlug will not be used.
29 | */
30 | function genHrefInfo(opts: { currentSlug: string; href: string }) {
31 | const { href, currentSlug } = opts
32 | let isLocalHash = false
33 | const isExternalLink = /\/\//.test(href)
34 | let anchorSlug = href
35 | if (!isExternalLink) {
36 | if (href.startsWith('#')) {
37 | anchorSlug = currentSlug
38 | isLocalHash = true
39 | } else if (href.startsWith('..') || !href.startsWith('/')) {
40 | anchorSlug = path.resolve(path.dirname(currentSlug), href)
41 | }
42 | }
43 | return {
44 | anchorSlug,
45 | isExternalLink,
46 | isLocalHash,
47 | }
48 | }
49 |
50 | function padHrefWithAnchor(href: string, anchor?: string) {
51 | if (anchor) {
52 | return `${href}#${slugify(anchor)}`
53 | }
54 | return href
55 | }
56 |
57 | const AnchorTag = ({
58 | title,
59 | href,
60 | references = [],
61 | withoutLink,
62 | withoutPopup,
63 | currentSlug,
64 | currentLocation,
65 | refWordMdxSlugDict,
66 | wikiLinkLabelTemplateFn,
67 | ...restProps
68 | }: Props) => {
69 | // prettier-ignore
70 | const { anchorSlug } = genHrefInfo({ currentSlug, href })
71 |
72 | function getSlugByRefWord(title: string) {
73 | if (!refWordMdxSlugDict) return;
74 | if (title in refWordMdxSlugDict) return `/${refWordMdxSlugDict[title]}`
75 | return;
76 | }
77 |
78 | let ref: Reference | undefined
79 |
80 | ref = references.find((x) => {
81 | const refSlug = x.target.parent?.fields.slug || ''
82 | return (
83 | `/${x.refWord}` === href ||
84 | getSlugByRefWord(title) === refSlug ||
85 | withPrefix(refSlug) === withPrefix(anchorSlug)
86 | )
87 | })
88 | // console.log('ref', ref, 'href', href, 'anchorSlug', anchorSlug, references)
89 |
90 | let content
91 | let popupContent
92 | let child
93 |
94 | if (ref && ref.target.parent) {
95 | // console.log('reference is', ref, 'withoutLink', withoutLink)
96 | const targetFileNode = ref.target.parent
97 | const fields = ref.target.parent.fields
98 | const mdxBody = ref.target.body
99 | const nestedComponents = {
100 | a(props) {
101 | const {
102 | anchorSlug: nestedAnchorSlug,
103 | isExternalLink: nestedIsExternalLink,
104 | } = genHrefInfo({ currentSlug, href: props.href })
105 | if (nestedIsExternalLink) {
106 | return {props.children}
107 | } else {
108 | let toSlug = nestedAnchorSlug
109 | if (refWordMdxSlugDict) {
110 | // nested content's anchor label will not be replaced with topic title,
111 | // so it can be used to form slug
112 | const maybeSlug = getSlugByRefWord(props.title)
113 | if (maybeSlug) toSlug = maybeSlug
114 | }
115 | return {props.children}
116 | }
117 | },
118 | p(props) {
119 | return
120 | },
121 | }
122 | if (ref.label) {
123 | // markdown link
124 | content = ref.label
125 | } else {
126 | content = wikiLinkLabelTemplateFn
127 | ? wikiLinkLabelTemplateFn({ refWord: ref.refWord, title: fields.title })
128 | : restProps.children
129 | }
130 | popupContent = (
131 |
132 |
133 |
134 | {mdxBody}
135 |
136 |
137 |
138 | )
139 | child = (
140 |
145 | {content}
146 |
147 | )
148 | } else {
149 | content = restProps.children
150 | popupContent = {href}
151 | child = (
152 |
161 | )
162 | return child
163 | }
164 |
165 | if (withoutLink) {
166 | return {content}
167 | }
168 |
169 | if (withoutPopup) {
170 | return child
171 | }
172 |
173 | return (
174 |
180 | {child}
181 |
182 | )
183 | }
184 |
185 | export default AnchorTag
186 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/components/mdx-components/MDXRenderer.tsx:
--------------------------------------------------------------------------------
1 | import { MDXRenderer } from 'gatsby-plugin-mdx'
2 |
3 | export default MDXRenderer
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/components/mdx-components/anchor-tag.css:
--------------------------------------------------------------------------------
1 | .anchor-tag__popover {
2 | position: relative;
3 | max-width: 26rem;
4 | box-shadow: 0 1px 3px var(--kb-shadow-bg);
5 | padding: 1rem;
6 | border-radius: 0.5rem;
7 | background-color: var(--kb-references-bg);
8 | max-height: 26rem;
9 | overflow: auto; /* for browser that doesn't support overlay */
10 | overflow: overlay;
11 | }
12 |
13 | .anchor-tag__popover.no-max-width {
14 | max-width: 90vw;
15 | }
16 |
17 | .anchor-tag__popover.with-markdown {
18 | font-size: 0.875rem;
19 | }
20 |
21 | .anchor-tag__popover h1,
22 | .anchor-tag__popover h2,
23 | .anchor-tag__popover h3,
24 | .anchor-tag__popover h4,
25 | .anchor-tag__popover h5,
26 | .anchor-tag__popover h6 {
27 | margin-bottom: 5px;
28 | padding: 0;
29 | font-size: 1rem;
30 | }
31 |
32 | .anchor-tag__popover blockquote {
33 | margin-left: 0;
34 | margin-bottom: 0;
35 | padding: 0;
36 | }
37 |
38 | .anchor-tag__popover ul {
39 | padding-left: 1rem;
40 | margin-left: 0;
41 | margin-bottom: 0;
42 | }
43 |
44 | .anchor-tag__popover li {
45 | margin-bottom: 0;
46 | }
47 |
48 | /* copied from tippy.js/animations/shift-away.css */
49 | .tippy-box[data-animation='shift-away'][data-state='hidden'] {
50 | opacity: 0;
51 | }
52 | .tippy-box[data-animation='shift-away'][data-state='hidden'][data-placement^='top'] {
53 | transform: translateY(10px);
54 | }
55 | .tippy-box[data-animation='shift-away'][data-state='hidden'][data-placement^='bottom'] {
56 | transform: translateY(-10px);
57 | }
58 | .tippy-box[data-animation='shift-away'][data-state='hidden'][data-placement^='left'] {
59 | transform: translateX(10px);
60 | }
61 | .tippy-box[data-animation='shift-away'][data-state='hidden'][data-placement^='right'] {
62 | transform: translateX(-10px);
63 | }
64 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/components/mdx-components/header-components.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { slugifyTitle } from '../../utils/toc'
3 |
4 | function makeHeaderComponent(tag) {
5 | return (props) => {
6 | const slugified = typeof props.children === 'string' ? slugifyTitle(props.children): props.children
7 | const id = slugified ? slugified: props.children
8 | // console.log(`${tag} props`, props, id)
9 | return React.createElement(tag, {
10 | id,
11 | ...props,
12 | }, props.children)
13 | }
14 | }
15 |
16 | export const h1 = makeHeaderComponent('h1')
17 | export const h2 = makeHeaderComponent('h2')
18 | export const h3 = makeHeaderComponent('h3')
19 | export const h4 = makeHeaderComponent('h4')
20 | export const h5 = makeHeaderComponent('h5')
21 | export const h6 = makeHeaderComponent('h6')
22 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/components/seo.js:
--------------------------------------------------------------------------------
1 | /**
2 | * SEO component that queries for data with
3 | * Gatsby's useStaticQuery React hook
4 | *
5 | * See: https://www.gatsbyjs.com/docs/use-static-query/
6 | */
7 |
8 | import React from "react"
9 | import PropTypes from "prop-types"
10 | import { Helmet } from "react-helmet"
11 | import { useStaticQuery, graphql } from "gatsby"
12 |
13 | function SEO({ description, lang, meta, title }) {
14 | const { site } = useStaticQuery(
15 | graphql`
16 | query {
17 | site {
18 | siteMetadata {
19 | title
20 | description
21 | author
22 | }
23 | }
24 | }
25 | `
26 | )
27 |
28 | const metaDescription = description || site.siteMetadata.description
29 | const defaultTitle = site.siteMetadata?.title
30 |
31 | return (
32 |
73 | )
74 | }
75 |
76 | SEO.defaultProps = {
77 | lang: `en`,
78 | meta: [],
79 | description: ``,
80 | }
81 |
82 | SEO.propTypes = {
83 | description: PropTypes.string,
84 | lang: PropTypes.string,
85 | meta: PropTypes.arrayOf(PropTypes.object),
86 | title: PropTypes.string.isRequired,
87 | }
88 |
89 | export default SEO
90 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/configs/hotkeys.ts:
--------------------------------------------------------------------------------
1 | export const SEARCH_HOTKEY = 's s'
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/env.ts:
--------------------------------------------------------------------------------
1 | export const isServer = typeof window === 'undefined';
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/images/gatsby-astronaut.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hikerpig/gatsby-project-kb/2307f08f9d443f2156d77081ccab42264973da13/packages/gatsby-theme-kb/src/images/gatsby-astronaut.png
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/images/gatsby-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hikerpig/gatsby-project-kb/2307f08f9d443f2156d77081ccab42264973da13/packages/gatsby-theme-kb/src/images/gatsby-icon.png
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/pages/404.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 |
3 | import Layout from "../components/layout"
4 | import SEO from "../components/seo"
5 |
6 | const NotFoundPage = () => (
7 |
8 |
9 | 404: Not Found
10 | You just hit a route that doesn't exist... the sadness.
11 |
12 | )
13 |
14 | export default NotFoundPage
15 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/styles/base.css:
--------------------------------------------------------------------------------
1 | @import './vars.css';
2 |
3 | @import './components/scrollbar.css';
4 |
5 | html {
6 | -ms-text-size-adjust: 100%;
7 | -webkit-text-size-adjust: 100%;
8 | box-sizing: border-box;
9 | overflow-y: overlay;
10 | }
11 | body {
12 | margin: 0;
13 | -webkit-font-smoothing: antialiased;
14 | -moz-osx-font-smoothing: grayscale;
15 | color: var(--kb-text-color);
16 | background-color: var(--bg-color-2);
17 | font-family: var(--kb-font-family);
18 | font-weight: normal;
19 | word-wrap: break-word;
20 | font-kerning: normal;
21 | -moz-font-feature-settings: "kern", "liga", "clig", "calt";
22 | -ms-font-feature-settings: "kern", "liga", "clig", "calt";
23 | -webkit-font-feature-settings: "kern", "liga", "clig", "calt";
24 | font-feature-settings: "kern", "liga", "clig", "calt";
25 | }
26 | article,
27 | aside,
28 | details,
29 | figcaption,
30 | figure,
31 | footer,
32 | header,
33 | main,
34 | menu,
35 | nav,
36 | section,
37 | summary {
38 | display: block;
39 | }
40 | audio,
41 | canvas,
42 | progress,
43 | video {
44 | display: inline-block;
45 | }
46 | audio:not([controls]) {
47 | display: none;
48 | height: 0;
49 | }
50 | progress {
51 | vertical-align: baseline;
52 | }
53 | [hidden],
54 | template {
55 | display: none;
56 | }
57 | a {
58 | background-color: transparent;
59 | -webkit-text-decoration-skip: objects;
60 | color: var(--kb-link-color);
61 | }
62 | a:active,
63 | a:hover {
64 | outline-width: 0;
65 | }
66 |
67 | abbr[title] {
68 | border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5);
69 | cursor: help;
70 | text-decoration: none;
71 | }
72 | b,
73 | strong {
74 | font-weight: inherit;
75 | font-weight: bolder;
76 | }
77 | dfn {
78 | font-style: italic;
79 | }
80 | h1 {
81 | margin-left: 0;
82 | margin-right: 0;
83 | margin-top: 0;
84 | padding-bottom: 0;
85 | padding-left: 0;
86 | padding-right: 0;
87 | padding-top: 0;
88 | margin-bottom: 1.45rem;
89 | color: inherit;
90 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
91 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
92 | font-weight: bold;
93 | text-rendering: optimizeLegibility;
94 | font-size: 2.25rem;
95 | line-height: 1.1;
96 | }
97 | mark {
98 | background-color: #ff0;
99 | color: #000;
100 | }
101 | small {
102 | font-size: 80%;
103 | }
104 | sub,
105 | sup {
106 | font-size: 75%;
107 | line-height: 0;
108 | position: relative;
109 | vertical-align: baseline;
110 | }
111 | sub {
112 | bottom: -0.25em;
113 | }
114 | sup {
115 | top: -0.5em;
116 | }
117 | img {
118 | border-style: none;
119 | max-width: 100%;
120 | margin-left: 0;
121 | margin-right: 0;
122 | margin-top: 0;
123 | padding-bottom: 0;
124 | padding-left: 0;
125 | padding-right: 0;
126 | padding-top: 0;
127 | margin-bottom: 1.45rem;
128 | }
129 | svg:not(:root) {
130 | overflow: hidden;
131 | }
132 | code,
133 | kbd,
134 | pre,
135 | samp {
136 | font-family: monospace;
137 | font-size: 1em;
138 | }
139 | figure {
140 | margin-left: 0;
141 | margin-right: 0;
142 | margin-top: 0;
143 | padding-bottom: 0;
144 | padding-left: 0;
145 | padding-right: 0;
146 | padding-top: 0;
147 | margin-bottom: 1.45rem;
148 | }
149 | hr {
150 | box-sizing: content-box;
151 | overflow: visible;
152 | margin-left: 0;
153 | margin-right: 0;
154 | margin-top: 0;
155 | padding-bottom: 0;
156 | padding-left: 0;
157 | padding-right: 0;
158 | padding-top: 0;
159 | margin-bottom: calc(1.45rem - 1px);
160 | background: hsla(0, 0%, 0%, 0.2);
161 | border: none;
162 | height: 1px;
163 | }
164 | button,
165 | input,
166 | optgroup,
167 | select,
168 | textarea {
169 | font: inherit;
170 | margin: 0;
171 | }
172 | optgroup {
173 | font-weight: 700;
174 | }
175 | button,
176 | input {
177 | overflow: visible;
178 | }
179 | button,
180 | select {
181 | text-transform: none;
182 | }
183 | [type="reset"],
184 | [type="submit"],
185 | button,
186 | html [type="button"] {
187 | -webkit-appearance: button;
188 | }
189 | [type="button"]::-moz-focus-inner,
190 | [type="reset"]::-moz-focus-inner,
191 | [type="submit"]::-moz-focus-inner,
192 | button::-moz-focus-inner {
193 | border-style: none;
194 | padding: 0;
195 | }
196 | [type="button"]:-moz-focusring,
197 | [type="reset"]:-moz-focusring,
198 | [type="submit"]:-moz-focusring,
199 | button:-moz-focusring {
200 | outline: 1px dotted ButtonText;
201 | }
202 | fieldset {
203 | border: 1px solid silver;
204 | padding: 0.35em 0.625em 0.75em;
205 | margin-left: 0;
206 | margin-right: 0;
207 | margin-top: 0;
208 | padding-bottom: 0;
209 | padding-left: 0;
210 | padding-right: 0;
211 | padding-top: 0;
212 | margin-bottom: 1.45rem;
213 | }
214 | legend {
215 | box-sizing: border-box;
216 | color: inherit;
217 | display: table;
218 | max-width: 100%;
219 | padding: 0;
220 | white-space: normal;
221 | }
222 | textarea {
223 | overflow: auto;
224 | }
225 | [type="checkbox"],
226 | [type="radio"] {
227 | box-sizing: border-box;
228 | padding: 0;
229 | }
230 | [type="number"]::-webkit-inner-spin-button,
231 | [type="number"]::-webkit-outer-spin-button {
232 | height: auto;
233 | }
234 | [type="search"] {
235 | -webkit-appearance: textfield;
236 | outline-offset: -2px;
237 | }
238 | [type="search"]::-webkit-search-cancel-button,
239 | [type="search"]::-webkit-search-decoration {
240 | -webkit-appearance: none;
241 | }
242 | ::-webkit-input-placeholder {
243 | color: inherit;
244 | opacity: 0.54;
245 | }
246 | ::-webkit-file-upload-button {
247 | -webkit-appearance: button;
248 | font: inherit;
249 | }
250 | * {
251 | box-sizing: inherit;
252 | }
253 | *:before {
254 | box-sizing: inherit;
255 | }
256 | *:after {
257 | box-sizing: inherit;
258 | }
259 | h2 {
260 | margin-left: 0;
261 | margin-right: 0;
262 | margin-top: 0;
263 | padding-bottom: 0;
264 | padding-left: 0;
265 | padding-right: 0;
266 | padding-top: 0;
267 | margin-bottom: 1.45rem;
268 | color: inherit;
269 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
270 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
271 | font-weight: bold;
272 | text-rendering: optimizeLegibility;
273 | font-size: 1.62671rem;
274 | line-height: 1.1;
275 | }
276 | h3 {
277 | margin-left: 0;
278 | margin-right: 0;
279 | margin-top: 0;
280 | padding-bottom: 0;
281 | padding-left: 0;
282 | padding-right: 0;
283 | padding-top: 0;
284 | margin-bottom: 1.45rem;
285 | color: inherit;
286 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
287 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
288 | font-weight: bold;
289 | text-rendering: optimizeLegibility;
290 | font-size: 1.38316rem;
291 | line-height: 1.1;
292 | }
293 | h4 {
294 | margin-left: 0;
295 | margin-right: 0;
296 | margin-top: 0;
297 | padding-bottom: 0;
298 | padding-left: 0;
299 | padding-right: 0;
300 | padding-top: 0;
301 | margin-bottom: 1.45rem;
302 | color: inherit;
303 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
304 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
305 | font-weight: bold;
306 | text-rendering: optimizeLegibility;
307 | font-size: 1rem;
308 | line-height: 1.1;
309 | }
310 | h5 {
311 | margin-left: 0;
312 | margin-right: 0;
313 | margin-top: 0;
314 | padding-bottom: 0;
315 | padding-left: 0;
316 | padding-right: 0;
317 | padding-top: 0;
318 | margin-bottom: 1.45rem;
319 | color: inherit;
320 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
321 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
322 | font-weight: bold;
323 | text-rendering: optimizeLegibility;
324 | font-size: 0.85028rem;
325 | line-height: 1.1;
326 | }
327 | h6 {
328 | margin-left: 0;
329 | margin-right: 0;
330 | margin-top: 0;
331 | padding-bottom: 0;
332 | padding-left: 0;
333 | padding-right: 0;
334 | padding-top: 0;
335 | margin-bottom: 1.45rem;
336 | color: inherit;
337 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
338 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
339 | font-weight: bold;
340 | text-rendering: optimizeLegibility;
341 | font-size: 0.78405rem;
342 | line-height: 1.1;
343 | }
344 | hgroup {
345 | margin-left: 0;
346 | margin-right: 0;
347 | margin-top: 0;
348 | padding-bottom: 0;
349 | padding-left: 0;
350 | padding-right: 0;
351 | padding-top: 0;
352 | margin-bottom: 1.45rem;
353 | }
354 | ul {
355 | margin-left: 1.45rem;
356 | margin-right: 0;
357 | margin-top: 0;
358 | padding-bottom: 0;
359 | padding-left: 0;
360 | padding-right: 0;
361 | padding-top: 0;
362 | margin-bottom: 1.45rem;
363 | list-style-position: outside;
364 | list-style-image: none;
365 | }
366 | ol {
367 | margin-left: 1.45rem;
368 | margin-right: 0;
369 | margin-top: 0;
370 | padding-bottom: 0;
371 | padding-left: 0;
372 | padding-right: 0;
373 | padding-top: 0;
374 | margin-bottom: 1.45rem;
375 | list-style-position: outside;
376 | list-style-image: none;
377 | }
378 | dl {
379 | margin-left: 0;
380 | margin-right: 0;
381 | margin-top: 0;
382 | padding-bottom: 0;
383 | padding-left: 0;
384 | padding-right: 0;
385 | padding-top: 0;
386 | margin-bottom: 1.45rem;
387 | }
388 | dd {
389 | margin-left: 0;
390 | margin-right: 0;
391 | margin-top: 0;
392 | padding-bottom: 0;
393 | padding-left: 0;
394 | padding-right: 0;
395 | padding-top: 0;
396 | margin-bottom: 1.45rem;
397 | }
398 | p {
399 | margin-left: 0;
400 | margin-right: 0;
401 | margin-top: 0;
402 | padding-bottom: 0;
403 | padding-left: 0;
404 | padding-right: 0;
405 | padding-top: 0;
406 | margin-bottom: 1.45rem;
407 | }
408 | pre {
409 | margin-left: 0;
410 | margin-right: 0;
411 | margin-top: 0;
412 | margin-bottom: 1.45rem;
413 | line-height: 1.42;
414 | background: hsla(0, 0%, 0%, 0.04);
415 | border-radius: 3px;
416 | overflow: auto;
417 | word-wrap: normal;
418 | padding: 1rem;
419 | }
420 | table {
421 | margin-left: 0;
422 | margin-right: 0;
423 | margin-top: 0;
424 | padding-bottom: 0;
425 | padding-left: 0;
426 | padding-right: 0;
427 | padding-top: 0;
428 | margin-bottom: 1.45rem;
429 | font-size: 1rem;
430 | line-height: 1.45rem;
431 | border-collapse: collapse;
432 | width: 100%;
433 | }
434 | blockquote {
435 | padding: 1.45rem;
436 | margin-bottom: 1.45rem;
437 | margin-inline: 0;
438 | background-color: var(--kb-blockquote-bg);
439 | border-left: 2px solid var(--kb-text-color);
440 | }
441 | form {
442 | margin-left: 0;
443 | margin-right: 0;
444 | margin-top: 0;
445 | padding-bottom: 0;
446 | padding-left: 0;
447 | padding-right: 0;
448 | padding-top: 0;
449 | margin-bottom: 1.45rem;
450 | }
451 | noscript {
452 | margin-left: 0;
453 | margin-right: 0;
454 | margin-top: 0;
455 | padding-bottom: 0;
456 | padding-left: 0;
457 | padding-right: 0;
458 | padding-top: 0;
459 | margin-bottom: 1.45rem;
460 | }
461 | iframe {
462 | margin-left: 0;
463 | margin-right: 0;
464 | margin-top: 0;
465 | padding-bottom: 0;
466 | padding-left: 0;
467 | padding-right: 0;
468 | padding-top: 0;
469 | margin-bottom: 1.45rem;
470 | }
471 | address {
472 | margin-left: 0;
473 | margin-right: 0;
474 | margin-top: 0;
475 | padding-bottom: 0;
476 | padding-left: 0;
477 | padding-right: 0;
478 | padding-top: 0;
479 | margin-bottom: 1.45rem;
480 | }
481 | b {
482 | font-weight: bold;
483 | }
484 | strong {
485 | font-weight: bold;
486 | }
487 | dt {
488 | font-weight: bold;
489 | }
490 | th {
491 | font-weight: bold;
492 | }
493 | ol li {
494 | padding-left: 0;
495 | }
496 | ul li {
497 | padding-left: 0;
498 | }
499 | li > ol {
500 | margin-left: 1.45rem;
501 | margin-bottom: calc(1.45rem / 2);
502 | margin-top: calc(1.45rem / 2);
503 | }
504 | li > ul {
505 | margin-left: 1.45rem;
506 | margin-bottom: calc(1.45rem / 2);
507 | margin-top: calc(1.45rem / 2);
508 | }
509 | blockquote *:last-child {
510 | margin-bottom: 0;
511 | }
512 | li *:last-child {
513 | margin-bottom: 0;
514 | }
515 | p *:last-child {
516 | margin-bottom: 0;
517 | }
518 | li > p {
519 | margin-bottom: calc(1.45rem / 2);
520 | }
521 | code {
522 | line-height: 1.45rem;
523 | }
524 | kbd {
525 | font-size: 0.85rem;
526 | line-height: 1.45rem;
527 | }
528 | samp {
529 | font-size: 0.85rem;
530 | line-height: 1.45rem;
531 | }
532 | abbr {
533 | border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5);
534 | cursor: help;
535 | }
536 | acronym {
537 | border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5);
538 | cursor: help;
539 | }
540 | thead {
541 | text-align: left;
542 | }
543 | td,
544 | th {
545 | text-align: left;
546 | border-bottom: 1px solid hsla(0, 0%, 0%, 0.12);
547 | font-feature-settings: "tnum";
548 | -moz-font-feature-settings: "tnum";
549 | -ms-font-feature-settings: "tnum";
550 | -webkit-font-feature-settings: "tnum";
551 | padding-left: 0.96667rem;
552 | padding-right: 0.96667rem;
553 | padding-top: 0.725rem;
554 | padding-bottom: calc(0.725rem - 1px);
555 | }
556 | th:first-child,
557 | td:first-child {
558 | padding-left: 0;
559 | }
560 | th:last-child,
561 | td:last-child {
562 | padding-right: 0;
563 | }
564 | tt,
565 | code {
566 | background-color: var(--code-bg-color);
567 | border-radius: 3px;
568 | font-family: "SFMono-Regular", Consolas, "Roboto Mono", "Droid Sans Mono",
569 | "Liberation Mono", Menlo, Courier, monospace;
570 | padding: 0.2em 0.3em;
571 | }
572 | pre code {
573 | background: none;
574 | line-height: 1.42;
575 | padding: 0;
576 | }
577 | code {
578 | color: var(--code-color);
579 | }
580 |
581 | @media only screen and (max-width: 480px) {
582 | html {
583 | font-size: 100%;
584 | }
585 | }
586 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/styles/components/link.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hikerpig/gatsby-project-kb/2307f08f9d443f2156d77081ccab42264973da13/packages/gatsby-theme-kb/src/styles/components/link.css
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/styles/components/scrollbar.css:
--------------------------------------------------------------------------------
1 | @media (min-width: 1024px) {
2 | ::-webkit-scrollbar {
3 | width: 8px;
4 | height: 8px;
5 | }
6 |
7 | ::-webkit-scrollbar-thumb {
8 | background-color: var(--kb-scrollbar-thumb);
9 | border-radius: 10px;
10 | }
11 | }
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/styles/global.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | /* @tailwind components; */
3 | @tailwind utilities;
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/styles/theme.css:
--------------------------------------------------------------------------------
1 | .dark-mode {
2 | --kb-link-color: var(--frost3);
3 | --kb-tree-cur-color: var(--dark-3);
4 | --kb-font-family: 'Avenir', -apple-system, sans-serif;
5 | --kb-shadow-bg: rgba(0, 0, 0, 0.2);
6 | --kb-text-color: #eceff4;
7 | --kb-text-inverse-color: white;
8 | --kb-references-bg: var(--dark-2);
9 | --kb-search-highlight-bg: var(--dark-3);
10 | --kb-separator-color: #666;
11 | --kb-scrollbar-thumb: var(--dark-4);
12 | --kb-blockquote-bg: #353c48;
13 |
14 | --bg-color-1: var(--dark-1);
15 | --bg-color-2: var(--dark-2);
16 | --code-bg-color: var(--dark-2);
17 | --code-color: var(--purple);
18 | }
19 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/styles/tocbot.css:
--------------------------------------------------------------------------------
1 | body {
2 | --toc-text-hover-color: var(--kb-link-color);
3 | --toc-cur-highlight-color: var(--kb-text-color);
4 | }
5 |
6 | .tocbot {
7 | font-size: 0.9rem;
8 | }
9 |
10 | .tocbot li {
11 | margin-bottom: 0;
12 | }
13 |
14 | .toc-link::before {
15 | height: 1.4em;
16 | background-color: transparent;
17 | }
18 |
19 | .toc-link.is-active-link::before {
20 | background-color: var(--toc-cur-highlight-color);
21 | }
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/styles/vars.css:
--------------------------------------------------------------------------------
1 | /* obsidian */
2 | :scope {
3 | --light-1: #F9FAFB;
4 | --light-2: #F3F4F6;
5 | --light-3: #E7E5E4;
6 | --light-4: #ffffff;
7 |
8 | --dark-1: #2e3440;
9 | --dark-2: #3b4252;
10 | --dark-3: #434c5e;
11 | --dark-4: #4c566a;
12 |
13 | --frost1: #8fbcbb;
14 | --frost2: #88c0d0;
15 | --frost3: #a1c4e6;
16 | --frost4: #81a1c1;
17 |
18 | --red: #bf616a;
19 | --orange: #d08770;
20 | --yellow: #ebcb8b;
21 | --green: #a3be8c;
22 | --purple: #b48ead;
23 | }
24 |
25 | /* default theme light */
26 | body {
27 | --kb-link-color: #ff5449;
28 | --kb-tree-cur-color: #e7e5e4;
29 | --kb-font-family: 'Avenir', -apple-system, sans-serif;
30 | --kb-shadow-bg: rgba(0, 0, 0, 0.2);
31 | --kb-text-color: hsl(2deg 20% 15%);
32 | --kb-text-inverse-color: white;
33 | --kb-references-bg: #fafafa;
34 | --kb-search-highlight-bg: #eaeaea;
35 | --kb-note-bg: var(--bg-color-1);
36 | --kb-separator-color: #ddd;
37 | --kb-scrollbar-thumb: #ddd;
38 | --kb-blockquote-bg: #f6f6f6;
39 |
40 | --bg-color-1: #F9FAFB;
41 | --bg-color-2: #F3F4F6;
42 | --code-bg-color: #f0f0f0;
43 | --code-color: #333;
44 | }
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/templates/Topic.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { graphql } from 'gatsby'
3 |
4 | import Topic from '../components/Topic'
5 | import Seo from '../components/seo'
6 | import TopicLayout from '../components/TopicLayout'
7 | import { isServer } from '../env'
8 |
9 | // minimal template engine
10 | function evaluateTemplate(tpl, data) {
11 | const re = /{{([^}]+)?}}/g
12 | let match
13 | while ((match = re.exec(tpl))) {
14 | const key = match[1].trim()
15 | tpl = tpl.replace(match[0], data[key])
16 | }
17 | return tpl
18 | }
19 |
20 | export default (props) => {
21 | if (!isServer) {
22 | console.debug('Topic props', props)
23 | }
24 |
25 | const { pageContext } = props
26 | const file = props.data.file
27 | let wikiLinkLabelTemplateFn = null
28 | if (pageContext.wikiLinkLabelTemplate) {
29 | wikiLinkLabelTemplateFn = function (data) {
30 | try {
31 | return evaluateTemplate(props.pageContext.wikiLinkLabelTemplate, data)
32 | } catch (error) {
33 | console.error('error while evaluateTemplate', error)
34 | }
35 | }
36 | }
37 |
38 | const tocTypes = ('tocTypes' in pageContext ? pageContext.tocTypes: null) || []
39 | const showInlineTOC = tocTypes && tocTypes.includes('inline')
40 | const showSidebarTOC = tocTypes && tocTypes.includes('sidebar')
41 |
42 | return (
43 |
44 |
45 |
52 |
53 | )
54 | }
55 |
56 | export const pageQuery = graphql`
57 | query($id: String!) {
58 | file(id: { eq: $id }) {
59 | childMdx {
60 | body
61 | frontmatter {
62 | title
63 | private
64 | }
65 | tableOfContents
66 | ...GatsbyGardenReferences
67 | }
68 | fields {
69 | slug
70 | title
71 | }
72 | }
73 | }
74 | `
75 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/type.ts:
--------------------------------------------------------------------------------
1 | export type PageContext = {
2 | id: string
3 | }
4 |
5 | export type Reference = {
6 | target: {
7 | __typename: string
8 | body: string
9 | parent: SimpleFileNode
10 | }
11 | targetAnchor?: string
12 | refWord: string
13 | referrer: {
14 | parent: SimpleFileNode
15 | }
16 | contextLine: string
17 | label?: string
18 | }
19 |
20 | export type SimpleFileNode = {
21 | id: string
22 | fields: {
23 | title: string
24 | slug: string
25 | }
26 | }
27 |
28 | export type TopicFlie = {
29 | id: string
30 | fields: {
31 | slug: string
32 | title: string
33 | }
34 | childMdx: {
35 | body: string
36 | inboundReferences: Reference[]
37 | outboundReferences: Reference[]
38 | frontmatter: {
39 | title: string
40 | }
41 | tableOfContents: TableOfContents
42 | }
43 | }
44 |
45 | export type WikiLinkLabelTemplateFn = (data: { refWord: string; title: string }) => string
46 |
47 | /**
48 | * Table of content item generated by remark
49 | */
50 | export type TOCItem = {
51 | items?: TOCItem[]
52 | title: string
53 | url: string
54 | }
55 |
56 | export type TableOfContents = TOCItem
57 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/use-graph-data.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react'
2 | import { useStaticQuery, graphql } from 'gatsby'
3 | import type { Note } from 'note-graph'
4 | import { Reference, TopicFlie } from './type'
5 |
6 | export const useGraphData = () => {
7 | const data: { allFile: { nodes: TopicFlie[] } } = useStaticQuery(graphql`
8 | {
9 | allFile {
10 | nodes {
11 | id
12 | fields {
13 | title
14 | slug
15 | }
16 | childMdx {
17 | inboundReferences {
18 | referrer {
19 | ... on Mdx {
20 | parent {
21 | id
22 | }
23 | }
24 | }
25 | }
26 | outboundReferences {
27 | target {
28 | ... on Mdx {
29 | parent {
30 | id
31 | }
32 | }
33 | }
34 | }
35 | }
36 | }
37 | }
38 | }
39 | `)
40 |
41 | const { notesMap, fileNodesMap } = useMemo(() => {
42 | const notes: Note[] = []
43 | const notesMap = new Map()
44 | const fileNodesMap = new Map()
45 | data.allFile.nodes.forEach((node) => {
46 | if (!node.fields || !node.fields.slug) {
47 | return
48 | }
49 | fileNodesMap.set(node.id, node)
50 |
51 | const note: Note = {
52 | id: node.id,
53 | title: node.fields.title,
54 | linkTo: [],
55 | referencedBy: [],
56 | }
57 | notes.push(note)
58 | notesMap.set(node.id, note)
59 |
60 | node.childMdx.inboundReferences.forEach((x: Reference) => {
61 | note.referencedBy && x.referrer.parent?.id && note.referencedBy.push(x.referrer.parent.id)
62 | })
63 |
64 | node.childMdx.outboundReferences.forEach((x: Reference) => {
65 | note.linkTo && note.linkTo.push(x.target.parent.id)
66 | })
67 | })
68 |
69 | return { notesMap, fileNodesMap }
70 | }, [data])
71 |
72 | return { notesMap, fileNodesMap }
73 | }
74 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/use-search.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useMemo } from 'react'
2 | import FlexSearch from 'flexsearch'
3 | import { graphql, useStaticQuery } from 'gatsby'
4 |
5 | export type SearchResult = {
6 | id: string
7 | path: string
8 | title: string
9 | excerpt?: any
10 | }
11 |
12 | export const LOADING_ID = 'loading'
13 |
14 | export default (query: string, opts: { searchOptions?: any } = {}): SearchResult[] => {
15 | const [store, setStore] = useState(null)
16 | const [pathIndex, setPathIndex] = useState(null)
17 | const [titleIndex, setTitleIndex] = useState(null)
18 | const [bodyIndex, setBodyIndex] = useState(null)
19 | // has fetched search indices
20 | const [hasFetched, setHasFetched] = useState(false)
21 |
22 | const searchOptions = opts.searchOptions || {}
23 |
24 | const data = useStaticQuery(graphql`
25 | query SearchBarQuery {
26 | localSearchPaths {
27 | publicIndexURL
28 | publicStoreURL
29 | }
30 | localSearchTitles {
31 | publicIndexURL
32 | }
33 | localSearchBodies {
34 | publicIndexURL
35 | }
36 | }
37 | `)
38 | // console.log('use-search data is', data)
39 |
40 | useEffect(() => {
41 | if (hasFetched || !query) return
42 |
43 | setHasFetched(true)
44 |
45 | fetch(data.localSearchPaths.publicIndexURL)
46 | .then((result) => result.text())
47 | .then((res) => {
48 | const importedIndex = FlexSearch.create()
49 | importedIndex.import(res)
50 |
51 | setPathIndex(importedIndex)
52 | })
53 | fetch(data.localSearchTitles.publicIndexURL)
54 | .then((result) => result.text())
55 | .then((res) => {
56 | const importedIndex = FlexSearch.create()
57 | importedIndex.import(res)
58 |
59 | setTitleIndex(importedIndex)
60 | })
61 | fetch(data.localSearchBodies.publicIndexURL)
62 | .then((result) => result.text())
63 | .then((res) => {
64 | const importedIndex = FlexSearch.create()
65 | importedIndex.import(res)
66 |
67 | setBodyIndex(importedIndex)
68 | })
69 | fetch(data.localSearchPaths.publicStoreURL)
70 | .then((result) => result.json())
71 | .then((res) => {
72 | setStore(res)
73 | })
74 | }, [setPathIndex, setTitleIndex, setBodyIndex, setStore, data, query])
75 |
76 | return useMemo(() => {
77 | if (!query || !store || (!pathIndex && !bodyIndex && !titleIndex))
78 | return [
79 | {
80 | id: LOADING_ID,
81 | title: '',
82 | excerpt:
,
83 | },
84 | ]
85 |
86 | const rawPathResults = pathIndex
87 | ? pathIndex.search(query, searchOptions)
88 | : []
89 | const rawBodyResults = bodyIndex
90 | ? bodyIndex.search(query, searchOptions)
91 | : []
92 | const rawTitleResults = titleIndex
93 | ? titleIndex.search(query, searchOptions)
94 | : []
95 |
96 | const uniqIds = new Set()
97 |
98 | return (
99 | rawPathResults
100 | .concat(rawTitleResults)
101 | .concat(rawBodyResults)
102 | .filter((id) => {
103 | if (uniqIds.has(id)) {
104 | return false
105 | }
106 | uniqIds.add(id)
107 | return true
108 | })
109 | .map((id) => store[id])
110 | )
111 | }, [query, pathIndex, titleIndex, bodyIndex, store, searchOptions])
112 | }
113 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/use-window-size.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 |
3 | export function useWindowSize() {
4 | const isClient = typeof window === 'object'
5 |
6 | function getSize() {
7 | return {
8 | width: isClient ? window.innerWidth : 0,
9 | height: isClient ? window.innerHeight : 0,
10 | }
11 | }
12 |
13 | const [windowSize, setWindowSize] = useState(getSize)
14 |
15 | useEffect(() => {
16 | if (isClient) {
17 | return () => null
18 | }
19 | function handleResize() {
20 | setWindowSize(getSize())
21 | }
22 |
23 | window.addEventListener('resize', handleResize)
24 | return () => window.removeEventListener('resize', handleResize)
25 | }, []) // eslint-disable-line react-hooks/exhaustive-deps
26 | // Empty array ensures that effect is only run on mount and unmount
27 |
28 | return windowSize
29 | }
30 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export function recursivelyCallNode(node: T, getNextNode: (node: T) => T | void, cb: (node: T) => void) {
2 | cb(node)
3 | const nextNode = getNextNode(node)
4 | if (nextNode) {
5 | recursivelyCallNode(nextNode, getNextNode, cb)
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/src/utils/toc.ts:
--------------------------------------------------------------------------------
1 | import slugify from 'slugify'
2 |
3 | export function slugifyTitle(str: string) {
4 | return slugify(str)
5 | }
6 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | purge: false, // gatsby-plugin-purgecss will do
3 | darkMode: 'class', // or 'media' or 'class'
4 | theme: {
5 | extend: {},
6 | },
7 | variants: {
8 | extend: {},
9 | },
10 | plugins: [],
11 | }
12 |
--------------------------------------------------------------------------------
/packages/gatsby-theme-kb/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "esnext",
4 | "lib": ["dom", "esnext"],
5 | "importHelpers": true,
6 | "declaration": true,
7 | "sourceMap": true,
8 | "strict": true,
9 | "noUnusedLocals": true,
10 | "noUnusedParameters": true,
11 | "noImplicitReturns": true,
12 | "noImplicitAny": false,
13 | "noFallthroughCasesInSwitch": true,
14 | "moduleResolution": "node",
15 | "skipLibCheck": true,
16 | "jsx": "react",
17 | "esModuleInterop": true,
18 | "baseUrl": ".",
19 | "paths": {
20 | "src/*": ["./src"]
21 | },
22 | "typeRoots": ["./node_modules/@types", "./typings"]
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/packages/transformer-wiki-references/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to this project will be documented in this file.
4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5 |
6 | # [0.3.0-alpha.2](https://github.com/hikerpig/gatsby-project-kb/compare/@gatsby-project-kb/transformer-wiki-references@0.3.0-alpha.1...@gatsby-project-kb/transformer-wiki-references@0.3.0-alpha.2) (2021-04-28)
7 |
8 |
9 | ### Features
10 |
11 | * **transformer-wiki-references:** change lifecycle from 'source-nodes' to 'onPreBootstrap' ([4d35818](https://github.com/hikerpig/gatsby-project-kb/commit/4d35818598833e4741a9eec5eab13f4d21a92b59))
12 |
13 |
14 |
15 |
16 |
17 | # [0.3.0-alpha.1](https://github.com/hikerpig/gatsby-project-kb/compare/@gatsby-project-kb/transformer-wiki-references@0.3.0-alpha.0...@gatsby-project-kb/transformer-wiki-references@0.3.0-alpha.1) (2021-04-07)
18 |
19 |
20 | ### Bug Fixes
21 |
22 | * should ignore directory in contentTree ([5d14716](https://github.com/hikerpig/gatsby-project-kb/commit/5d14716a9287ac1d12e52f43105535c851c582fb))
23 |
24 |
25 | ### Features
26 |
27 | * will pass `ignore` option to @gatsby-project-kb/transformer-wiki-references ([547b98b](https://github.com/hikerpig/gatsby-project-kb/commit/547b98b1dfd03b8f3c3171ade9bc43d0b35db6d6))
28 |
29 |
30 |
31 |
32 |
33 | # [0.3.0-alpha.0](https://github.com/hikerpig/gatsby-project-kb/compare/@gatsby-project-kb/transformer-wiki-references@0.2.1...@gatsby-project-kb/transformer-wiki-references@0.3.0-alpha.0) (2021-04-02)
34 |
35 |
36 | ### Features
37 |
38 | * Add anchor reference support, related [#8](https://github.com/hikerpig/gatsby-project-kb/issues/8) ([3c0d13a](https://github.com/hikerpig/gatsby-project-kb/commit/3c0d13a78146dc9b6bf1215af367fbd1e3a999d4))
39 | * better wiki references resolving with `contentPath` option ([46b66a9](https://github.com/hikerpig/gatsby-project-kb/commit/46b66a973bbdd702dfadb523e9ab0ab91ed1d417))
40 |
41 |
42 |
43 |
44 |
45 | ## [0.2.1](https://github.com/hikerpig/gatsby-project-kb/compare/@gatsby-project-kb/transformer-wiki-references@0.2.0...@gatsby-project-kb/transformer-wiki-references@0.2.1) (2021-02-14)
46 |
47 |
48 | ### Bug Fixes
49 |
50 | * backlink should show referrer title and link ([8e89b4d](https://github.com/hikerpig/gatsby-project-kb/commit/8e89b4d22f85a2dc3b0f4902f9530a4692e81161))
51 |
52 |
53 |
54 |
55 |
56 | # [0.2.0](https://github.com/hikerpig/gatsby-project-kb/compare/@gatsby-project-kb/transformer-wiki-references@0.1.2...@gatsby-project-kb/transformer-wiki-references@0.2.0) (2021-02-13)
57 |
58 |
59 | ### Features
60 |
61 | * **gatsby-theme-kb:** show backlink context, related [#1](https://github.com/hikerpig/gatsby-project-kb/issues/1) ([685b92c](https://github.com/hikerpig/gatsby-project-kb/commit/685b92c3970116cc593581f52ecc6e0b66b0c146))
62 |
--------------------------------------------------------------------------------
/packages/transformer-wiki-references/README.md:
--------------------------------------------------------------------------------
1 | # `@gatsby-project-kb/transformer-wiki-references`
2 |
3 | A gatsby transformer plugin to extract references between markdown nodes. You can then use them to create bi-directional links.
4 |
5 | Forked from [mathieudutour/gatsby-digital-garden](https://github.com/mathieudutour/gatsby-digital-garden/tree/master/packages/gatsby-transformer-markdown-references).
6 |
7 | An example site for using this plugin is at [https://wiki.hikerpig.cn/](https://wiki.hikerpig.cn/).
8 |
9 |
10 | ## Install
11 |
12 | ```
13 | yarn add @gatsby-project-kb/transformer-wiki-references
14 | ```
15 |
16 | ## Usage
17 |
18 | ```javascript
19 | // In your gatsby-config.js
20 | module.exports = {
21 | plugins: [
22 | // after a markdown or Mdx transformer
23 | {
24 | resolve: `@gatsby-project-kb/transformer-wiki-references`,
25 | options: {
26 | contentPath: '/home/hikerpig/Notes',
27 | types: ["Mdx"], // or ["MarkdownRemark"] (or both)
28 | ignore: [
29 | '**/.cache/**',
30 | '**/.github/**',
31 | ],
32 | },
33 | },
34 | ],
35 | };
36 | ```
37 |
38 |
39 | ### Configuration options
40 |
41 | **`contentPath`** [string][optional]
42 |
43 | The path to directory of your notes, if there are nested folders in your notes, it's recommended that this option is provided so the plugin can resolve the references correctly.
44 |
45 | **`types`** [Array][optional]
46 |
47 | The types of the nodes to transform. Defaults to `['Mdx']`
48 |
49 | **`ignore`** [Array][optional]
50 |
51 | Will be used along with `contentPath`, to filter out those files you want to ignore. Accepts globs or regexps, any format that's supported by [anymatch](https://www.npmjs.com/package/anymatch).
52 |
53 |
54 | ## How to query for references
55 |
56 | Two types of references are available: `outboundReferences` and `inboundReferences`.
57 |
58 | The fields will be created in your site's GraphQL schema on the nodes of types specified in the options.
59 |
60 | ```graphql
61 | {
62 | allMdx {
63 | outboundReferences {
64 | ... on Mdx {
65 | id
66 | parent {
67 | id
68 | }
69 | }
70 | }
71 | inboundReferences {
72 | ... on Mdx {
73 | id
74 | parent {
75 | id
76 | ... on RoamPage {
77 | title
78 | }
79 | }
80 | }
81 | }
82 | }
83 | }
84 | ```
85 |
--------------------------------------------------------------------------------
/packages/transformer-wiki-references/__tests__/get-references.spec.ts:
--------------------------------------------------------------------------------
1 | import { getReferences } from '../src/get-references'
2 | import outdent from 'outdent'
3 |
4 | describe('getReferences', () => {
5 | it('get linkDefinition and wiki-links', () => {
6 | const text = outdent.string(`
7 | # Title
8 | [[link-to-1]]
9 | [[link-to-2]]
10 |
11 | [link-to-1] target-page
12 | `)
13 | const result = getReferences(text)
14 | expect(result.pages.map(o => o.target)).toEqual([
15 | 'link-to-1',
16 | 'link-to-2',
17 | 'link-to-1',
18 | ])
19 | })
20 |
21 | it('can extract multiple references of same target', () => {
22 | const text = outdent.string(`
23 | [[link]] is a wiki-link
24 | [[link]] also referenced in this line
25 | `)
26 | const result = getReferences(text)
27 | expect(result.pages.map(o => o.target)).toEqual([
28 | 'link',
29 | 'link',
30 | ])
31 | })
32 |
33 | it('get link definitions with context', () => {
34 | const text = outdent.string(`
35 | # Title
36 | [[link]] is a wiki-link
37 | `)
38 | const result = getReferences(text)
39 | expect(result.pages).toEqual([
40 | { target: 'link', contextLine: '[[link]] is a wiki-link' }
41 | ])
42 | })
43 |
44 | it('parse link hash as anchor', () => {
45 | const text = outdent.string(`
46 | [[link#anchor]] is a wiki-link with anchor
47 | `)
48 |
49 | const result = getReferences(text)
50 | expect(result.pages[0]).toMatchObject(
51 | { target: 'link', targetAnchor: 'anchor' },
52 | )
53 | })
54 |
55 | it('can extract markdown link', () => {
56 | const text = outdent.string(`
57 | [label](target) is a markdown link
58 | `)
59 |
60 | const result = getReferences(text)
61 | expect(result.pages[0]).toMatchObject(
62 | { target: 'target', label: 'label', contextLine: '[label](target) is a markdown link' },
63 | )
64 | })
65 | })
66 |
--------------------------------------------------------------------------------
/packages/transformer-wiki-references/__tests__/markdown-utils.spec.ts:
--------------------------------------------------------------------------------
1 | import { findTopLevelHeading } from '../src/markdown-utils'
2 | import outdent from 'outdent'
3 |
4 | describe('findTopLevelHeading', () => {
5 | it('get the first h1', () => {
6 | const text = outdent.string(`
7 | # n1
8 | # n2
9 | ## l2
10 | `)
11 | expect(findTopLevelHeading(text)).toEqual('n1')
12 | })
13 |
14 | it('get the first settext heading', () => {
15 | const text = outdent.string(`
16 | n1
17 | ===
18 | # n2
19 | `)
20 | expect(findTopLevelHeading(text)).toEqual('n1')
21 | })
22 |
23 | it('# header should also work', () => {
24 | const text = outdent.string(`
25 | # n1
26 | n2
27 | ===
28 | `)
29 | expect(findTopLevelHeading(text)).toEqual('n1')
30 | })
31 | })
32 |
--------------------------------------------------------------------------------
/packages/transformer-wiki-references/gatsby-node.js:
--------------------------------------------------------------------------------
1 | exports.onPreBootstrap = require("./lib/on-pre-bootstrap.js").onPreBootstrap;
2 | exports.onCreateNode = require("./lib/on-create-node.js").onCreateNode;
3 | exports.createSchemaCustomization = require("./lib/schema-customization").createSchemaCustomization;
4 | exports.setFieldsOnGraphQLNodeType = require("./lib/schema-customization").setFieldsOnGraphQLNodeType;
--------------------------------------------------------------------------------
/packages/transformer-wiki-references/jest.config.js:
--------------------------------------------------------------------------------
1 | // const { defaults: tsjPreset } = require('ts-jest/presets')
2 |
3 | module.exports = {
4 | preset: 'ts-jest',
5 | testEnvironment: 'jest-environment-jsdom-sixteen',
6 | testRegex: `.*/__tests__/.*|\\.(spec)\\.tsx?$`,
7 | };
8 |
--------------------------------------------------------------------------------
/packages/transformer-wiki-references/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@gatsby-project-kb/transformer-wiki-references",
3 | "version": "0.3.1",
4 | "description": "A gatsby transformer plugin to extract references between markdown nodes",
5 | "author": "hikerpig ",
6 | "homepage": "https://github.com/hikerpig/gatsby-project-kb#readme",
7 | "license": "MIT",
8 | "main": "lib/index.js",
9 | "directories": {
10 | "lib": "lib",
11 | "test": "__tests__"
12 | },
13 | "files": [
14 | "lib",
15 | "gatsby-*"
16 | ],
17 | "repository": {
18 | "type": "git",
19 | "url": "git+https://github.com/hikerpig/gatsby-project-kb.git"
20 | },
21 | "scripts": {
22 | "prepublish": "tsc",
23 | "test": "jest",
24 | "watch": "tsc --watch",
25 | "build": "tsc"
26 | },
27 | "bugs": {
28 | "url": "https://github.com/hikerpig/gatsby-project-kb/issues"
29 | },
30 | "devDependencies": {
31 | "@types/jest": "^26.0.20",
32 | "jest": "^26.6.3",
33 | "jest-environment-jsdom-sixteen": "^1.0.3",
34 | "outdent": "^0.8.0",
35 | "ts-jest": "^26.5.0",
36 | "typescript": "^4.1.2"
37 | },
38 | "resolutions": {
39 | "jest-environment-jsdom": "^26.0.1"
40 | },
41 | "dependencies": {
42 | "@nodelib/fs.walk": "^1.2.6",
43 | "anymatch": "^3.1.2"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/packages/transformer-wiki-references/src/cache.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs'
2 | import * as path from 'path'
3 | import { Node, GatsbyCache } from 'gatsby'
4 | import { nonNullable } from './util'
5 | import { References } from './get-references'
6 | import { NodeReference } from './type'
7 |
8 | export const cacheDirectory = (cache: any): string => {
9 | return cache.directory
10 | }
11 |
12 | export type CachedNode = {
13 | node: Node
14 | outboundReferences: References
15 | resolvedOutboundReferences?: NodeReference[]
16 | title: string
17 | aliases: string[]
18 | }
19 | export type InboundReferences = { [id: string]: NodeReference[] }
20 | const inboundFile = `___inboundReferences.json`
21 |
22 | export const getAllCachedNodes = async (cache: GatsbyCache, getNode: Function) => {
23 | const dir = cacheDirectory(cache)
24 | const files = await fs.promises.readdir(dir)
25 |
26 | return (
27 | await Promise.all(
28 | files.map((f) => {
29 | if (f === inboundFile) {
30 | return
31 | }
32 | const id = decodeURIComponent(f.replace(/\.json$/, ''))
33 | return getCachedNode(cache, id, getNode)
34 | })
35 | )
36 | ).filter(nonNullable)
37 | }
38 |
39 | export const setCachedNode = (cache: GatsbyCache, id: string, data: CachedNode) => {
40 | return fs.promises.writeFile(
41 | path.join(cacheDirectory(cache), `${encodeURIComponent(id)}.json`),
42 | JSON.stringify({
43 | outboundReferences: data.outboundReferences,
44 | title: data.title,
45 | aliases: data.aliases,
46 | resolvedOutboundReferences: data.resolvedOutboundReferences,
47 | })
48 | )
49 | }
50 |
51 | export const getCachedNode = async (
52 | cache: GatsbyCache,
53 | id: string,
54 | getNode: Function
55 | ): Promise => {
56 | const node = getNode(id)
57 |
58 | if (!node) {
59 | try {
60 | // clean up the cache if we have some file that aren't node
61 | await fs.promises.unlink(
62 | path.join(cacheDirectory(cache), `${encodeURIComponent(id)}.json`)
63 | )
64 | } catch (err) {}
65 | return undefined
66 | }
67 |
68 | try {
69 | const data = JSON.parse(
70 | await fs.promises.readFile(
71 | path.join(cacheDirectory(cache), `${encodeURIComponent(id)}.json`),
72 | 'utf8'
73 | )
74 | )
75 |
76 | return { node, ...data }
77 | } catch (err) {
78 | return undefined
79 | }
80 | }
81 |
82 | export const setInboundReferences = (cache: any, data: InboundReferences) => {
83 | return fs.promises.writeFile(
84 | path.join(cacheDirectory(cache), inboundFile),
85 | JSON.stringify(data)
86 | )
87 | }
88 |
89 | export const getInboundReferences = async (
90 | cache: any
91 | ): Promise => {
92 | try {
93 | return JSON.parse(
94 | await fs.promises.readFile(
95 | path.join(cacheDirectory(cache), inboundFile),
96 | 'utf8'
97 | )
98 | )
99 | } catch (err) {
100 | return undefined
101 | }
102 | }
103 |
104 | export const clearInboundReferences = async (cache: any) => {
105 | try {
106 | await fs.promises.unlink(path.join(cacheDirectory(cache), inboundFile))
107 | } catch (e) {}
108 | }
109 |
--------------------------------------------------------------------------------
/packages/transformer-wiki-references/src/compute-inbounds.ts:
--------------------------------------------------------------------------------
1 | import { basename, extname } from 'path'
2 | import { GatsbyCache } from 'gatsby'
3 | import { nonNullable } from './util'
4 | import {
5 | getAllCachedNodes,
6 | setCachedNode,
7 | setInboundReferences,
8 | InboundReferences,
9 | CachedNode,
10 | } from './cache'
11 | import { MdxNode, NodeReference } from './type'
12 | import { Reference } from './get-references'
13 | import { ContentNode, ContentTree, CONTEXT_TREE_CACHE_KEY } from './content-tree'
14 |
15 | function hasChildInArrayExcept(
16 | node: MdxNode,
17 | array: MdxNode[],
18 | except: string,
19 | getNode: (id: string) => MdxNode | undefined
20 | ): boolean {
21 | return node.children.some((id) => {
22 | if (id === except) {
23 | return false
24 | }
25 |
26 | if (array.some((x) => x.id === id)) {
27 | return true
28 | }
29 |
30 | const child = getNode(id)
31 | if (!child || !child.children || !child.children.length) {
32 | return false
33 | }
34 |
35 | return hasChildInArrayExcept(child, array, except, getNode)
36 | })
37 | }
38 |
39 | let currentGeneration: Promise | undefined
40 |
41 | function getFilePathFromCachedNode(x: CachedNode) {
42 | let filePath: string
43 | if (typeof x.node.fileAbsolutePath === 'string') {
44 | filePath = x.node.fileAbsolutePath
45 | } else if (typeof x.node.absolutePath === 'string') {
46 | filePath = x.node.absolutePath
47 | }
48 | return filePath
49 | }
50 |
51 | export async function generateData(cache: GatsbyCache, getNode: Function) {
52 | if (currentGeneration) {
53 | return currentGeneration
54 | }
55 |
56 | currentGeneration = Promise.resolve().then(async () => {
57 | const nodes = await getAllCachedNodes(cache, getNode)
58 | const inboundReferences: InboundReferences = {}
59 |
60 | let tree: ContentTree
61 | let cachedNodeToContentNodeMap: Map
62 | const contextTreeRepr = await cache.get(CONTEXT_TREE_CACHE_KEY)
63 | if (contextTreeRepr) {
64 | tree = ContentTree.fromRepr(contextTreeRepr)
65 |
66 | cachedNodeToContentNodeMap = new Map()
67 | nodes.forEach((cachedNode) => {
68 | const filePath = getFilePathFromCachedNode(cachedNode)
69 | if (!filePath) return
70 | const contentNode = tree.getNode(filePath)
71 | if (contentNode) {
72 | cachedNodeToContentNodeMap.set(cachedNode, contentNode)
73 | }
74 | })
75 | }
76 |
77 | function getRefNode(ref: Reference) {
78 | const title = ref.target
79 | if (!title) return
80 | let node = nodes.find((x) => {
81 | if (x.title === title || x.aliases.some((alias) => alias === title)) {
82 | return true
83 | }
84 |
85 | const filePath = getFilePathFromCachedNode(x)
86 | if (filePath) {
87 | if (tree) {
88 | if (cachedNodeToContentNodeMap.has(x)) {
89 | const contentNodeByTitle = tree.getNode(title)
90 | if (contentNodeByTitle === cachedNodeToContentNodeMap.get(x)) {
91 | return true
92 | }
93 | }
94 | } else {
95 | return basename(filePath, extname(filePath)) === title
96 | }
97 | }
98 | })
99 | return node
100 | }
101 |
102 | await Promise.all(
103 | nodes
104 | .map((node) => {
105 | const mapped = node.outboundReferences.pages
106 | .map((reference) => {
107 | const cachedNode = getRefNode(reference)
108 | if (!cachedNode) return null
109 | return {
110 | contextLine: reference.contextLine,
111 | target: cachedNode.node,
112 | referrer: reference.referrerNode,
113 | refWord: reference.label || reference.target,
114 | label: reference.label,
115 | targetAnchor: reference.targetAnchor,
116 | } as NodeReference
117 | })
118 | .filter(nonNullable)
119 |
120 | mapped.forEach((item) => {
121 | const nodeId = item.target.id
122 | if (!inboundReferences[nodeId]) {
123 | inboundReferences[nodeId] = []
124 | }
125 | inboundReferences[nodeId].push({
126 | contextLine: item.contextLine,
127 | target: item.target,
128 | referrer: node.node,
129 | refWord: item.refWord,
130 | })
131 | })
132 |
133 | return {
134 | ...node,
135 | resolvedOutboundReferences: mapped,
136 | }
137 | })
138 | .map((data) => setCachedNode(cache, data.node.id, data))
139 | )
140 |
141 | Object.keys(inboundReferences).forEach((nodeId) => {
142 | inboundReferences[nodeId] = inboundReferences[nodeId].filter(
143 | (reference) =>
144 | getNode(reference.target.parent) &&
145 | !hasChildInArrayExcept(
146 | getNode(reference.target.parent),
147 | inboundReferences[nodeId].map((o) => o.target),
148 | reference.target.id,
149 | getNode as any
150 | )
151 | )
152 | })
153 |
154 | await setInboundReferences(cache, inboundReferences)
155 |
156 | currentGeneration = undefined
157 |
158 | return true
159 | })
160 |
161 | return currentGeneration
162 | }
163 |
--------------------------------------------------------------------------------
/packages/transformer-wiki-references/src/content-tree.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path'
2 |
3 | export const CONTEXT_TREE_CACHE_KEY = 'content-tree-repr'
4 |
5 | export class ContentNode {
6 | constructor(public data: T) {}
7 | }
8 |
9 | export class ContentTree {
10 | pathMap = new Map()
11 | shortPathMap = new Map()
12 | keyMap = new Map()
13 | nodes = new Set()
14 |
15 | constructor(public rootNode: ContentNode) {}
16 |
17 | static fromRepr(repr: string) {
18 | const { nodes, rootPath } = JSON.parse(repr)
19 | const tree = new ContentTree(
20 | new ContentNode({
21 | name: rootPath,
22 | path: rootPath,
23 | key: rootPath,
24 | })
25 | )
26 | nodes.forEach((node) => {
27 | tree.add(node)
28 | })
29 | return tree
30 | }
31 |
32 | add(node: ContentNode) {
33 | const p = node.data.path
34 | this.pathMap.set(node.data.path, node)
35 | this.keyMap.set(node.data.key, node)
36 |
37 | const extLength = path.extname(p).length
38 | const pathWithoutExt = p.slice(0, -extLength)
39 | this.shortPathMap.set(pathWithoutExt, node)
40 |
41 | this.nodes.add(node)
42 | }
43 |
44 | getByName(p: string) {
45 | return this.keyMap.get(p)
46 | }
47 |
48 | serialize(): string {
49 | const nodes = Array.from(this.nodes)
50 | return JSON.stringify({ nodes, rootPath: this.rootNode.data.name }, null, 2)
51 | }
52 |
53 | getNode(input: string) {
54 | const relPath = path.relative(this.rootNode.data.name, input)
55 | const key = path.basename(relPath)
56 | if (this.pathMap.has(relPath)) return this.pathMap.get(relPath)
57 | if (this.shortPathMap.has(relPath)) return this.shortPathMap.get(relPath)
58 | if (this.keyMap.has(key)) return this.keyMap.get(key)
59 | }
60 | }
61 |
62 |
--------------------------------------------------------------------------------
/packages/transformer-wiki-references/src/get-references.ts:
--------------------------------------------------------------------------------
1 | import { cleanupMarkdown, findInMarkdownLines } from './markdown-utils'
2 | import { MdxNode } from './type'
3 |
4 | export type References = {
5 | pages: Reference[]
6 | }
7 |
8 | export type Reference = {
9 | /** target page slug */
10 | target: string
11 | targetAnchor?: string
12 | contextLine: string
13 | referrerNode?: MdxNode
14 | label?: string
15 | }
16 |
17 | export function rxWikiLink(): RegExp {
18 | const pattern = '\\[\\[([^\\]]+)\\]\\]' // [[wiki-link-regex]]
19 | return new RegExp(pattern, 'ig')
20 | }
21 |
22 | export function rxBlockLink(): RegExp {
23 | const pattern = '\\(\\(([^\\]]+)\\)\\)' // ((block-link-regex))
24 | return new RegExp(pattern, 'ig')
25 | }
26 |
27 | export function rxHashtagLink(): RegExp {
28 | const pattern = '(?:^|\\s)#([^\\s]+)' // #hashtag
29 | return new RegExp(pattern, 'ig')
30 | }
31 |
32 | const getLinkDefinitionPattern = () => /^\[([\d\w-_]+)\]\s+(.*)/ig // [defintion]: target
33 |
34 | export function rxMarkdownLink(): RegExp {
35 | // const pattern = '\\[\\[([^\\]]+)\\]\\]' // [](markdown-link)
36 | const pattern = /\[(.*)\]\((.*)\)/ig // [](markdown-link)
37 | return pattern
38 | }
39 |
40 | function findReferenceWithPattern(md: string, pattern: RegExp): Reference[] {
41 | return findInMarkdownLines(md, pattern).map(({ lineContent, matchStr }) => {
42 | let target = matchStr
43 | let targetAnchor: string
44 | const hashIndex = matchStr.indexOf('#')
45 | if (hashIndex > -1) {
46 | target = matchStr.substring(0, hashIndex)
47 | targetAnchor = matchStr.substring(hashIndex + 1)
48 | }
49 | const ref: Reference = {
50 | target,
51 | contextLine: lineContent,
52 | }
53 | if (targetAnchor !== undefined) {
54 | ref.targetAnchor = targetAnchor
55 | }
56 | return ref
57 | })
58 | }
59 |
60 | function findMarkdownLinkReference(md: string) {
61 | const pattern = rxMarkdownLink()
62 | return findInMarkdownLines(md, pattern).map(({ lineContent, match }) => {
63 | let [_, label, target] = match
64 | const ref: Reference = {
65 | target,
66 | contextLine: lineContent,
67 | label,
68 | }
69 | return ref
70 | })
71 | }
72 |
73 | export const getReferences = (string: string, onReferenceAdd?: (ref: Reference) => Reference) => {
74 | const md = cleanupMarkdown(string)
75 | onReferenceAdd = onReferenceAdd || ((o) => o)
76 |
77 | const references: References = {
78 | pages: [
79 | ...findReferenceWithPattern(md, rxHashtagLink()),
80 | ...findReferenceWithPattern(md, rxWikiLink()),
81 | ...findReferenceWithPattern(md, getLinkDefinitionPattern()),
82 | ...findMarkdownLinkReference(md),
83 | ].map(o => onReferenceAdd(o)),
84 | }
85 |
86 | return references
87 | }
88 |
--------------------------------------------------------------------------------
/packages/transformer-wiki-references/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './markdown-utils'
2 | export * from './type'
--------------------------------------------------------------------------------
/packages/transformer-wiki-references/src/markdown-utils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Adapted from vscode-markdown/src/util.ts
3 | * https://github.com/yzhang-gh/vscode-markdown/blob/master/src/util.ts
4 | */
5 |
6 | export const REGEX_FENCED_CODE_BLOCK = /^( {0,3}|\t)```[^`\r\n]*$[\w\W]+?^( {0,3}|\t)``` *$/gm
7 |
8 | const SETTEXT_REGEX = /(.*)\n={3,}/
9 |
10 | export function markdownHeadingToPlainText(text: string) {
11 | // Remove Markdown syntax (bold, italic, links etc.) in a heading
12 | // For example: `_italic_` -> `italic`
13 | return text.replace(/\[([^\]]*)\]\[[^\]]*\]/, (_, g1) => g1)
14 | }
15 |
16 | export function rxMarkdownHeading(level: number): RegExp {
17 | const pattern = `^#{${level}}\\s+(.+)$`
18 | return new RegExp(pattern, 'im')
19 | }
20 |
21 | export function findTopLevelHeading(md: unknown): string | null {
22 | if (typeof md !== 'string') {
23 | return null
24 | }
25 |
26 | const headingRegex = rxMarkdownHeading(1)
27 | const headingMatch = headingRegex.exec(md)
28 | const settextMatch = SETTEXT_REGEX.exec(md)
29 | let match = headingMatch
30 | if (settextMatch && (!headingMatch || settextMatch.index < headingMatch.index)) {
31 | match = settextMatch
32 | }
33 | if (match) {
34 | return markdownHeadingToPlainText(match[1])
35 | }
36 |
37 | return null
38 | }
39 |
40 | export function cleanupMarkdown(markdown: string) {
41 | const replacer = (foundStr: string) => foundStr.replace(/[^\r\n]/g, '')
42 | return markdown
43 | .replace(REGEX_FENCED_CODE_BLOCK, replacer) //// Remove fenced code blocks
44 | .replace(//g, replacer) //// Remove comments
45 | .replace(/^---[\W\w]+?(\r?\n)---/, replacer) //// Remove YAML front matter
46 | }
47 |
48 | export function findInMarkdown(markdown: string, regex: RegExp): string[] {
49 | const unique = new Set()
50 |
51 | let match
52 | while ((match = regex.exec(markdown))) {
53 | const [, name] = match
54 | if (name) {
55 | unique.add(name)
56 | }
57 | }
58 |
59 | return Array.from(unique)
60 | }
61 |
62 | export function findInMarkdownLines(markdown: string, regex: RegExp) {
63 | const lines = markdown.split('\n')
64 | const result: {
65 | matchStr: string
66 | lineContent: string
67 | match: RegExpMatchArray | undefined
68 | lineNum: number
69 | }[] = []
70 |
71 | lines.forEach((lineContent, lineNum) => {
72 | let match
73 | regex.lastIndex = 0
74 | while ((match = regex.exec(lineContent))) {
75 | const [, name] = match
76 | if (name) {
77 | result.push({ matchStr: name, match, lineContent, lineNum })
78 | }
79 | }
80 | })
81 | return result
82 | }
83 |
--------------------------------------------------------------------------------
/packages/transformer-wiki-references/src/on-create-node.ts:
--------------------------------------------------------------------------------
1 | import { CreateNodeArgs, Node } from 'gatsby'
2 | import { getReferences } from './get-references'
3 | import { PluginOptions, resolveOptions } from './options'
4 | import { clearInboundReferences, setCachedNode } from './cache'
5 | import { findTopLevelHeading } from './markdown-utils'
6 | import { MdxNode } from './type'
7 |
8 |
9 | function getTitle(node: MdxNode, content: string) {
10 | if (
11 | typeof node.frontmatter === 'object' &&
12 | node.frontmatter &&
13 | 'title' in node.frontmatter &&
14 | node.frontmatter['title']
15 | ) {
16 | return node.frontmatter['title'] as string
17 | }
18 | return findTopLevelHeading(content) || ''
19 | }
20 |
21 | function getAliases(node: MdxNode) {
22 | if (
23 | typeof node.frontmatter === 'object' &&
24 | node.frontmatter &&
25 | 'aliases' in node.frontmatter &&
26 | Array.isArray(node.frontmatter['aliases'])
27 | ) {
28 | return node.frontmatter['aliases'] as string[]
29 | }
30 | return []
31 | }
32 |
33 | export const onCreateNode = async (
34 | { cache, node, loadNodeContent, getNode }: CreateNodeArgs,
35 | _options?: PluginOptions
36 | ) => {
37 | const options = resolveOptions(_options)
38 |
39 | // if we shouldn't process this node, then return
40 | if (!options.types.includes(node.internal.type)) {
41 | return
42 | }
43 |
44 | const content = await loadNodeContent(node)
45 |
46 | const outboundReferences = getReferences(content, (ref) => {
47 | ref.referrerNode = node
48 | return ref
49 | })
50 |
51 | const title = getTitle(node as MdxNode, content)
52 | const aliases = getAliases(node as MdxNode)
53 |
54 | await clearInboundReferences(cache)
55 | await setCachedNode(cache, node.id, {
56 | node,
57 | outboundReferences,
58 | title,
59 | aliases,
60 | })
61 | }
62 |
--------------------------------------------------------------------------------
/packages/transformer-wiki-references/src/on-pre-bootstrap.ts:
--------------------------------------------------------------------------------
1 | import { ParentSpanPluginArgs } from 'gatsby'
2 | import * as path from 'path'
3 | import * as fsWalk from '@nodelib/fs.walk'
4 | import { PluginOptions, resolveOptions } from './options'
5 | import { ContentTree, ContentNode, CONTEXT_TREE_CACHE_KEY } from './content-tree'
6 | import anymatch from 'anymatch'
7 |
8 | export const onPreBootstrap = async (
9 | { cache }: ParentSpanPluginArgs,
10 | _options?: PluginOptions
11 | ) => {
12 | const options = resolveOptions(_options)
13 | const { contentPath, extensions, ignore } = options
14 | if (!options.contentPath) return
15 |
16 | const tree = new ContentTree(
17 | new ContentNode({
18 | name: contentPath,
19 | path: contentPath,
20 | key: contentPath,
21 | })
22 | )
23 |
24 | const entries: fsWalk.Entry[] = fsWalk
25 | .walkSync(contentPath, {
26 | basePath: '',
27 | deepFilter: (entry) => {
28 | return !/\/node_modules\//.test(entry.path)
29 | },
30 | entryFilter: (entry) => {
31 | if (ignore) {
32 | return !anymatch(ignore, entry.path)
33 | }
34 | return true
35 | }
36 | })
37 | .filter((entry) => {
38 | if (entry.dirent.isDirectory()) return false
39 | if (!extensions.includes(path.extname(entry.name))) {
40 | return false
41 | }
42 | return true
43 | })
44 | // walk the contentPath and collect all possible files,
45 | // then write the constructed file tree to gatsby cache for further usage in `setFieldsOnGraphQLNodeType` phase
46 | entries.forEach((entry) => {
47 | const node = new ContentNode({
48 | name: entry.name,
49 | path: entry.path,
50 | key: path.basename(entry.name, path.extname(entry.name)),
51 | })
52 | tree.add(node)
53 | })
54 | const repr = tree.serialize()
55 | await cache.set(CONTEXT_TREE_CACHE_KEY, repr)
56 | }
57 |
--------------------------------------------------------------------------------
/packages/transformer-wiki-references/src/options.ts:
--------------------------------------------------------------------------------
1 | export type PluginOptions = {
2 | types?: string[]
3 | extensions?: string[]
4 | contentPath?: string
5 | ignore?: string[]
6 | }
7 |
8 | const defaultOptions = {
9 | types: ['Mdx'],
10 | extensions: ['.md', '.mdx'],
11 | }
12 |
13 | export const resolveOptions = (options?: PluginOptions) => {
14 | return { ...defaultOptions, ...options }
15 | }
16 |
--------------------------------------------------------------------------------
/packages/transformer-wiki-references/src/schema-customization.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CreateSchemaCustomizationArgs,
3 | SetFieldsOnGraphQLNodeTypeArgs,
4 | Node,
5 | } from 'gatsby'
6 | import { PluginOptions, resolveOptions } from './options'
7 | import { generateData } from './compute-inbounds'
8 | import { getCachedNode, getInboundReferences } from './cache'
9 | import { nonNullable } from './util'
10 | import { NodeReference } from './type'
11 |
12 | export const createSchemaCustomization = (
13 | { actions }: CreateSchemaCustomizationArgs,
14 | _options?: PluginOptions
15 | ) => {
16 | const options = resolveOptions(_options)
17 | actions.createTypes(`
18 | union ReferenceTarget = ${options.types.join(' | ')}
19 |
20 | type NodeReference {
21 | target: ReferenceTarget
22 | refWord: String
23 | referrer: ReferenceTarget
24 | targetAnchor: String
25 | contextLine: String
26 | label: String
27 | }
28 | `)
29 | }
30 |
31 | export const setFieldsOnGraphQLNodeType = (
32 | { cache, type, getNode }: SetFieldsOnGraphQLNodeTypeArgs,
33 | _options?: PluginOptions
34 | ) => {
35 | const options = resolveOptions(_options)
36 |
37 | // if we shouldn't process this node, then return
38 | if (!options.types.includes(type.name)) {
39 | return {}
40 | }
41 |
42 | return {
43 | outboundReferences: {
44 | type: `[NodeReference!]!`,
45 | resolve: async (node: Node) => {
46 | let cachedNode = await getCachedNode(cache, node.id, getNode)
47 |
48 | if (!cachedNode || !cachedNode.resolvedOutboundReferences) {
49 | await generateData(cache, getNode)
50 | cachedNode = await getCachedNode(cache, node.id, getNode)
51 | }
52 |
53 | if (cachedNode && cachedNode.resolvedOutboundReferences) {
54 | return cachedNode.resolvedOutboundReferences
55 | .map((refNode) => {
56 | const { target } = refNode
57 | const targetNode = getNode(target.id)
58 | if (!targetNode) return null
59 | return {
60 | ...refNode,
61 | target: targetNode,
62 | } as NodeReference
63 | })
64 | .filter(nonNullable)
65 | }
66 |
67 | return []
68 | },
69 | },
70 | inboundReferences: {
71 | type: `[NodeReference!]!`,
72 | resolve: async (node: Node) => {
73 | let data = await getInboundReferences(cache)
74 |
75 | if (!data) {
76 | await generateData(cache, getNode)
77 | data = await getInboundReferences(cache)
78 | }
79 |
80 | if (data) {
81 | return (data[node.id] || [])
82 | .map((refNode) => {
83 | const { target } = refNode
84 | const targetNode = getNode(target.id)
85 | if (!targetNode) return null
86 | return {
87 | ...refNode,
88 | target: targetNode,
89 | } as NodeReference
90 | })
91 | .filter(nonNullable)
92 | }
93 |
94 | return []
95 | },
96 | },
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/packages/transformer-wiki-references/src/type.ts:
--------------------------------------------------------------------------------
1 | import { Node } from 'gatsby'
2 |
3 | export interface MdxNode extends Node {
4 | frontmatter?: {
5 | title?: string
6 | aliases?: string[]
7 | }
8 | }
9 |
10 | /**
11 | * Used in GraphQL
12 | */
13 | export type NodeReference = {
14 | target: MdxNode
15 | refWord: string
16 | targetAnchor?: string
17 | referrer: MdxNode
18 | contextLine: string
19 | /**
20 | * Markdown relative link has this
21 | */
22 | label?: string
23 | }
24 |
--------------------------------------------------------------------------------
/packages/transformer-wiki-references/src/util.ts:
--------------------------------------------------------------------------------
1 | export function nonNullable(value: T): value is NonNullable {
2 | return value !== null && value !== undefined
3 | }
4 |
--------------------------------------------------------------------------------
/packages/transformer-wiki-references/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2017",
4 | "baseUrl": ".",
5 | "outDir": "lib",
6 | "rootDir": "src",
7 | "moduleResolution": "node",
8 | "module": "commonjs",
9 | "declaration": true,
10 | "inlineSourceMap": false,
11 | "esModuleInterop": true,
12 | "lib": ["es5", "dom"],
13 | "types": ["jest", "node"],
14 | "typeRoots": ["../../node_modules/@types"],
15 | "skipLibCheck": true,
16 | "paths": {
17 | "src/*": ["./src/*"]
18 | },
19 | "jsx": "react"
20 | },
21 | "include": ["src/**/*.ts"],
22 | "exclude": ["node_modules/**"],
23 | "compileOnSave": false
24 | }
25 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "esnext",
4 | "lib": ["dom", "esnext"],
5 | "importHelpers": true,
6 | "declaration": true,
7 | "sourceMap": true,
8 | "strict": true,
9 | "noUnusedLocals": true,
10 | "noUnusedParameters": true,
11 | "noImplicitReturns": true,
12 | "noImplicitAny": false,
13 | "noFallthroughCasesInSwitch": true,
14 | "moduleResolution": "node",
15 | "jsx": "react",
16 | "esModuleInterop": true
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.build.json",
3 | "include": ["packages", "types", "scripts", "example"],
4 | "compilerOptions": {
5 | "allowJs": false,
6 | "baseUrl": ".",
7 | "typeRoots": ["./node_modules/@types", "./types"],
8 | "paths": {
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------