├── .github
├── README.md
├── images
│ └── demo.jpg
└── workflows
│ └── publish.yml
├── .gitignore
├── .nvmrc
├── .yarn
├── releases
│ └── yarn-4.8.1.cjs
└── versions
│ ├── 21f98b06.yml
│ ├── 4e0236a4.yml
│ ├── 738a0150.yml
│ ├── 7c8e8bbb.yml
│ ├── 8890918f.yml
│ ├── 9706cd5b.yml
│ └── aa836340.yml
├── .yarnrc.yml
├── LICENSE
├── package.json
├── packages
├── example
│ ├── package.json
│ ├── src
│ │ ├── main.ts
│ │ └── user
│ │ │ ├── Admin.ts
│ │ │ └── User.ts
│ ├── tsconfig.json
│ └── typedoc.json
└── plugin
│ ├── .eslintignore
│ ├── .eslintrc.cjs
│ ├── LICENSE
│ ├── README.md
│ ├── assets
│ ├── css
│ │ └── custom.css
│ ├── images
│ │ ├── folder-open.svg
│ │ ├── folder.svg
│ │ └── ts.svg
│ └── js
│ │ ├── HierarchyManager.ts
│ │ ├── StateManager.ts
│ │ └── custom.ts
│ ├── package.json
│ ├── src
│ ├── index.tsx
│ ├── partials
│ │ └── navigation.tsx
│ └── themes
│ │ ├── OverrideTheme.tsx
│ │ └── OverrideThemeContext.tsx
│ ├── tsconfig.json
│ ├── tsup.config.ts
│ └── webpack.config.cjs
└── yarn.lock
/.github/README.md:
--------------------------------------------------------------------------------
1 | ../packages/plugin/README.md
--------------------------------------------------------------------------------
/.github/images/demo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DiFuks/typedoc-theme-hierarchy/1c1236e5ed4e126f8fdcaab394bf106f1a4c06c8/.github/images/demo.jpg
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Build and Publish
2 |
3 | on: push
4 |
5 | jobs:
6 | build-and-publish:
7 | runs-on: ubuntu-latest
8 | defaults:
9 | run:
10 | working-directory: ./packages/plugin
11 | env:
12 | NPM_DIFUKS_TOKEN: ${{ secrets.NPM_DIFUKS_TOKEN }}
13 | steps:
14 | - uses: actions/checkout@v4
15 | with:
16 | token: ${{ secrets.PERSONAL_TOKEN }}
17 |
18 | # for yarn version check
19 | - if: github.ref != 'refs/heads/master'
20 | run: |
21 | git fetch --unshallow origin master
22 |
23 | - uses: actions/setup-node@v4
24 | with:
25 | node-version-file: .nvmrc
26 | cache: yarn
27 | cache-dependency-path: yarn.lock
28 |
29 | - name: Install modules
30 | run: yarn
31 |
32 | - name: Build plugin
33 | run: yarn build
34 |
35 | - name: Lint plugin
36 | run: yarn lint
37 |
38 | - name: Check packages versions
39 | if: github.ref != 'refs/heads/master'
40 | run: yarn version check
41 |
42 | - name: Apply versions
43 | if: github.ref == 'refs/heads/master'
44 | run: yarn version apply
45 |
46 | - name: Publish package
47 | if: github.ref == 'refs/heads/master'
48 | run: yarn npm publish --tolerate-republish
49 |
50 | - uses: stefanzweifel/git-auto-commit-action@v5
51 | if: github.ref == 'refs/heads/master'
52 | with:
53 | commit_message: |
54 | ci: Release packages
55 |
56 | [skip ci]
57 |
58 | - name: Get package version
59 | if: github.ref == 'refs/heads/master'
60 | id: package-version
61 | uses: martinbeentjes/npm-get-version-action@v1.3.1
62 | with:
63 | path: packages/plugin
64 |
65 | - uses: stefanzweifel/git-auto-commit-action@v5
66 | if: github.ref == 'refs/heads/master'
67 | with:
68 | tagging_message: v${{ steps.package-version.outputs.current-version }}
69 | commit_message: |
70 | ci: Release packages
71 |
72 | [skip ci]
73 |
74 | - name: Get pull request info
75 | if: github.ref == 'refs/heads/master'
76 | id: pull-request-info
77 | uses: actions-ecosystem/action-get-merged-pull-request@v1.0.1
78 | with:
79 | github_token: ${{ secrets.GITHUB_TOKEN }}
80 |
81 | - uses: ncipollo/release-action@v1
82 | id: create-release
83 | if: github.ref == 'refs/heads/master'
84 | with:
85 | skipIfReleaseExists: true
86 | makeLatest: true
87 | name: Release ${{ steps.package-version.outputs.current-version }}
88 | body: ${{ steps.pull-request-info.outputs.body || github.event.head_commit.message }}
89 | tag: v${{ steps.package-version.outputs.current-version }}
90 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | tsconfig.*tsbuildinfo
3 | docs
4 | .idea/
5 | .pnp.*
6 | .yarn/*
7 | node_modules
8 | !.yarn/patches
9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/sdks
12 | !.yarn/versions
13 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v22
2 |
--------------------------------------------------------------------------------
/.yarn/versions/21f98b06.yml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DiFuks/typedoc-theme-hierarchy/1c1236e5ed4e126f8fdcaab394bf106f1a4c06c8/.yarn/versions/21f98b06.yml
--------------------------------------------------------------------------------
/.yarn/versions/4e0236a4.yml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DiFuks/typedoc-theme-hierarchy/1c1236e5ed4e126f8fdcaab394bf106f1a4c06c8/.yarn/versions/4e0236a4.yml
--------------------------------------------------------------------------------
/.yarn/versions/738a0150.yml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DiFuks/typedoc-theme-hierarchy/1c1236e5ed4e126f8fdcaab394bf106f1a4c06c8/.yarn/versions/738a0150.yml
--------------------------------------------------------------------------------
/.yarn/versions/7c8e8bbb.yml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DiFuks/typedoc-theme-hierarchy/1c1236e5ed4e126f8fdcaab394bf106f1a4c06c8/.yarn/versions/7c8e8bbb.yml
--------------------------------------------------------------------------------
/.yarn/versions/8890918f.yml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DiFuks/typedoc-theme-hierarchy/1c1236e5ed4e126f8fdcaab394bf106f1a4c06c8/.yarn/versions/8890918f.yml
--------------------------------------------------------------------------------
/.yarn/versions/9706cd5b.yml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DiFuks/typedoc-theme-hierarchy/1c1236e5ed4e126f8fdcaab394bf106f1a4c06c8/.yarn/versions/9706cd5b.yml
--------------------------------------------------------------------------------
/.yarn/versions/aa836340.yml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DiFuks/typedoc-theme-hierarchy/1c1236e5ed4e126f8fdcaab394bf106f1a4c06c8/.yarn/versions/aa836340.yml
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | enableGlobalCache: true
2 |
3 | nodeLinker: pnp
4 |
5 | npmAuthToken: "${NPM_DIFUKS_TOKEN:-}"
6 |
7 | npmPublishAccess: public
8 |
9 | npmRegistryServer: "https://registry.npmjs.com/"
10 |
11 | packageExtensions:
12 | eslint-module-utils@*:
13 | dependencies:
14 | eslint-import-resolver-typescript: 3.6.1
15 | typedoc@*:
16 | peerDependencies:
17 | typedoc-theme-hierarchy: "*"
18 | debug@*:
19 | dependencies:
20 | "supports-color": "*"
21 |
22 | yarnPath: .yarn/releases/yarn-4.8.1.cjs
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Dmitriy Fuks
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 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "root",
3 | "workspaces": [
4 | "packages/*"
5 | ],
6 | "packageManager": "yarn@4.8.1"
7 | }
8 |
--------------------------------------------------------------------------------
/packages/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "scripts": {
4 | "build": "tsc",
5 | "doc": "typedoc && http-server public/docs"
6 | },
7 | "devDependencies": {
8 | "http-server": "14.1.1",
9 | "typedoc": "0.28.1",
10 | "typedoc-theme-hierarchy": "workspace:*",
11 | "typescript": "5.8.2"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/packages/example/src/main.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Main interface
3 | */
4 | export interface Main {
5 | getDate(date: any): string;
6 | }
7 |
--------------------------------------------------------------------------------
/packages/example/src/user/Admin.ts:
--------------------------------------------------------------------------------
1 | import { User } from './User';
2 |
3 | export interface Admin extends User {
4 | role: 'admin';
5 | }
6 |
--------------------------------------------------------------------------------
/packages/example/src/user/User.ts:
--------------------------------------------------------------------------------
1 | export interface User {
2 | name: string;
3 | age: number;
4 | }
5 |
--------------------------------------------------------------------------------
/packages/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "ESNext",
4 | "outDir": "./dist",
5 | "strict": false,
6 | "noEmit": false,
7 | "skipLibCheck": true,
8 | },
9 | }
10 |
--------------------------------------------------------------------------------
/packages/example/typedoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "entryPoints": ["src"],
3 | "entryPointStrategy": "expand",
4 | "out": "public/docs",
5 | "plugin": ["typedoc-theme-hierarchy"],
6 | "theme": "hierarchy",
7 | "tsconfig": "./tsconfig.json",
8 | "name": "Project name"
9 | }
10 |
--------------------------------------------------------------------------------
/packages/plugin/.eslintignore:
--------------------------------------------------------------------------------
1 | dist
--------------------------------------------------------------------------------
/packages/plugin/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | require(`@rushstack/eslint-patch/modern-module-resolution`);
2 |
3 | module.exports = {
4 | extends: [`eslint-config-fuks`],
5 | parserOptions: {
6 | project: `./tsconfig.json`,
7 | sourceType: `module`,
8 | },
9 | rules: {
10 | 'no-relative-imports/no-relative-imports': `off`,
11 | 'react/no-unknown-property': `off`,
12 | 'react/jsx-key': `off`,
13 | 'react/display-name': `off`,
14 | 'unicorn/prefer-node-protocol': `off`,
15 | 'react/function-component-definition': `off`,
16 | 'react/destructuring-assignment': `off`,
17 | 'react/button-has-type': `off`,
18 | 'react/jsx-props-no-spreading': `off`,
19 | 'no-underscore-dangle': `off`,
20 | '@typescript-eslint/no-use-before-define': `off`,
21 | 'jsx-a11y/control-has-associated-label': `off`,
22 | },
23 | overrides: [
24 | {
25 | files: [`*.json`],
26 | parserOptions: {
27 | project: false,
28 | },
29 | },
30 | ],
31 | };
32 |
--------------------------------------------------------------------------------
/packages/plugin/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Dmitriy Fuks
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 |
--------------------------------------------------------------------------------
/packages/plugin/README.md:
--------------------------------------------------------------------------------
1 | # Typedoc Theme Hierarchy
2 |
3 | Hierarchy theme for [typedoc](https://typedoc.org/)
4 |
5 | [](https://www.npmjs.com/package/typedoc-theme-hierarchy)
6 | [](https://www.npmjs.com/package/typedoc-theme-hierarchy)
7 | [](https://github.com/DiFuks/typedoc-theme-hierarchy)
8 |
9 | See [example here](https://github.com/DiFuks/typedoc-theme-hierarchy/tree/master/packages/example)
10 |
11 | The plugin supports only `expand` and `resolve` values for the `entryPointStrategy` option. Support for `packages` is planned for the future. Please create an issue if you need it.
12 |
13 | 
14 |
15 | ## Installing
16 |
17 | ```bash
18 | # For typedoc ^0.28.0
19 | npm i typedoc-theme-hierarchy@^6.0.0 -D
20 |
21 | # For typedoc ^0.26.0 || ^0.27.0
22 | npm i typedoc-theme-hierarchy@^5.0.0 -D
23 |
24 | # For typedoc ^0.24.0 || ^0.25.0
25 | npm i typedoc-theme-hierarchy@^4.0.0 -D
26 |
27 | # For typedoc ^0.23.6
28 | npm i typedoc-theme-hierarchy@^3.0.0 -D
29 |
30 | # For typedoc 0.23.5
31 | npm i typedoc-theme-hierarchy@^2.0.0 -D
32 |
33 | # For typedoc ^0.22.0 || <=0.23.4
34 | npm i typedoc-theme-hierarchy@^1.3.5 -D
35 | ```
36 |
37 | ## Usage
38 |
39 | From terminal:
40 |
41 | ```bash
42 | typedoc --entryPoints src --entryPointStrategy expand --out docs --plugin typedoc-theme-hierarchy --theme hierarchy
43 | ```
44 |
45 | From `typedoc.json`:
46 |
47 | ```json
48 | {
49 | "entryPoints": ["src"],
50 | "entryPointStrategy": "Expand",
51 | "out": "public/docs",
52 | "plugin": ["typedoc-theme-hierarchy"],
53 | "theme": "hierarchy",
54 | "tsconfig": "./tsconfig.json",
55 | "name": "Project name"
56 | }
57 | ```
58 |
--------------------------------------------------------------------------------
/packages/plugin/assets/css/custom.css:
--------------------------------------------------------------------------------
1 | .tree {
2 | margin-top: 20px;
3 | background: var(--color-panel);
4 | }
5 |
6 | .tree-config {
7 | display: flex;
8 | gap: 8px;
9 | justify-content: end;
10 | padding: 8px;
11 | }
12 |
13 | .tree-config__button {
14 | border: 0;
15 | cursor: pointer;
16 | height: 20px;
17 | padding: 0;
18 | width: 20px;
19 | display: flex;
20 | align-items: center;
21 | justify-content: center;
22 | background-color: transparent;
23 | color: var(--color-toolbar-text);
24 | opacity: 0.8;
25 | }
26 |
27 | .tree-config__button:hover {
28 | opacity: 0.9;
29 | }
30 |
31 | .tree-content {
32 | position: relative;
33 | padding: 0 20px 20px;
34 | font-size: 0.85rem;
35 | font-weight: 400;
36 | line-height: 1.5;
37 | color: var(--color-text);
38 | }
39 |
40 | .tree-content span {
41 | font-size: 13px;
42 | letter-spacing: 0.4px;
43 | }
44 |
45 | .tree-content ul {
46 | padding-left: 5px;
47 | list-style: none;
48 | margin: 0;
49 | }
50 |
51 | .tree-content ul li {
52 | position: relative;
53 | padding-top: 5px;
54 | padding-bottom: 5px;
55 | padding-left: 15px;
56 | -webkit-box-sizing: border-box;
57 | -moz-box-sizing: border-box;
58 | box-sizing: border-box;
59 | }
60 |
61 | .tree-content ul li:before {
62 | position: absolute;
63 | top: 15px;
64 | left: 0;
65 | width: 10px;
66 | height: 1px;
67 | margin: auto;
68 | content: "";
69 | background-color: #666;
70 | }
71 |
72 | .tree-content ul li:after {
73 | position: absolute;
74 | top: 0;
75 | bottom: 0;
76 | left: 0;
77 | width: 1px;
78 | height: 100%;
79 | content: "";
80 | background-color: #666;
81 | }
82 |
83 | .tree-content ul li:last-child:after {
84 | height: 15px;
85 | }
86 |
87 | .tree-content ul a {
88 | cursor: pointer;
89 | }
90 |
91 | .category:not([data-id="root"]) {
92 | display: none;
93 |
94 | }
95 |
96 | .category:not([data-id="root"])._open {
97 | display: block;
98 | }
99 |
100 | .category__title {
101 | cursor: pointer;
102 | color: var(--color-text-aside);
103 | }
104 |
105 | .category__title, .category__link {
106 | text-decoration: none;
107 | display: flex;
108 | align-items: center;
109 | flex-shrink: 0;
110 | }
111 |
112 | a.category__title:hover, a.category__link:hover {
113 | text-decoration: underline;
114 | }
115 |
116 | .category__title._open .category__folder {
117 | background: url("../images/folder-open.svg");
118 | }
119 |
120 | .category__folder {
121 | display: inline-block;
122 | height: 15px;
123 | width: 15px;
124 | background: url("../images/folder.svg");
125 | margin-right: 6px;
126 | flex-shrink: 0;
127 | }
128 |
129 | .category__link._active {
130 | color: inherit;
131 | }
132 |
133 | .category__link--ts:before {
134 | content: "";
135 | display: inline-block;
136 | width: 15px;
137 | height: 15px;
138 | margin: 0 7px 2px 0;
139 | background-image: url(../images/ts.svg);
140 | vertical-align: middle;
141 | flex-shrink: 0;
142 | }
143 |
--------------------------------------------------------------------------------
/packages/plugin/assets/images/folder-open.svg:
--------------------------------------------------------------------------------
1 |
2 |
10 |
--------------------------------------------------------------------------------
/packages/plugin/assets/images/folder.svg:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/packages/plugin/assets/images/ts.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/packages/plugin/assets/js/HierarchyManager.ts:
--------------------------------------------------------------------------------
1 | import { StateManager } from './StateManager.js';
2 |
3 | export class HierarchyManager {
4 | private readonly stateManager = new StateManager();
5 |
6 | private readonly titleSelector = `.js-category-title`;
7 |
8 | private readonly listSelector = `.js-category-list`;
9 |
10 | public init(): void {
11 | this.addListeners();
12 | this.initSaved();
13 | this.openCurrentPath();
14 | }
15 |
16 | private openPathAndSave(id: string): void {
17 | this.openPath(id);
18 |
19 | this.stateManager.addOpenedPath(id);
20 | }
21 |
22 | private openPath(id: string): void {
23 | const list = document.querySelector(`${this.listSelector}[data-id="${id}"]`);
24 |
25 | if (!list) {
26 | return;
27 | }
28 |
29 | list.classList.add(`_open`);
30 | list.parentNode?.querySelector(this.titleSelector)?.classList.add(`_open`);
31 | }
32 |
33 | private closePath(id: string): void {
34 | const list = document.querySelector(`${this.listSelector}[data-id="${id}"]`);
35 |
36 | if (!list) {
37 | return;
38 | }
39 |
40 | list.classList.remove(`_open`);
41 | list.parentNode?.querySelector(this.titleSelector)?.classList.remove(`_open`);
42 |
43 | this.stateManager.removeOpenedPath(id);
44 | }
45 |
46 | private closePathWithChildren(id: string): void {
47 | this.closePath(id);
48 |
49 | const list = document.querySelector(`${this.listSelector}[data-id="${id}"]`);
50 |
51 | if (!list) {
52 | return;
53 | }
54 |
55 | const childLists = list.querySelectorAll(this.listSelector);
56 |
57 | for (const item of childLists) {
58 | this.closePath(item.dataset.id || ``);
59 | }
60 | }
61 |
62 | private togglePath(id: string): void {
63 | const list = document.querySelector(`${this.listSelector}[data-id="${id}"]`);
64 |
65 | if (!list) {
66 | return;
67 | }
68 |
69 | if (list.classList.contains(`_open`)) {
70 | this.closePathWithChildren(id);
71 |
72 | return;
73 | }
74 |
75 | this.openPathAndSave(id);
76 | }
77 |
78 | private addListeners(): void {
79 | const items = document.querySelectorAll(`.js-category-title:not([data-id="root"])`);
80 |
81 | for (const item of items) {
82 | item.addEventListener(`click`, () => {
83 | const id = item.dataset.id || ``;
84 |
85 | this.togglePath(id);
86 | });
87 | }
88 |
89 | this.addExpandListener();
90 | this.addCollapseListener();
91 | this.addTargetListener();
92 | }
93 |
94 | private addExpandListener(): void {
95 | const expandButton = document.querySelector(`.js-tree-expand`);
96 |
97 | expandButton?.addEventListener(`click`, () => {
98 | const items = document.querySelectorAll(this.listSelector);
99 |
100 | for (const item of items) {
101 | const id = item.dataset.id || ``;
102 |
103 | this.openPathAndSave(id);
104 | }
105 | });
106 | }
107 |
108 | private addCollapseListener(): void {
109 | const collapseButton = document.querySelector(`.js-tree-collapse`);
110 |
111 | collapseButton?.addEventListener(`click`, () => {
112 | const items = document.querySelectorAll(this.listSelector);
113 |
114 | for (const item of items) {
115 | const id = item.dataset.id || ``;
116 |
117 | this.closePath(id);
118 | }
119 | });
120 | }
121 |
122 | private addTargetListener(): void {
123 | const targetButton = document.querySelector(`.js-tree-target`);
124 |
125 | targetButton?.addEventListener(`click`, () => {
126 | const targetElement = this.openCurrentPath();
127 |
128 | targetElement?.scrollIntoView();
129 | });
130 | }
131 |
132 | private initSaved(): void {
133 | const savedPaths = this.stateManager.getOpenedPaths();
134 |
135 | for (const id of savedPaths) {
136 | this.openPath(id);
137 | }
138 | }
139 |
140 | private openCurrentPath(): Element | null {
141 | const pathnameSplit = window.location.pathname.split(`/`);
142 | const pathname = `/${pathnameSplit[pathnameSplit.length - 2] || ``}/${
143 | pathnameSplit[pathnameSplit.length - 1] || ``
144 | }`;
145 |
146 | const activeElement = document.querySelector(`.js-category-link[data-id="${pathname}"]`);
147 |
148 | if (!activeElement) {
149 | return null;
150 | }
151 |
152 | activeElement.classList.add(`_active`);
153 |
154 | let parent = activeElement.closest(this.listSelector);
155 |
156 | // eslint-disable-next-line no-constant-condition,@typescript-eslint/no-unnecessary-condition
157 | while (true) {
158 | if (!parent) {
159 | break;
160 | }
161 |
162 | const id = parent.dataset.id || ``;
163 |
164 | this.openPath(id);
165 |
166 | parent = (parent.parentNode as HTMLElement).closest(this.listSelector);
167 | }
168 |
169 | return activeElement;
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/packages/plugin/assets/js/StateManager.ts:
--------------------------------------------------------------------------------
1 | export class StateManager {
2 | private readonly openedPathLsKey = `opened-path-state`;
3 |
4 | private openedPaths: string[] = [];
5 |
6 | public constructor() {
7 | const lsOpenedPathState = localStorage.getItem(`opened-path-state`);
8 |
9 | this.openedPaths = lsOpenedPathState ? (JSON.parse(lsOpenedPathState) as string[]) : [];
10 | }
11 |
12 | /**
13 | * Добавляет path в стейт.
14 | */
15 | public addOpenedPath(path: string): void {
16 | this.openedPaths.push(path);
17 | this.updateState();
18 | }
19 |
20 | /**
21 | * Удаляет path из стейта.
22 | */
23 | public removeOpenedPath(path: string): void {
24 | this.openedPaths = this.openedPaths.filter(savedPath => savedPath !== path);
25 | this.updateState();
26 | }
27 |
28 | /**
29 | * Получает все открытые paths.
30 | */
31 | public getOpenedPaths(): string[] {
32 | return this.openedPaths;
33 | }
34 |
35 | private updateState(): void {
36 | localStorage.setItem(this.openedPathLsKey, JSON.stringify(this.openedPaths));
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/packages/plugin/assets/js/custom.ts:
--------------------------------------------------------------------------------
1 | import { HierarchyManager } from './HierarchyManager.js';
2 |
3 | import '../css/custom.css';
4 |
5 | const hierarchyManager = new HierarchyManager();
6 |
7 | hierarchyManager.init();
8 |
--------------------------------------------------------------------------------
/packages/plugin/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "typedoc-theme-hierarchy",
3 | "version": "6.0.0",
4 | "license": "MIT",
5 | "type": "module",
6 | "main": "dist/index.cjs",
7 | "module": "dist/index.js",
8 | "exports": {
9 | ".": {
10 | "import": "./dist/index.js",
11 | "require": "./dist/index.cjs"
12 | }
13 | },
14 | "repository": "git@github.com:DiFuks/typedoc-theme-hierarchy.git",
15 | "homepage": "https://github.com/DiFuks/typedoc-theme-hierarchy",
16 | "description": "Hierarchy theme for typedoc",
17 | "keywords": [
18 | "typedoc-theme",
19 | "typedocplugin"
20 | ],
21 | "files": [
22 | "dist",
23 | "LICENSE",
24 | "README.md"
25 | ],
26 | "author": {
27 | "name": "Dmitry Fuks",
28 | "email": "difuks@gmail.com",
29 | "url": "https://github.com/DiFuks"
30 | },
31 | "devDependencies": {
32 | "@rushstack/eslint-patch": "^1.1.4",
33 | "@types/fs-extra": "^9.0.13",
34 | "@types/mini-css-extract-plugin": "^2.4.0",
35 | "@types/webpack": "^5.28.0",
36 | "clean-webpack-plugin": "^4.0.0",
37 | "css-loader": "^6.5.1",
38 | "css-minimizer-webpack-plugin": "^3.3.1",
39 | "eslint": "^8.18.0",
40 | "eslint-config-fuks": "2.0.0",
41 | "mini-css-extract-plugin": "^2.4.5",
42 | "terser-webpack-plugin": "^5.3.3",
43 | "ts-loader": "^9.4.4",
44 | "tsup": "^8.4.0",
45 | "typedoc": "0.28.1",
46 | "typescript": "5.8.2",
47 | "webpack": "5.92.1",
48 | "webpack-cli": "5.1.4"
49 | },
50 | "peerDependencies": {
51 | "typedoc": "^0.28.0"
52 | },
53 | "scripts": {
54 | "build": "webpack && tsup",
55 | "lint": "eslint \"./**/*.{ts,tsx,js}\""
56 | },
57 | "dependencies": {
58 | "fs-extra": "11.1.1"
59 | },
60 | "packageManager": "yarn@3.6.1"
61 | }
62 |
--------------------------------------------------------------------------------
/packages/plugin/src/index.tsx:
--------------------------------------------------------------------------------
1 | import { type Application, JSX } from 'typedoc';
2 |
3 | import { OverrideTheme } from './themes/OverrideTheme.js';
4 |
5 | /**
6 | * Инициализирует плагин с темой.
7 | */
8 | export const load = (app: Application): void => {
9 | app.renderer.hooks.on(
10 | `head.end`,
11 | (context): JSX.Element => ,
12 | );
13 |
14 | app.renderer.hooks.on(
15 | `body.end`,
16 | (context): JSX.Element => ,
17 | );
18 |
19 | app.renderer.defineTheme(`hierarchy`, OverrideTheme);
20 | };
21 |
--------------------------------------------------------------------------------
/packages/plugin/src/partials/navigation.tsx:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import * as process from 'process';
3 | import {
4 | type DeclarationReflection,
5 | type DefaultThemeRenderContext,
6 | JSX,
7 | type PageEvent,
8 | type Reflection,
9 | ReflectionKind,
10 | } from 'typedoc';
11 |
12 | import { type OverrideThemeContext } from '../themes/OverrideThemeContext.js';
13 |
14 | interface IDeclarationItem {
15 | title: string;
16 | children: DeclarationReflection[];
17 | url?: string;
18 | }
19 |
20 | declare module 'typedoc' {
21 | export interface DeclarationReflection {
22 | title?: string;
23 | }
24 | }
25 |
26 | type IItem = IDeclarationItem | DeclarationReflection;
27 |
28 | interface ICategory {
29 | id: string;
30 | items: IItem[];
31 | categories: Record;
32 | }
33 |
34 | export const navigation =
35 | (context: OverrideThemeContext) =>
36 | (props: PageEvent): JSX.Element => {
37 | const categories = formatFileHierarchy(props.model.project.children || []);
38 |
39 | return (
40 |
41 |
42 |
54 |
65 |
76 |
77 |
78 |
79 |
80 |
81 | );
82 | };
83 |
84 | const Navigation = ({
85 | id,
86 | categories,
87 | items,
88 | context,
89 | }: ICategory & {
90 | context: DefaultThemeRenderContext;
91 | }): JSX.Element => (
92 |
93 | {Object.entries(categories).map(([key, item]) => (
94 | -
95 |
96 |
97 | {key}
98 |
99 |
100 |
101 | ))}
102 | {items.map(item => (
103 | -
104 |
105 |
106 | ))}
107 |
108 | );
109 |
110 | const Item = ({ item, context }: { item: IItem; context: DefaultThemeRenderContext }): JSX.Element => {
111 | if (`id` in item) {
112 | return (
113 | <>
114 |
119 | {item.title}
120 |
121 |
135 | >
136 | );
137 | }
138 |
139 | return (
140 | <>
141 | {item.title}
142 |
156 | >
157 | );
158 | };
159 |
160 | const getName = (item: DeclarationReflection): string => {
161 | const fullFileName = item.sources?.[0]?.fullFileName || ``;
162 | const targetFileName = fullFileName.replaceAll(path.sep, `/`);
163 | const currentDirName = process.cwd().replaceAll(path.sep, `/`);
164 |
165 | return targetFileName.replace(currentDirName, ``).slice(1);
166 | };
167 |
168 | const formatFileHierarchy = (values: DeclarationReflection[]): ICategory => {
169 | const result: ICategory = {
170 | items: [],
171 | categories: {},
172 | id: `root`,
173 | };
174 |
175 | for (const item of values) {
176 | const titleSplit = getName(item).split(`/`);
177 |
178 | addToCategory(result, item, titleSplit, 0);
179 | }
180 |
181 | return result;
182 | };
183 |
184 | const addToCategory = (category: ICategory, item: DeclarationReflection, titleSplit: string[], idx: number): void => {
185 | if (idx === titleSplit.length - 1) {
186 | // Если элементом является модуль (файл), то файлом считается он. Актуально для Expand мода
187 | if (item.kind === ReflectionKind.Module) {
188 | item.title = titleSplit[idx] || ``;
189 | item.children = item.children || [];
190 |
191 | category.items.push(item);
192 |
193 | return;
194 | }
195 |
196 | const existsFile = category.items.find(existItem => existItem.title === titleSplit[idx]);
197 |
198 | // Если элементом не является модуль, то файлом считается виртуальный файл. Страница для него будет недоступна. Актуально для одной точки входа
199 | if (!existsFile) {
200 | category.items.push({
201 | title: titleSplit[idx] ?? ``,
202 | children: [item],
203 | });
204 |
205 | return;
206 | }
207 |
208 | existsFile.children?.push(item);
209 |
210 | return;
211 | }
212 |
213 | const title = titleSplit[idx];
214 |
215 | if (!title) {
216 | return;
217 | }
218 |
219 | if (!category.categories[title]) {
220 | // eslint-disable-next-line no-param-reassign
221 | category.categories[title] = {
222 | items: [],
223 | categories: {},
224 | id: `${category.id}-${title}`,
225 | };
226 | }
227 |
228 | const categoryToAdd = category.categories[title];
229 |
230 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
231 | if (!categoryToAdd) {
232 | return;
233 | }
234 |
235 | addToCategory(categoryToAdd, item, titleSplit, idx + 1);
236 | };
237 |
--------------------------------------------------------------------------------
/packages/plugin/src/themes/OverrideTheme.tsx:
--------------------------------------------------------------------------------
1 | import fse from 'fs-extra';
2 | import path from 'path';
3 | import { DefaultTheme, type PageEvent, type Reflection, type Renderer, RendererEvent } from 'typedoc';
4 |
5 | import { OverrideThemeContext } from './OverrideThemeContext.js';
6 |
7 | export class OverrideTheme extends DefaultTheme {
8 | public constructor(renderer: Renderer) {
9 | super(renderer);
10 |
11 | this.owner.on(RendererEvent.END, event => {
12 | fse.copySync(
13 | path.join(require.resolve(`typedoc-theme-hierarchy`), `../assets`),
14 | path.join(event.outputDirectory, `assets`),
15 | );
16 | });
17 | }
18 |
19 | public override getRenderContext(page: PageEvent): OverrideThemeContext {
20 | return new OverrideThemeContext(this.router, this, page, this.application.options);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/packages/plugin/src/themes/OverrideThemeContext.tsx:
--------------------------------------------------------------------------------
1 | import { DefaultThemeRenderContext, type JSX, type PageEvent, type Reflection } from 'typedoc';
2 |
3 | import { navigation } from '../partials/navigation.js';
4 |
5 | export class OverrideThemeContext extends DefaultThemeRenderContext {
6 | override navigation = (context: PageEvent): JSX.Element => navigation(this)(context);
7 | }
8 |
--------------------------------------------------------------------------------
/packages/plugin/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "target": "ES2022",
5 | "module": "NodeNext",
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "strict": true,
9 | "noUncheckedIndexedAccess": true,
10 | "allowJs": true,
11 | "skipLibCheck": true,
12 | "moduleResolution": "nodenext",
13 | "jsx": "react",
14 | "jsxFactory": "JSX.createElement",
15 | "jsxFragmentFactory": "JSX.Fragment",
16 | "isolatedModules": false
17 | },
18 | "include": ["**/*.ts", "**/*.tsx", "**/*.js", ".eslintrc.js"]
19 | }
20 |
--------------------------------------------------------------------------------
/packages/plugin/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'tsup';
2 |
3 | // eslint-disable-next-line import/no-default-export
4 | export default defineConfig([
5 | {
6 | entry: [`src/index.tsx`],
7 | format: [`cjs`, `esm`],
8 | dts: true,
9 | external: [`typedoc-theme-hierarchy`, `typedoc`],
10 | banner: ({ format }) => {
11 | if (format === `esm`) {
12 | return {
13 | js: `import { createRequire } from 'module'; const require = createRequire(import.meta.url);`,
14 | };
15 | }
16 |
17 | return {};
18 | },
19 | },
20 | ]);
21 |
--------------------------------------------------------------------------------
/packages/plugin/webpack.config.cjs:
--------------------------------------------------------------------------------
1 | const path = require(`node:path`);
2 |
3 | const { CleanWebpackPlugin } = require(`clean-webpack-plugin`);
4 | const CssMinimizerPlugin = require(`css-minimizer-webpack-plugin`);
5 | const TerserPlugin = require(`terser-webpack-plugin`);
6 | const MiniCssExtractPlugin = require(`mini-css-extract-plugin`);
7 | /**
8 | * @typedef {import('webpack').Configuration} Configuration
9 | */
10 |
11 | /**
12 | * @type {Configuration}
13 | */
14 | const config = {
15 | mode: `production`,
16 | entry: `./assets/js/custom.ts`,
17 | output: {
18 | filename: `hierarchy-theme.js`,
19 | path: path.resolve(process.cwd(), `dist/assets`),
20 | },
21 | plugins: [
22 | new MiniCssExtractPlugin({
23 | filename: `hierarchy.css`,
24 | }),
25 | new CleanWebpackPlugin(),
26 | ],
27 | optimization: {
28 | minimize: true,
29 | minimizer: [new TerserPlugin(), new CssMinimizerPlugin()],
30 | },
31 | module: {
32 | rules: [
33 | {
34 | test: /\.tsx?$/,
35 | use: {
36 | loader: `ts-loader`,
37 | },
38 | exclude: /node_modules/,
39 | },
40 | {
41 | test: /\.css$/i,
42 | use: [MiniCssExtractPlugin.loader, `css-loader`],
43 | },
44 | ],
45 | },
46 | resolve: {
47 | extensions: [`.tsx`, `.ts`, `.js`],
48 | extensionAlias: {
49 | '.js': ['.js', '.ts'],
50 | },
51 | },
52 | };
53 |
54 | /**
55 | * Описывает webpack-конфиг.
56 | */
57 | module.exports = config;
58 |
--------------------------------------------------------------------------------