├── .eslintrc.js
├── .github
├── dependabot.yml
└── workflows
│ └── github-actions.yml
├── .gitignore
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── demo
├── .gitignore
├── README.md
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
├── src
│ ├── App.js
│ ├── App.test.js
│ ├── components
│ │ ├── Bottom.jsx
│ │ ├── Content.jsx
│ │ └── Top.jsx
│ ├── consts.js
│ ├── index.css
│ ├── index.js
│ ├── reportWebVitals.js
│ ├── setupTests.js
│ └── styles
│ │ ├── Bottom.css
│ │ ├── Content.css
│ │ └── Top.css
└── yarn.lock
├── jest.config.js
├── package.json
├── release
└── README.md
├── rollup.config.js
├── src
├── Heading.tsx
├── __tests__
│ ├── Heading.test.tsx
│ ├── __snapshots__
│ │ ├── Heading.test.tsx.snap
│ │ └── index.test.tsx.snap
│ ├── index.test.tsx
│ └── utils.test.ts
├── index.tsx
├── styles.module.css
├── typings.d.ts
└── utils.ts
├── tsconfig.json
├── tsconfig.test.json
└── yarn.lock
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "parser": "@typescript-eslint/parser",
3 | "ignorePatterns": [
4 | "typings.d.ts"
5 | ],
6 | "settings": {
7 | "react": {
8 | "version": "detect"
9 | }
10 | },
11 | "extends": [
12 | "eslint:recommended",
13 | "plugin:@typescript-eslint/recommended",
14 | "plugin:jest/recommended",
15 | "plugin:react/recommended",
16 | ],
17 | "rules": {
18 | "react/prop-types": "off",
19 | "no-control-regex": 0,
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # Basic dependabot.yml file with
2 | # minimum configuration for two package managers
3 |
4 | version: 2
5 | updates:
6 | # Enable version updates for npm
7 | - package-ecosystem: "npm"
8 | # Look for `package.json` and `lock` files in the `root` directory
9 | directory: "/"
10 | # Check the npm registry for updates every day (weekdays)
11 | schedule:
12 | interval: "daily"
13 |
--------------------------------------------------------------------------------
/.github/workflows/github-actions.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on: push
3 | jobs:
4 | jest:
5 | runs-on: ubuntu-latest
6 | steps:
7 | - uses: actions/checkout@v2
8 | - name: Install modules
9 | run: yarn
10 | - name: Run tests
11 | run: yarn run test
12 | eslint:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: Install modules
17 | run: yarn
18 | - name: Run ESLint
19 | run: yarn run lint
20 | create-release-on-github:
21 | name: Create Release
22 | runs-on: ubuntu-latest
23 | needs: [jest, eslint]
24 | if: startsWith(github.ref, 'refs/tags/v')
25 | steps:
26 | - name: Checkout code
27 | uses: actions/checkout@v2
28 | - name: Create Release
29 | id: create_release
30 | uses: actions/create-release@v1
31 | env:
32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
33 | with:
34 | tag_name: ${{ github.ref }}
35 | release_name: Release ${{ github.ref }}
36 | body: ''
37 | draft: false
38 | prerelease: false
39 | publish:
40 | runs-on: ubuntu-latest
41 | needs: [jest, eslint, create-release-on-github]
42 | if: startsWith(github.ref, 'refs/tags/v')
43 | steps:
44 | - uses: actions/checkout@v2
45 | - uses: actions/setup-node@v2
46 | with:
47 | node-version: 14
48 | registry-url: 'https://registry.npmjs.org'
49 | - run: npm install
50 | - run: npm publish
51 | env:
52 | NODE_AUTH_TOKEN: ${{ secrets.NPM__TOKEN }}
53 |
54 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # See https://help.github.com/ignore-files/ for more about ignoring files.
3 |
4 | # dependencies
5 | node_modules
6 |
7 | # builds
8 | build
9 | dist
10 | .rpt2_cache
11 |
12 | # misc
13 | .DS_Store
14 |
15 | npm-debug.log*
16 | yarn-debug.log*
17 | yarn-error.log*
18 |
19 | # jest coverage
20 | coverage
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.autoSave": "onFocusChange",
3 | "eslint.validate": [
4 | "javascript",
5 | "javascriptreact",
6 | "typescript",
7 | "typescriptreact",
8 | ],
9 | "eslint.workingDirectories": [
10 | "./client"
11 | ],
12 | "editor.codeActionsOnSave": {
13 | "source.fixAll.eslint": true
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 K-Sato
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | 
3 |
4 |
5 |
6 | [](https://www.npmjs.com/package/react-toc)
7 | 
8 | 
9 | 
10 |
11 |
12 | ## Overview
13 |
14 | - The idea is that you can automatically create a customizable table of contents from your markdown text.
15 | - It's regex based. Thus, managed to keep the bundle size pretty tiny.(Check it out on [BUNDLEPHOBIA](https://bundlephobia.com/package/react-toc))
16 |
17 |
18 | [](https://gyazo.com/3e63575305ea5c12e1d52b73a96cdfaa)
19 |
20 | ### Demo
21 |
22 | [Check out the demo page.](https://react-toc-k-sato1995.vercel.app/)
23 |
24 | ## Installation
25 |
26 | ```bash
27 | npm install --save react-toc
28 | ```
29 |
30 | or
31 |
32 | ```bash
33 | yarn add react-toc
34 | ```
35 |
36 | ## Usage
37 |
38 | Import Toc from the package and pass props to it. As for now, `markdownText` is the only required prop.
39 |
40 | ```jsx
41 | import React from "react";
42 | import Toc from "react-toc";
43 |
44 | const Example = () => {
45 | const yourMarkdownText = "# test \n your markdown Content # test2\n";
46 | return ;
47 | };
48 |
49 | export default Example;
50 | ```
51 |
52 | ## Props
53 |
54 | | Name | Type | Description |
55 | | -------------------- | ----------------- | ----------------------------------------------------------------------------- |
56 | | `markdownText` | string | **Required** The markdown text you want to creat a TOC from. |
57 | | `titleLimit` | number | The maximum length of each title in the TOC. |
58 | | `highestHeadingLevel` | number | The highest level of headings you want to extract from the given markdownText. |
59 | | `lowestHeadingLevel` | number | The lowest level of headings you want to extract from the given markdownText. |
60 | | `className` | strig | Your custom className. |
61 | | `type` | "deafult" or"raw" | The type of a TOC you want to use. |
62 | | `customMatchers` | { [key: string]: string } | The matchers you want to use to replace the letters with. |
63 |
64 | ## CustomDesign
65 |
66 | ### Add a custom className
67 |
68 | Pass `className` like the code below.
69 |
70 | ```jsx
71 | import React from "react";
72 | import Toc from "react-toc";
73 |
74 | const Example = () => {
75 | const yourMarkdownText = "# test \n your markdown Content # test2\n";
76 | return ;
77 | };
78 |
79 | export default Example;
80 | ```
81 |
82 | ### Style the custom class
83 |
84 | Now you can style your custom class just like the code below.
85 |
86 | ```css
87 | .customClassName {
88 | border: solid 1px;
89 | }
90 | .customClassName > li {
91 | padding-bottom: 10px;
92 | }
93 | ```
94 |
95 |
96 | ## Custom Matchers
97 |
98 | You can use the `customMatchers` prop to replace letters in your toc.
99 | For instance, if you want to replace `?` or `!` with `-` in your list, you can simply do this.
100 |
101 | ```jsx
102 | import React from "react";
103 | import Toc from "react-toc";
104 |
105 | const Example = () => {
106 | const yourMarkdownText = "# test \n your markdown Content # test2\n";
107 | const matchers = { "[?!]": "-" }
108 |
109 | return ;
110 | };
111 |
112 | export default Example;
113 | ```
114 |
115 | You can also give more options to the `customMatchers` prop like the code below.
116 |
117 | ```jsx
118 | import React from "react";
119 | import Toc from "react-toc";
120 |
121 | const Example = () => {
122 | const yourMarkdownText = "# test \n your markdown Content # test2\n";
123 | const matchers = { "[?!]": "-", "\\*": "" }
124 |
125 | return ;
126 | };
127 |
128 | export default Example;
129 | ```
130 |
131 |
132 | ## Development
133 |
134 | - Install dev dependencies.
135 |
136 | ```
137 | $ yarn install
138 | ```
139 |
140 | - Test
141 |
142 | ```
143 | $ yarn test
144 | ```
145 |
146 | - Lint
147 |
148 | ```
149 | $ yarn lint
150 | ```
151 |
152 | - Run demo locally
153 |
154 | ```
155 | $ cd demo && yarn && yarn start
156 | ```
157 |
158 | ## License
159 |
160 | [MIT](https://github.com/K-Sato1995/react-toc/blob/master/LICENSE)
161 |
--------------------------------------------------------------------------------
/demo/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/demo/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Create React App
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `npm start`
10 |
11 | Runs the app in the development mode.\
12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
13 |
14 | The page will reload when you make changes.\
15 | You may also see any lint errors in the console.
16 |
17 | ### `npm test`
18 |
19 | Launches the test runner in the interactive watch mode.\
20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
21 |
22 | ### `npm run build`
23 |
24 | Builds the app for production to the `build` folder.\
25 | It correctly bundles React in production mode and optimizes the build for the best performance.
26 |
27 | The build is minified and the filenames include the hashes.\
28 | Your app is ready to be deployed!
29 |
30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
31 |
32 | ### `npm run eject`
33 |
34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!**
35 |
36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37 |
38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
39 |
40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
41 |
42 | ## Learn More
43 |
44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
45 |
46 | To learn React, check out the [React documentation](https://reactjs.org/).
47 |
48 | ### Code Splitting
49 |
50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
51 |
52 | ### Analyzing the Bundle Size
53 |
54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
55 |
56 | ### Making a Progressive Web App
57 |
58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
59 |
60 | ### Advanced Configuration
61 |
62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
63 |
64 | ### Deployment
65 |
66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
67 |
68 | ### `npm run build` fails to minify
69 |
70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
71 |
--------------------------------------------------------------------------------
/demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "demo",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.16.5",
7 | "@testing-library/react": "^13.4.0",
8 | "@testing-library/user-event": "^14.4.3",
9 | "react": "^18.2.0",
10 | "react-dom": "^18.2.0",
11 | "react-markdown": "^8.0.3",
12 | "react-syntax-highlighter": "^15.5.0",
13 | "react-toc": "^3.0.0",
14 | "remark-gfm": "^3.0.1",
15 | "web-vitals": "^3.0.3"
16 | },
17 | "devDependencies": {
18 | "react-scripts": "^5.0.1"
19 | },
20 | "scripts": {
21 | "start": "react-scripts start",
22 | "build": "react-scripts build",
23 | "test": "react-scripts test",
24 | "eject": "react-scripts eject"
25 | },
26 | "eslintConfig": {
27 | "extends": [
28 | "react-app",
29 | "react-app/jest"
30 | ]
31 | },
32 | "browserslist": {
33 | "production": [
34 | ">0.2%",
35 | "not dead",
36 | "not op_mini all"
37 | ],
38 | "development": [
39 | "last 1 chrome version",
40 | "last 1 firefox version",
41 | "last 1 safari version"
42 | ]
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/demo/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/K-Sato1995/react-toc/af8569d6dff8cbfb685b5614f7952eb19dceab73/demo/public/favicon.ico
--------------------------------------------------------------------------------
/demo/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/demo/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/K-Sato1995/react-toc/af8569d6dff8cbfb685b5614f7952eb19dceab73/demo/public/logo192.png
--------------------------------------------------------------------------------
/demo/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/K-Sato1995/react-toc/af8569d6dff8cbfb685b5614f7952eb19dceab73/demo/public/logo512.png
--------------------------------------------------------------------------------
/demo/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/demo/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/demo/src/App.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Top from './components/Top'
3 | import Content from './components/Content';
4 | import Bottom from "./components/Bottom";
5 |
6 | function App() {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 | );
14 | }
15 |
16 | export default App;
17 |
--------------------------------------------------------------------------------
/demo/src/App.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import App from './App';
3 |
4 | test('renders learn react link', () => {
5 | render();
6 | const linkElement = screen.getByText(/learn react/i);
7 | expect(linkElement).toBeInTheDocument();
8 | });
9 |
--------------------------------------------------------------------------------
/demo/src/components/Bottom.jsx:
--------------------------------------------------------------------------------
1 | import '../styles/Bottom.css';
2 |
3 | const Bottom = () => {
4 |
5 | return (
6 |
9 | )
10 | };
11 |
12 | export default Bottom;
13 |
--------------------------------------------------------------------------------
/demo/src/components/Content.jsx:
--------------------------------------------------------------------------------
1 | import '../styles/Content.css';
2 | import React from "react";
3 | import { CONTENT as mdContent } from '../consts'
4 | import Toc from "react-toc";
5 | import remarkGfm from 'remark-gfm'
6 | import ReactMarkdown from 'react-markdown'
7 |
8 | import {vscDarkPlus} from 'react-syntax-highlighter/dist/esm/styles/prism'
9 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
10 |
11 | const flatten = (text, child) => {
12 | return typeof child === "string"
13 | ? text + child
14 | : React.Children.toArray(child.props.children).reduce(flatten, text);
15 | };
16 |
17 | export const HeadingRenderer = (props) => {
18 | var children = React.Children.toArray(props.children);
19 | var text = children.reduce(flatten, "");
20 | var slug = text.toLowerCase().replace(/[!?\s]/g, "-");
21 | return React.createElement(
22 | "h" + props.level,
23 | { id: slug, className: "anchor" },
24 | props.children
25 | );
26 | };
27 |
28 | const Content = () => {
29 | return (
30 |
31 |
Table of contents
32 |
33 |
34 |
49 | ) : (
50 |
51 | {children}
52 |
53 | )
54 | },
55 | h1: HeadingRenderer,
56 | h2: HeadingRenderer,
57 | }}
58 | />
59 |
60 | )
61 | };
62 |
63 | export default Content;
64 |
--------------------------------------------------------------------------------
/demo/src/components/Top.jsx:
--------------------------------------------------------------------------------
1 | import '../styles/Top.css';
2 |
3 | const Top = () => {
4 |
5 | return (
6 |
7 |
react-toc
8 |
9 | )
10 | };
11 |
12 | export default Top;
13 |
--------------------------------------------------------------------------------
/demo/src/consts.js:
--------------------------------------------------------------------------------
1 | const CONTENT = `
2 | # Overview
3 |
4 | The idea is that you can automatically create a customizable table of contents from your markdown text.
5 |
6 | PRs/Issues are always welcome.
7 |
8 | # Installation
9 |
10 | ## Install the package with npm
11 |
12 | Run the command below.
13 |
14 | \`\`\`bash
15 | npm install --save react-toc
16 | \`\`\`
17 |
18 | ## Install the package with yarn
19 |
20 | Run the command below.
21 |
22 | \`\`\`bash
23 | yarn add react-toc
24 | \`\`\`
25 |
26 | # Usage
27 |
28 | Import Toc from the package and pass props to it. As for now, \`markdownText\` is the only required prop.
29 |
30 | \`\`\`jsx
31 | import React from "react";
32 | import Toc from "react-toc";
33 |
34 | const Example = () => {
35 | const yourMarkdownText = '# test your markdown Content # test2'
36 | return
37 | };
38 |
39 | export default Example;
40 | \`\`\`
41 |
42 |
43 | # Props
44 |
45 | | Name | Type | Description |
46 | | ------------------ | ----------------- | ----------------------------------------------------------------------------- |
47 | | \`markdownText\` | string | **Required** The markdown text you want to creat a TOC from. |
48 | | \`titleLimit\` | number | The maximum length of each title in the TOC. |
49 | | \`lowestHeadingLevel\` | number | The lowest level of headings you want to extract from the given markdownText. |
50 | | \`className\` | string | Your custom className. |
51 | | \`type\` | "deafult" or"raw" | The type of a TOC you want to use. |
52 | | \`customMatchers\` | { [key: string]: string } | The matchers you want to use to replace the letters with. |
53 |
54 | # CustomDesign
55 |
56 | ## Add a custom className
57 |
58 | Pass \`className\` like the code below.
59 |
60 | \`\`\`jsx
61 | import React from "react";
62 | import Toc from "react-toc";
63 |
64 | const Example = () => {
65 | const yourMarkdownText = '# test your markdown Content # test2'
66 | return
67 | };
68 |
69 | export default Example;
70 | \`\`\`
71 |
72 | ## Style the custom class
73 |
74 | Now you can style your custom class just like the code below.
75 |
76 | \`\`\`css
77 | .customClassName {
78 | border: solid 1px;
79 | }
80 | .customClassName > li {
81 | padding-bottom: 10px;
82 | }
83 | \`\`\`
84 |
85 | # Custom Matchers
86 |
87 | You can use the \`customMatchers\` prop to replace letters in your toc.
88 | For instance, if you want to replace \`?\` or \`!\` with \`-\` in your list, you can simply do this.
89 |
90 | \`\`\`jsx
91 | import React from "react";
92 | import Toc from "react-toc";
93 |
94 | const Example = () => {
95 | const yourMarkdownText = "# test your markdown Content # test2";
96 | const matchers = { "[?!]": "-" }
97 |
98 | return ;
99 | };
100 |
101 | export default Example;
102 | \`\`\`
103 |
104 | You can also give more options to the \`customMatchers\` prop like the code below.
105 |
106 | \`\`\`jsx
107 | import React from "react";
108 | import Toc from "react-toc";
109 |
110 | const Example = () => {
111 | const yourMarkdownText = "# test your markdown Content # test2";
112 | const matchers = { "[?!]": "-", "\\*": "" }
113 |
114 | return ;
115 | };
116 |
117 | export default Example;
118 | \`\`\`
119 | `
120 |
121 | export { CONTENT }
122 |
--------------------------------------------------------------------------------
/demo/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: Helvetica,Arial,sans-serif;
4 | -webkit-font-smoothing: antialiased;
5 | background-color: #F6F9FC;
6 | -moz-osx-font-smoothing: grayscale;
7 | line-height: 1.25;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/demo/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import './index.css';
4 | import App from './App';
5 | import reportWebVitals from './reportWebVitals';
6 |
7 | const root = ReactDOM.createRoot(document.getElementById('root'));
8 | root.render(
9 |
10 |
11 |
12 | );
13 |
14 | // If you want to start measuring performance in your app, pass a function
15 | // to log results (for example: reportWebVitals(console.log))
16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
17 | reportWebVitals();
18 |
--------------------------------------------------------------------------------
/demo/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/demo/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/demo/src/styles/Bottom.css:
--------------------------------------------------------------------------------
1 | .bottom-container {
2 | height: 300px;
3 | /* border-top: solid 1px #E5E7EB; */
4 | display: flex;
5 | justify-content: center;
6 | align-items: center;
7 | background-color: #fff;
8 | }
9 |
10 | .link {
11 | color: #565656;
12 | text-decoration: none;
13 | font-size: 1.5rem;
14 | }
15 |
16 | .link:hover {
17 | color: #7B74FF;
18 | }
--------------------------------------------------------------------------------
/demo/src/styles/Content.css:
--------------------------------------------------------------------------------
1 | .content-container {
2 | max-width: 860px;
3 | margin: 0 auto;
4 | padding: 2rem;
5 | margin-top: 2rem;
6 | margin-bottom: 2rem;
7 | background-color: #fff;
8 | /* border: solid 1px #E5E7EB; */
9 | border-radius: 5px;
10 | }
11 |
12 | .content-container h1 {
13 | border-bottom: solid 1px #E5E7EB;
14 | padding-bottom: 0.3rem;
15 | }
16 |
17 | .content-container h2 {
18 | color: #3C4257;
19 | }
20 |
21 | table {
22 | border: 1px solid #ccc;
23 | border-collapse: collapse;
24 | margin:0;
25 | padding:0;
26 | width: 100%;
27 | }
28 |
29 | table tr {
30 | border: 1px solid #ddd;
31 | padding: 5px;
32 | }
33 |
34 | table th, table td {
35 | padding: 10px;
36 | text-align: center;
37 | }
38 |
39 | table th {
40 | font-size: 14px;
41 | letter-spacing: 1px;
42 | text-transform: uppercase;
43 | }
44 |
45 |
46 | .toc {
47 | padding-left: 1.5rem;
48 | }
49 |
50 | .toc > li,
51 | .toc > ul > li,
52 | .toc > ul > ul > li,
53 | .toc > ul > ul > ul > li,
54 | .toc > ul > ul > ul > ul > li,
55 | .toc > ul > ul > ul > ul > ul > li {
56 | padding-bottom: 10px;
57 | }
58 |
59 | .toc > li > a,
60 | .toc > ul > li > a,
61 | .toc > ul > ul > li > a,
62 | .toc > ul > ul > ul > li > a,
63 | .toc > ul > ul > ul > ul > li > a,
64 | .toc > ul > ul > ul > ul > ul > li > a {
65 | color: #565656;
66 | text-decoration: none;
67 | }
68 |
69 | .toc > li > a:hover,
70 | .toc > ul > li > a:hover,
71 | .toc > ul > ul > li > a:hover,
72 | .toc > ul > ul > ul > li > a:hover,
73 | .toc > ul > ul > ul > ul > li > a:hover,
74 | .toc > ul > ul > ul > ul > ul > li > a:hover {
75 | color: #7B74FF;
76 | }
77 |
78 | @media screen and (max-width: 600px) {
79 | table {
80 | border: 0;
81 | }
82 | table thead {
83 | display: none;
84 | }
85 | table tr {
86 | border-bottom: 2px solid #ddd;
87 | display: block;
88 | margin-bottom: 10px;
89 | }
90 | table td {
91 | border-bottom: 1px dotted #ccc;
92 | display: block;
93 | font-size: 13px;
94 | text-align: left;
95 | }
96 | table td:last-child {
97 | border-bottom: 0;
98 | }
99 | table td:before {
100 | content: attr(data-label);
101 | float: left;
102 | font-weight: bold;
103 | text-transform: uppercase;
104 | }
105 | }
106 |
107 | code {
108 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
109 | monospace;
110 | background-color: #1E1E1E;
111 | color: #f8f8f2;
112 | border-radius: 0.3em;
113 | padding: 0 4px;
114 | white-space: nowrap;
115 | }
--------------------------------------------------------------------------------
/demo/src/styles/Top.css:
--------------------------------------------------------------------------------
1 | .top-container {
2 | height: 500px;
3 | background: linear-gradient(to bottom right,rgba(123,116,255,1) 00%, rgba(127,233,255,1) 50%, #fff 50%);
4 | display: flex;
5 | justify-content: center;
6 | align-items: center;
7 | /* border-bottom: solid 1px #E5E7EB; */
8 | }
9 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | transform: {
3 | "^.+\\.tsx?$": "ts-jest",
4 | ".+\\.(css|styl|less|sass|scss)$": "jest-css-modules-transform",
5 | },
6 | testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
7 | testPathIgnorePatterns: ["/lib/", "/node_modules/", "/dist/", "/demo/"],
8 | moduleFileExtensions: ["ts", "tsx", "js", "jsx"],
9 | testEnvironmentOptions: {
10 | url: "http://localhost/"
11 | },
12 | coverageThreshold: {
13 | global: {
14 | branches: 50,
15 | functions: 80,
16 | lines: 80,
17 | statements: 80,
18 | },
19 | },
20 | coverageReporters: ["json", "lcovonly", "text", "clover"],
21 | bail: true,
22 | collectCoverage: true,
23 | };
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-toc",
3 | "version": "3.1.0",
4 | "description": "Create a table of contents from the given markdown text.",
5 | "author": "K-Sato1995",
6 | "license": "MIT",
7 | "repository": "K-Sato1995/react-toc",
8 | "main": "dist/index.js",
9 | "module": "dist/index.es.js",
10 | "jsnext:main": "dist/index.es.js",
11 | "engines": {
12 | "node": ">=8",
13 | "npm": ">=5"
14 | },
15 | "scripts": {
16 | "test": "./node_modules/.bin/jest --config ./jest.config.js",
17 | "lint": "eslint --ext=ts,tsx src",
18 | "lint:fix": "eslint --fix --ext .ts,.tsx src",
19 | "build": "rollup -c",
20 | "start": "rollup -c -w",
21 | "prepare": "yarn run build",
22 | "predeploy": "cd example && yarn install && yarn run build",
23 | "deploy": "gh-pages -d example/build"
24 | },
25 | "peerDependencies": {
26 | "react": ">=16",
27 | "react-dom": ">=16"
28 | },
29 | "devDependencies": {
30 | "@svgr/rollup": "^6.2.1",
31 | "@types/jest": "^29.1.2",
32 | "@types/react": "^18.0.11",
33 | "@types/react-dom": "^18.0.5",
34 | "@types/react-test-renderer": "^18.0.0",
35 | "@typescript-eslint/eslint-plugin": "^5.27.0",
36 | "@typescript-eslint/parser": "^5.27.0",
37 | "babel-runtime": "^6.26.0",
38 | "eslint": "^8.17.0",
39 | "eslint-plugin-jest": "^27.1.1",
40 | "eslint-plugin-react": "^7.30.0",
41 | "gh-pages": "^4.0.0",
42 | "jest": "^29.1.2",
43 | "jest-css-modules-transform": "^4.4.2",
44 | "react": "^18.1.0",
45 | "react-dom": "^18.1.0",
46 | "react-test-renderer": "^18.1.0",
47 | "rollup": "^2.75.5",
48 | "rollup-plugin-babel": "^4.4.0",
49 | "rollup-plugin-commonjs": "^10.1.0",
50 | "rollup-plugin-node-resolve": "^5.2.0",
51 | "rollup-plugin-peer-deps-external": "^2.2.0",
52 | "rollup-plugin-postcss-modules": "^2.0.2",
53 | "rollup-plugin-typescript2": "^0.32.0",
54 | "rollup-plugin-url": "^3.0.1",
55 | "ts-jest": "^29.0.3",
56 | "typescript": "4.8.4"
57 | },
58 | "files": [
59 | "dist"
60 | ]
61 | }
62 |
--------------------------------------------------------------------------------
/release/README.md:
--------------------------------------------------------------------------------
1 | ## Release flow
2 |
3 | 1. Merget the changes to master
4 | 2. Bump version in package.json
5 | 3. Create a new tag locally and push it to Github Repo
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import typescript from 'rollup-plugin-typescript2'
2 | import commonjs from 'rollup-plugin-commonjs'
3 | import external from 'rollup-plugin-peer-deps-external'
4 | import postcss from 'rollup-plugin-postcss-modules'
5 | import resolve from 'rollup-plugin-node-resolve'
6 | import url from 'rollup-plugin-url'
7 | import svgr from '@svgr/rollup'
8 |
9 | import pkg from './package.json'
10 |
11 | export default {
12 | input: 'src/index.tsx',
13 | output: [
14 | {
15 | file: pkg.main,
16 | format: 'cjs',
17 | exports: 'named',
18 | sourcemap: true
19 | },
20 | {
21 | file: pkg.module,
22 | format: 'es',
23 | exports: 'named',
24 | sourcemap: true
25 | }
26 | ],
27 | plugins: [
28 | external(),
29 | postcss({
30 | modules: true
31 | }),
32 | url(),
33 | svgr(),
34 | resolve(),
35 | typescript({
36 | rollupCommonJSResolveHack: true,
37 | clean: true
38 | }),
39 | commonjs()
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/src/Heading.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { replaceAll, createLink, createTitle } from './utils'
3 |
4 | export default class Heading {
5 | title: string
6 | level: number
7 | titleLimit: number
8 | customMatchers: CustomMatchers
9 |
10 | constructor(
11 | title: string,
12 | level: number,
13 | titleLimit: number,
14 | customMatchers?: CustomMatchers,
15 | ) {
16 | this.title = title
17 | this.level = level
18 | this.titleLimit = titleLimit
19 | this.customMatchers = customMatchers ? customMatchers : {}
20 | }
21 |
22 | generateList(): JSX.Element {
23 | const link = createLink(this.title)
24 | const listItem = (
25 |
26 |
27 | {createTitle(this.title, this.titleLimit)}
28 |
29 |
30 | )
31 |
32 | return <>{nestUl(this.level, listItem)}>
33 | }
34 | }
35 |
36 | /*
37 | Create a new heading object from the given string
38 | */
39 | const newHeading = (
40 | headingText: string,
41 | titleLimit: number,
42 | customMatchers?: CustomMatchers,
43 | ): Heading | null => {
44 | const matchedHashes = headingText.match(/^#+/)
45 | if (!matchedHashes) return null
46 | const headingLevel: number = matchedHashes[0].split('').length
47 | const matchers = customMatchers ? customMatchers : {}
48 |
49 | return new Heading(headingText, headingLevel, titleLimit, matchers)
50 | }
51 |
52 | /*
53 | Return a nested Unordered list based on the given heading level.
54 | */
55 | const nestUl = (level: number, listItem: React.ReactNode) => {
56 | switch (level) {
57 | case 1:
58 | return listItem
59 | case 2:
60 | return
61 | case 3:
62 | return (
63 |
66 | )
67 | case 4:
68 | return (
69 |
74 | )
75 | case 5:
76 | return (
77 |
84 | )
85 | case 6:
86 | return (
87 |
96 | )
97 | default:
98 | return listItem
99 | }
100 | }
101 |
102 | export { newHeading }
103 |
--------------------------------------------------------------------------------
/src/__tests__/Heading.test.tsx:
--------------------------------------------------------------------------------
1 | import Heading, { newHeading } from '../Heading'
2 | import renderer from 'react-test-renderer'
3 |
4 | describe('Instantiate Heading', () => {
5 | const createdObject = new Heading('## Test Heading?! ', 5, 50, {
6 | '[?!]': '-',
7 | })
8 | it('Create a new heading object from the given string', () => {
9 | expect(createdObject).toEqual({
10 | level: 5,
11 | title: '## Test Heading?! ',
12 | titleLimit: 50,
13 | customMatchers: { '[?!]': '-' },
14 | })
15 | })
16 | })
17 |
18 | describe('newHeading', () => {
19 | const createdObject = newHeading('## Test Heading?! ', 50, { '[?!]': '-' })
20 | it('Create a new heading object.', () => {
21 | expect(createdObject instanceof Heading).toBeTruthy()
22 | })
23 |
24 | it('Create a new heading object from the given string', () => {
25 | expect(createdObject).toEqual({
26 | level: 2,
27 | title: '## Test Heading?! ',
28 | titleLimit: 50,
29 | customMatchers: { '[?!]': '-' },
30 | })
31 | })
32 |
33 | it('returns null when there is no # in the given string', () => {
34 | expect(newHeading('Test Heading ', 50)).toBeNull()
35 | })
36 | })
37 |
38 | describe('generateList', () => {
39 | it('renders in with a heading1', () => {
40 | const headingObj = new Heading('# this is the title', 1, 100)
41 | const component = renderer.create(headingObj.generateList())
42 | const tree = component.toJSON()
43 |
44 | expect(tree).toMatchSnapshot()
45 | })
46 |
47 | it('renders in with a heading2', () => {
48 | const headingObj = new Heading('## this is the title', 2, 100)
49 | const component = renderer.create(headingObj.generateList())
50 | const tree = component.toJSON()
51 |
52 | expect(tree).toMatchSnapshot()
53 | })
54 |
55 | it('renders in with a heading3', () => {
56 | const headingObj = new Heading('### this is the title', 3, 100)
57 | const component = renderer.create(headingObj.generateList())
58 | const tree = component.toJSON()
59 |
60 | expect(tree).toMatchSnapshot()
61 | })
62 |
63 | it('renders in with a heading4', () => {
64 | const headingObj = new Heading('#### this is the title', 4, 100)
65 | const component = renderer.create(headingObj.generateList())
66 | const tree = component.toJSON()
67 |
68 | expect(tree).toMatchSnapshot()
69 | })
70 |
71 | it('renders in with a heading5', () => {
72 | const headingObj = new Heading('##### this is the title', 5, 100)
73 | const component = renderer.create(headingObj.generateList())
74 | const tree = component.toJSON()
75 |
76 | expect(tree).toMatchSnapshot()
77 | })
78 |
79 | it('renders in with a heading6', () => {
80 | const headingObj = new Heading('###### this is the title', 6, 100)
81 | const component = renderer.create(headingObj.generateList())
82 | const tree = component.toJSON()
83 |
84 | expect(tree).toMatchSnapshot()
85 | })
86 |
87 | it("renders the listItem when the given heading's level is over 6", () => {
88 | const headingObj = new Heading('####### this is the title', 7, 100)
89 | const component = renderer.create(headingObj.generateList())
90 | const tree = component.toJSON()
91 |
92 | expect(tree).toMatchSnapshot()
93 | })
94 | })
95 |
--------------------------------------------------------------------------------
/src/__tests__/__snapshots__/Heading.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`generateList renders in with a heading1 1`] = `
4 |
5 |
8 | this is the title
9 |
10 |
11 | `;
12 |
13 | exports[`generateList renders in with a heading2 1`] = `
14 |
23 | `;
24 |
25 | exports[`generateList renders in with a heading3 1`] = `
26 |
37 | `;
38 |
39 | exports[`generateList renders in with a heading4 1`] = `
40 |
53 | `;
54 |
55 | exports[`generateList renders in with a heading5 1`] = `
56 |
71 | `;
72 |
73 | exports[`generateList renders in with a heading6 1`] = `
74 |
91 | `;
92 |
93 | exports[`generateList renders the listItem when the given heading's level is over 6 1`] = `
94 |
95 |
98 | this is the title
99 |
100 |
101 | `;
102 |
--------------------------------------------------------------------------------
/src/__tests__/__snapshots__/index.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` renders properly 1`] = `
4 |
38 | `;
39 |
40 | exports[` renders properly with className option 1`] = `
41 |
72 | `;
73 |
74 | exports[` renders properly with customMatchers option 1`] = `
75 |
109 | `;
110 |
111 | exports[` renders properly with lowestHeadingLevel option 1`] = `
112 |
143 | `;
144 |
145 | exports[` renders properly with titleLimit option 1`] = `
146 |
177 | `;
178 |
179 | exports[` renders properly with type option 1`] = `
180 |
211 | `;
212 |
--------------------------------------------------------------------------------
/src/__tests__/index.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import Toc from '../index'
3 | import renderer from 'react-test-renderer'
4 |
5 | const markdownText = `
6 | # Heading1!?
7 | This is a test sentence.
8 |
9 | ## Heading2!
10 | This is a test sentence.
11 |
12 | ### Heading3?
13 | This is a test sentence.
14 | `
15 |
16 | describe('', () => {
17 | it('returns null if the markdownText dose not exist', () => {
18 | const component = renderer.create()
19 | const tree = component.toJSON()
20 |
21 | expect(tree).toBe(null)
22 | })
23 |
24 | it('renders properly', () => {
25 | const component = renderer.create()
26 | const tree = component.toJSON()
27 |
28 | expect(tree).toMatchSnapshot()
29 | })
30 |
31 | it('renders properly with titleLimit option', () => {
32 | const component = renderer.create(
33 | ,
34 | )
35 | const tree = component.toJSON()
36 |
37 | expect(tree).toMatchSnapshot()
38 | })
39 |
40 | it('renders properly with lowestHeadingLevel option', () => {
41 | const component = renderer.create(
42 | ,
43 | )
44 | const tree = component.toJSON()
45 |
46 | expect(tree).toMatchSnapshot()
47 | })
48 |
49 | it('renders properly with className option', () => {
50 | const component = renderer.create(
51 | ,
57 | )
58 | const tree = component.toJSON()
59 |
60 | expect(tree).toMatchSnapshot()
61 | })
62 |
63 | it('renders properly with type option', () => {
64 | const component = renderer.create(
65 | ,
72 | )
73 | const tree = component.toJSON()
74 |
75 | expect(tree).toMatchSnapshot()
76 | })
77 |
78 | it('renders properly with customMatchers option', () => {
79 | const customMatchers = {
80 | '[?!]': '-',
81 | }
82 | const component = renderer.create(
83 | ,
90 | )
91 | const tree = component.toJSON()
92 |
93 | expect(tree).toMatchSnapshot()
94 | })
95 | })
96 |
--------------------------------------------------------------------------------
/src/__tests__/utils.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | replaceAll,
3 | createLink,
4 | createTitle,
5 | extractHeadingsFromMd,
6 | removeCodeBlockFromMd,
7 | } from '../utils'
8 |
9 | describe('replaceAll', () => {
10 | it("won't replace any string if the given macher is {}", () => {
11 | expect(replaceAll('Test String', {})).toEqual('Test String')
12 | })
13 | it('replaces all letters based on the given customMatchers', () => {
14 | expect(replaceAll('THIS ! IS TEST STRING!?', { '[?!]': '-' })).toEqual(
15 | 'THIS - IS TEST STRING--',
16 | )
17 | expect(
18 | replaceAll('aaaeeeuuuuooooo', { a: 'z', e: 'y', u: 'w', o: 'x' }),
19 | ).toEqual('zzzyyywwwwxxxxx')
20 |
21 | expect(replaceAll('**ui**非依存', { '\\*': '' })).toEqual('ui非依存')
22 |
23 | expect(
24 | replaceAll('(ui/web/devices/db/external-interfaces)', {
25 | '[\\(\\)]': '',
26 | '/': '',
27 | }),
28 | ).toEqual('uiwebdevicesdbexternal-interfaces')
29 | })
30 | })
31 |
32 | describe('createLink', () => {
33 | it("removes # and connects each word with '-'.", () => {
34 | expect(createLink('# Test Heading ')).toEqual('test-heading')
35 | expect(createLink('## This is a test heading')).toEqual(
36 | 'this-is-a-test-heading',
37 | )
38 | })
39 | })
40 |
41 | describe('createTitle', () => {
42 | it('removes #.', () => {
43 | expect(createTitle('# Test Heading', 100)).toEqual('Test Heading')
44 | })
45 |
46 | it('shortens the string', () => {
47 | expect(createTitle('# Test Heading', 6)).toEqual('Test H..')
48 | })
49 | })
50 |
51 | describe('extractHeadingsFromMd', () => {
52 | const markdownText = `
53 | # Heading1
54 | This is the first paragraph.
55 | ## Heading2
56 | This is the second paragraph.
57 | ### Heading3
58 | This is the third paragraph.
59 | `
60 |
61 | it('extracts 1-3 headings from the given markdownText.', () => {
62 | expect(extractHeadingsFromMd(markdownText, 1, 3)).toEqual([
63 | '# Heading1\n',
64 | '## Heading2\n',
65 | '### Heading3\n',
66 | ])
67 | })
68 |
69 | it('extracts 1-2 headings from the given markdownText.', () => {
70 | expect(extractHeadingsFromMd(markdownText, 1, 2)).toEqual([
71 | '# Heading1\n',
72 | '## Heading2\n',
73 | ])
74 | })
75 |
76 | it('extracts 2-3 headings from the given markdownText.', () => {
77 | expect(extractHeadingsFromMd(markdownText, 2, 3)).toEqual([
78 | '## Heading2\n',
79 | '### Heading3\n',
80 | ])
81 | })
82 |
83 | it('extracts headings from the given markdownText.', () => {
84 | expect(extractHeadingsFromMd(markdownText, 2, 2)).toEqual(['## Heading2\n'])
85 | })
86 | })
87 |
88 | describe('removeCodeBlockFromMd', () => {
89 | const markdownText = `
90 | # Heading1
91 | This is the first paragraph.
92 | ## Heading2
93 | This is the second paragraph.
94 | \`\`\`
95 | ### This is typical codeblock
96 | \`\`\`
97 | Text between codeblock
98 | \`\`\`\`
99 | \`\`\`
100 | ### Escaped codeblock
101 | \`\`\`
102 | \`\`\`\`
103 | Text between codeblock
104 | ~~~
105 | \`\`\`
106 | ### Escaped codeblock
107 | \`\`\`
108 | ~~~`
109 |
110 | it('removes codeblock from the given markdownText.', () => {
111 | expect(removeCodeBlockFromMd(markdownText)).toEqual(`
112 | # Heading1
113 | This is the first paragraph.
114 | ## Heading2
115 | This is the second paragraph.
116 |
117 | Text between codeblock
118 |
119 | Text between codeblock
120 | `)
121 | })
122 | })
123 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import styles from './styles.module.css'
3 | import { extractHeadingsFromMd, removeCodeBlockFromMd } from './utils'
4 | import Heading, { newHeading } from './Heading'
5 |
6 | type TocProps = {
7 | /*
8 | The markdown text you want to creat a TOC from.
9 | */
10 | markdownText: string
11 | /*
12 | The maximum length of each title in the TOC.
13 | */
14 | titleLimit?: number
15 | /*
16 | The highest level of headings you want to extract from the given markdownText.
17 | */
18 | highestHeadingLevel?: number
19 | /*
20 | The lowest level of headings you want to extract from the given markdownText.
21 | */
22 | lowestHeadingLevel?: number
23 | /*
24 | The custom className.
25 | You can style the TOC like this.
26 |
27 | ```css
28 | .customClassName {
29 | border: solid 1px;
30 | }
31 | .customClassName > li {
32 | padding-bottom: 10px;
33 | }
34 | ```
35 | */
36 | className?: string
37 | /*
38 | The type of a TOC you want to use.
39 | */
40 | type?: 'default' | 'raw' // "fixed-left" | "fixed-right" | "material" | "bootstrap"
41 | /*
42 | The custom options for the anchors
43 | */
44 | customMatchers?: CustomMatchers
45 | }
46 |
47 | const Toc = ({
48 | markdownText,
49 | titleLimit,
50 | highestHeadingLevel,
51 | lowestHeadingLevel,
52 | className,
53 | type,
54 | customMatchers,
55 | }: TocProps): JSX.Element | null => {
56 | if (!markdownText) return null
57 | // Set default values
58 | const limit = titleLimit ? titleLimit : 200
59 | const defaultClass = type === 'raw' ? '' : 'react-toc'
60 | const customClass = className || defaultClass
61 | const headingLevels: number[] = [
62 | highestHeadingLevel || 1,
63 | lowestHeadingLevel || 6,
64 | ]
65 |
66 | // Style settings
67 | const style: string | undefined = styles[customClass] || className
68 |
69 | // Mutate headings
70 | const matchedHeadings: RegExpMatchArray | null = extractHeadingsFromMd(
71 | removeCodeBlockFromMd(markdownText),
72 | headingLevels[0],
73 | headingLevels[1],
74 | )
75 | const headingObjects = matchedHeadings?.map((heading) =>
76 | newHeading(heading, limit, customMatchers),
77 | )
78 | const headingTags:
79 | | JSX.Element[]
80 | | undefined = headingObjects?.map((heading: Heading) =>
81 | heading.generateList(),
82 | )
83 |
84 | if (!headingTags) return null
85 |
86 | return (
87 |
88 | {headingTags.map((heading: JSX.Element, index: number) => (
89 | {heading}
90 | ))}
91 |
92 | )
93 | }
94 |
95 | export default Toc
96 |
--------------------------------------------------------------------------------
/src/styles.module.css:
--------------------------------------------------------------------------------
1 | /* add css styles here (optional) */
2 |
3 | .react-toc > li,
4 | .react-toc > ul > li,
5 | .react-toc > ul > ul > li,
6 | .react-toc > ul > ul > ul > li,
7 | .react-toc > ul > ul > ul > ul > li,
8 | .react-toc > ul > ul > ul > ul > ul > li {
9 | padding-bottom: 10px;
10 | }
11 |
12 | .react-toc > li > a,
13 | .react-toc > ul > li > a,
14 | .react-toc > ul > ul > li > a,
15 | .react-toc > ul > ul > ul > li > a,
16 | .react-toc > ul > ul > ul > ul > li > a,
17 | .react-toc > ul > ul > ul > ul > ul > li > a {
18 | color: #dc014e;
19 | text-decoration: none;
20 | }
21 |
22 | .react-toc > li > a:hover,
23 | .react-toc > ul > li > a:hover,
24 | .react-toc > ul > ul > li > a:hover,
25 | .react-toc > ul > ul > ul > li > a:hover,
26 | .react-toc > ul > ul > ul > ul > li > a:hover,
27 | .react-toc > ul > ul > ul > ul > ul > li > a:hover {
28 | color: #565656;
29 | }
30 |
--------------------------------------------------------------------------------
/src/typings.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Default CSS definition for typescript,
3 | * will be overridden with file-specific definitions by rollup
4 | */
5 | declare module "*.css" {
6 | const content: { [className: string]: string };
7 | export default content;
8 | }
9 |
10 | interface SvgrComponent
11 | extends React.StatelessComponent> {}
12 |
13 | declare module "*.svg" {
14 | const svgUrl: string;
15 | const svgComponent: SvgrComponent;
16 | export default svgUrl;
17 | export { svgComponent as ReactComponent };
18 | }
19 |
20 | type CustomMatchers = { [key: string]: string }
21 |
22 | interface TocProps {
23 | /*
24 | The markdown text you want to creat a TOC from.
25 | */
26 | markdownText: string
27 | /*
28 | The maximum length of each title in the TOC.
29 | */
30 | titleLimit?: number | undefined
31 | /*
32 | The highest level of headings you want to extract from the given markdownText.
33 | */
34 | highestHeadingLevel?: number
35 | /*
36 | The lowest level of headings you want to extract from the given markdownText.
37 | */
38 | lowestHeadingLevel?: number
39 | /*
40 | The custom className.
41 | You can style the TOC like this.
42 |
43 | ```css
44 | .customClassName {
45 | border: solid 1px;
46 | }
47 | .customClassName > li {
48 | padding-bottom: 10px;
49 | }
50 | ```
51 | */
52 | className?: string
53 | /*
54 | The type of a TOC you want to use.
55 | */
56 | type?: 'default' | 'raw' // "fixed-left" | "fixed-right" | "material" | "bootstrap"
57 | }
58 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | // Replaces all the specified letters.
2 | const replaceAll = (retStr: string, customMatchers: CustomMatchers): string => {
3 | for (const key in customMatchers) {
4 | retStr = retStr.replace(new RegExp(key, 'g'), customMatchers[key])
5 | }
6 | return retStr
7 | }
8 |
9 | // Removes # and connects each word with '-'.
10 | const createLink = (string: string): string => {
11 | const shapedString = string.toLowerCase().replace(/^#+\s/, '').trimRight()
12 | const anchor = shapedString.split(' ').join('-')
13 | return anchor
14 | }
15 |
16 | // It removes # from the given string. And it shortens the string if its longer than "stringLimit".
17 | const createTitle = (string: string, stringLimit: number): string => {
18 | const rawTitle = string.replace(/^#+\s/g, '')
19 |
20 | if (rawTitle.length >= stringLimit)
21 | return `${rawTitle.slice(0, stringLimit)}..`
22 |
23 | return rawTitle
24 | }
25 |
26 | // It extracts headings from the given markdownText.
27 | const extractHeadingsFromMd = (
28 | markdownText: string,
29 | highestTargetHeadings: number,
30 | lowestTargetHeadings: number,
31 | ): RegExpMatchArray | null => {
32 | const headingRegex = new RegExp(
33 | `^#{${highestTargetHeadings},${lowestTargetHeadings}}\\s.+(\\n|\\r|\\r\\n)`,
34 | 'gm',
35 | )
36 | return markdownText.match(headingRegex)
37 | }
38 |
39 | const removeCodeBlockFromMd = (markdownText: string): string => {
40 | const codeBlockRegex = new RegExp(
41 | '((````[a-z]*\n[\\s\\S]*?\n````)|(```[a-z]*\n[\\s\\S]*?\n```)|(~~~[a-z]*\n[\\s\\S]*?\n~~~))',
42 | 'gms',
43 | )
44 | return markdownText.replace(codeBlockRegex, '')
45 | }
46 |
47 | export {
48 | replaceAll,
49 | createLink,
50 | createTitle,
51 | extractHeadingsFromMd,
52 | removeCodeBlockFromMd,
53 | }
54 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "build",
4 | "module": "esnext",
5 | "target": "es5",
6 | "lib": ["es6", "dom", "es2016", "es2017"],
7 | "sourceMap": true,
8 | "allowJs": false,
9 | "jsx": "react",
10 | "declaration": true,
11 | "moduleResolution": "node",
12 | "forceConsistentCasingInFileNames": true,
13 | "noImplicitReturns": true,
14 | "noImplicitThis": true,
15 | "noImplicitAny": true,
16 | "strictNullChecks": true,
17 | "suppressImplicitAnyIndexErrors": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "esModuleInterop": true
21 | },
22 | "include": ["src"],
23 | "exclude": ["node_modules", "build", "dist", "example", "rollup.config.js"]
24 | }
25 |
--------------------------------------------------------------------------------
/tsconfig.test.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs"
5 | }
6 | }
--------------------------------------------------------------------------------