vuepress-jsdoc 5.0.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,165 +1,223 @@
1
- "use strict";
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
10
- };
11
- var __importDefault = (this && this.__importDefault) || function (mod) {
12
- return (mod && mod.__esModule) ? mod : { "default": mod };
13
- };
14
- Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.writeContentToFile = exports.parseVueFile = exports.parseFile = void 0;
16
1
  /*
17
2
  * @vuepress
18
3
  * ---
19
4
  * headline: lib/parser.ts
20
5
  * ---
21
6
  */
22
- const jsdoc_to_markdown_1 = __importDefault(require("jsdoc-to-markdown"));
23
- const mkdirp_1 = require("mkdirp");
24
- const promises_1 = __importDefault(require("node:fs/promises"));
25
- const node_path_1 = require("node:path");
26
- const compileTemplates_1 = __importDefault(require("vue-docgen-cli/lib/compileTemplates"));
27
- const docgen_1 = require("vue-docgen-cli/lib/docgen");
28
- const constants_1 = require("../constants");
29
- const comment_parser_1 = require("./comment-parser");
7
+ import jsdoc2md from 'jsdoc-to-markdown';
8
+ import fs from 'node:fs/promises';
9
+ import nodeModule, { createRequire } from 'node:module';
10
+ import path from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { StatisticType, TYPESCRIPT_EXTENSIONS, VUE_EXTENSIONS } from '../constants.js';
13
+ import { parseVuepressFileHeader } from './comment-parser.js';
14
+ const require = createRequire(import.meta.url);
15
+ const TEMPLATE_FOLDER = fileURLToPath(new URL('../../template/', import.meta.url));
30
16
  /**
31
- * Parse a typescript or javascript file
32
- * @param file {DirectoryFile}
33
- * @param srcFolder {string}
34
- * @param destFolder {string}
35
- * @param configPath {string}
36
- * @param partials {string | string[]}
37
- * @returns {object} file data
17
+ * Get the folder of the generated markdown file
18
+ * @param {DirectoryFile} file
19
+ * @param {string} srcFolder
20
+ * @param {string} destFolder
21
+ * @returns {string} destination folder
38
22
  */
39
- const parseFile = (file, srcFolder, destFolder, configPath, partials) => __awaiter(void 0, void 0, void 0, function* () {
40
- if (!file.folder)
41
- return null;
42
- const root = process.cwd();
43
- const relativePathDest = (0, node_path_1.join)(destFolder, file.folder.replace(srcFolder, ''));
44
- const folderInDest = (0, node_path_1.join)(root, relativePathDest);
45
- const folderInSrc = (0, node_path_1.join)(root, file.folder);
46
- let success = true;
47
- let empty = false;
48
- let excluded = false;
49
- let fileContent = '';
50
- // parse file
23
+ export const getDestFolder = (file, srcFolder, destFolder) => path.join(destFolder, path.relative(path.resolve(srcFolder), path.resolve(file.folder)));
24
+ /**
25
+ * Get a readable error message
26
+ * @param {unknown} error
27
+ * @returns {string}
28
+ */
29
+ const getErrorMessage = (error) => (error instanceof Error ? error.message : String(error)).split('\n').filter(Boolean).slice(0, 3).join('\n');
30
+ /**
31
+ * Remove TypeScript types from source code, so jsdoc is able to parse it.
32
+ * Types are replaced with whitespace, so all comments and line numbers stay intact.
33
+ * @param {string} code TypeScript code
34
+ * @returns {string} JavaScript code
35
+ */
36
+ export const stripTypes = (code) => {
37
+ const { stripTypeScriptTypes } = nodeModule;
38
+ if (typeof stripTypeScriptTypes !== 'function') {
39
+ throw new Error('TypeScript files need Node.js >= 22.13 or a jsdoc config (--jsDocConfigPath)');
40
+ }
41
+ // stripTypeScriptTypes is still experimental and would print a warning
42
+ const emitWarning = process.emitWarning;
43
+ process.emitWarning = () => { };
51
44
  try {
52
- let content = '';
53
- let fileName = file.name;
54
- if (fileName.endsWith('index')) {
55
- fileName = 'index';
56
- }
57
- content = yield jsdoc_to_markdown_1.default.render({
58
- 'no-cache': configPath ? true : false,
59
- files: [(0, node_path_1.join)(process.cwd(), file.folder, fileName + file.ext)],
60
- configure: configPath,
61
- partial: [
62
- (0, node_path_1.resolve)(__filename, '../../../template/header.hbs'),
63
- (0, node_path_1.resolve)(__filename, '../../../template/main.hbs'),
64
- ...partials
65
- ]
66
- });
67
- fileContent = (0, comment_parser_1.parseVuepressFileHeader)(yield promises_1.default.readFile(`${(0, node_path_1.join)(folderInSrc, fileName + file.ext)}`, 'utf-8'), file);
68
- if (content) {
69
- fileContent += content;
70
- }
71
- else {
72
- empty = true;
73
- }
45
+ return stripTypeScriptTypes(code, { mode: 'strip' });
74
46
  }
75
47
  catch (e) {
76
- success = false;
77
- excluded = true;
48
+ throw new Error(getErrorMessage(e), { cause: e });
78
49
  }
79
- return {
80
- success,
81
- file,
82
- empty,
83
- excluded,
84
- relativePathDest,
85
- relativePathSrc: file.folder,
86
- dest: folderInDest,
87
- content: fileContent
88
- };
89
- });
90
- exports.parseFile = parseFile;
50
+ finally {
51
+ process.emitWarning = emitWarning;
52
+ }
53
+ };
91
54
  /**
92
- * Parse a vue file
93
- * @param {DirectoryFile} file
94
- * @param {string} srcFolder
95
- * @param {string} destFolder
96
- * @returns {object} file data
55
+ * Load the TypeScript compiler API. The project of the user is preferred over the bundled one.
56
+ * `typescript` >= 7 has no JavaScript API anymore, so `@typescript/typescript6` is tried as well.
57
+ * The result is cached, because it is used for every TypeScript file.
58
+ * @returns {TypeScriptApi | null} TypeScript module or null if it is not installed
59
+ */
60
+ let typescript;
61
+ const loadTypeScript = () => {
62
+ if (typescript !== undefined)
63
+ return typescript;
64
+ const requires = [createRequire(path.join(process.cwd(), 'package.json')), require];
65
+ for (const req of requires) {
66
+ for (const name of ['typescript', '@typescript/typescript6']) {
67
+ try {
68
+ const ts = req(name);
69
+ if (typeof ts?.transpileModule === 'function')
70
+ return (typescript = ts);
71
+ }
72
+ catch {
73
+ // try the next one
74
+ }
75
+ }
76
+ }
77
+ return (typescript = null);
78
+ };
79
+ /**
80
+ * Transpile TypeScript with the TypeScript compiler. Comments and JSX are preserved.
81
+ * This supports syntax that type stripping can not handle: `enum`, `namespace`, parameter properties and JSX.
82
+ * @param {string} code TypeScript code
83
+ * @param {string} fileName used to detect `.tsx`
84
+ * @returns {string | null} JavaScript code or null if TypeScript is not installed
97
85
  */
98
- const parseVueFile = (file, srcFolder, destFolder) => __awaiter(void 0, void 0, void 0, function* () {
99
- if (!file.folder)
86
+ const transpileTypes = (code, fileName) => {
87
+ const ts = loadTypeScript();
88
+ if (!ts)
100
89
  return null;
101
- const root = process.cwd();
102
- const relativePathDest = (0, node_path_1.join)(destFolder, file.folder.replace(srcFolder, ''));
103
- const folderInDest = (0, node_path_1.join)(root, relativePathDest);
104
- const folderInSrc = (0, node_path_1.join)(root, file.folder);
105
- const vueDocGenCliConf = yield (0, docgen_1.extractConfig)((0, node_path_1.join)(root, file.folder));
106
- const config = Object.assign(Object.assign({}, vueDocGenCliConf), { components: file.name + file.ext });
107
- let success = true;
108
- let empty = false;
109
- let fileContent = '';
110
- try {
111
- let fileName = file.name;
112
- if (fileName.endsWith('index')) {
113
- fileName = 'index';
90
+ return ts.transpileModule(code, {
91
+ fileName,
92
+ reportDiagnostics: false,
93
+ compilerOptions: {
94
+ target: ts.ScriptTarget.ESNext,
95
+ module: ts.ModuleKind.ESNext,
96
+ jsx: ts.JsxEmit.Preserve,
97
+ removeComments: false,
98
+ verbatimModuleSyntax: false,
99
+ isolatedModules: true
114
100
  }
115
- // parse file
116
- const data = yield (0, compileTemplates_1.default)('add', (0, node_path_1.join)(config.componentsRoot, fileName + file.ext), config, fileName + file.ext);
117
- fileContent = (0, comment_parser_1.parseVuepressFileHeader)(yield promises_1.default.readFile(`${(0, node_path_1.join)(folderInSrc, fileName + file.ext)}`, 'utf-8'), file);
118
- if (data.content) {
119
- fileContent += data.content;
101
+ }).outputText;
102
+ };
103
+ /**
104
+ * Convert TypeScript to JavaScript. Type stripping of Node.js is used first, because it keeps all line numbers.
105
+ * If the file contains syntax which needs a transformation, the TypeScript compiler is used.
106
+ * @param {string} code TypeScript code
107
+ * @param {string} fileName
108
+ * @returns {string} JavaScript code
109
+ */
110
+ export const typescriptToJavaScript = (code, fileName) => {
111
+ let reason = 'JSX can not be stripped';
112
+ if (!fileName.endsWith('.tsx')) {
113
+ try {
114
+ return stripTypes(code);
120
115
  }
121
- else {
122
- empty = true;
116
+ catch (e) {
117
+ reason = getErrorMessage(e);
123
118
  }
124
119
  }
125
- catch (e) {
126
- console.log(e);
127
- success = false;
128
- }
129
- return {
130
- success,
131
- file,
132
- empty,
133
- relativePathDest,
134
- relativePathSrc: file.folder,
135
- dest: folderInDest,
136
- content: fileContent
137
- };
138
- });
139
- exports.parseVueFile = parseVueFile;
120
+ const transpiled = transpileTypes(code, fileName);
121
+ if (transpiled !== null)
122
+ return transpiled;
123
+ throw new Error(`${reason}\nInstall typescript or use a jsdoc config with babel (--jsDocConfigPath) for this file.`);
124
+ };
125
+ /**
126
+ * Escape vue interpolations (`{{ }}`) outside of code, because VuePress compiles every page as a vue template.
127
+ * @param {string} markdown
128
+ * @returns {string} escaped markdown
129
+ */
130
+ export const escapeVueInterpolation = (markdown) => markdown
131
+ // split by fenced code blocks, <code> and <pre> tags and inline code
132
+ .split(/(```[\s\S]*?```|<pre[\s\S]*?<\/pre>|<code>[\s\S]*?<\/code>|`[^`\n]*`)/)
133
+ // odd parts are code, which is kept. Inside <code> tags vue still interpolates.
134
+ .map((part, i) => i % 2 === 1 && !part.startsWith('<code>')
135
+ ? part
136
+ : part.replace(/\{\{/g, '&#123;&#123;').replace(/\}\}/g, '&#125;&#125;'))
137
+ .join('');
140
138
  /**
141
- * Write content on disk
142
- * @param {ParseReturn | null} parseData
143
- * @param {string} dest
144
- * @returns {Promise | null} null or type with some data of the saved file
139
+ * Render a javascript or typescript file with jsdoc-to-markdown
140
+ * @param {DirectoryFile} file
141
+ * @param {ParseOptions} options
142
+ * @param {string} source content of the file
143
+ * @returns {Promise<string>} markdown
145
144
  */
146
- const writeContentToFile = (parseData, dest) => __awaiter(void 0, void 0, void 0, function* () {
147
- const root = process.cwd();
148
- dest = (0, node_path_1.join)(root, dest);
149
- let type = constants_1.StatisticType.ERROR;
150
- if (parseData === null || parseData === void 0 ? void 0 : parseData.excluded) {
151
- type = constants_1.StatisticType.EXCLUDE;
145
+ const renderScript = async (file, options, source) => {
146
+ const absolutePath = path.resolve(file.path);
147
+ const jsdocOptions = {
148
+ partial: [
149
+ path.join(TEMPLATE_FOLDER, 'header.hbs'),
150
+ path.join(TEMPLATE_FOLDER, 'main.hbs'),
151
+ ...(options.partials || [])
152
+ ]
153
+ };
154
+ if (options.jsDocConfigPath) {
155
+ // the user config takes care of the file (e.g. typescript via jsdoc-babel)
156
+ jsdocOptions.files = [absolutePath];
157
+ jsdocOptions.configure = path.resolve(options.jsDocConfigPath);
158
+ jsdocOptions['no-cache'] = true;
159
+ }
160
+ else if (TYPESCRIPT_EXTENSIONS.includes(file.ext)) {
161
+ jsdocOptions.source = typescriptToJavaScript(source, file.path);
162
+ }
163
+ else {
164
+ jsdocOptions.files = [absolutePath];
152
165
  }
166
+ return jsdoc2md.render(jsdocOptions);
167
+ };
168
+ /**
169
+ * Render a vue file with vue-docgen-cli
170
+ * @param {DirectoryFile} file
171
+ * @returns {Promise<string>} markdown
172
+ */
173
+ const renderVue = async (file) => {
174
+ const { default: compileTemplates } = require('vue-docgen-cli/lib/compileTemplates');
175
+ const { extractConfig } = require('vue-docgen-cli/lib/docgen');
176
+ const fileName = path.basename(file.path);
177
+ const config = await extractConfig(path.resolve(file.folder));
178
+ const { content } = await compileTemplates('add', path.join(config.componentsRoot, fileName), config, fileName);
179
+ return content;
180
+ };
181
+ /**
182
+ * Parse a javascript, typescript or vue file and create the markdown content
183
+ * @param {DirectoryFile} file
184
+ * @param {ParseOptions} options
185
+ * @returns {Promise<ParseReturn>} file data
186
+ */
187
+ export const parseFile = async (file, options) => {
188
+ const dest = getDestFolder(file, options.srcFolder, options.destFolder);
189
+ const empty = { type: StatisticType.EMPTY, file, dest, content: '' };
153
190
  try {
154
- if (parseData === null || parseData === void 0 ? void 0 : parseData.content) {
155
- const path = `${(0, node_path_1.join)(dest, parseData.file.name)}.md`;
156
- yield (0, mkdirp_1.mkdirp)(dest);
157
- yield promises_1.default.writeFile(path, parseData.content, 'utf-8');
158
- type = (parseData === null || parseData === void 0 ? void 0 : parseData.empty) ? constants_1.StatisticType.EMPTY : constants_1.StatisticType.INCLUDE;
159
- }
160
- return Object.assign(Object.assign({}, parseData), { type });
191
+ const source = await fs.readFile(file.path, 'utf-8');
192
+ const isVue = VUE_EXTENSIONS.includes(file.ext);
193
+ const content = isVue ? await renderVue(file) : await renderScript(file, options, source);
194
+ if (!content?.trim())
195
+ return empty;
196
+ return {
197
+ type: StatisticType.INCLUDE,
198
+ file,
199
+ dest,
200
+ content: parseVuepressFileHeader(source, file) + escapeVueInterpolation(content.trim()) + '\n'
201
+ };
161
202
  }
162
- catch (_a) { }
163
- return null;
164
- });
165
- exports.writeContentToFile = writeContentToFile;
203
+ catch (e) {
204
+ const error = getErrorMessage(e);
205
+ // the file is excluded by the jsdoc config (source.excludePattern)
206
+ if (/no input files to process/i.test(error))
207
+ return empty;
208
+ return { ...empty, type: StatisticType.ERROR, error };
209
+ }
210
+ };
211
+ /**
212
+ * Write the parsed content to disk. Files without content are skipped.
213
+ * @param {ParseReturn} parseData
214
+ * @returns {Promise<string | null>} path of the written file or null
215
+ */
216
+ export const writeContentToFile = async (parseData) => {
217
+ if (parseData.type !== StatisticType.INCLUDE || !parseData.content)
218
+ return null;
219
+ const filePath = path.join(parseData.dest, `${parseData.file.name}.md`);
220
+ await fs.mkdir(parseData.dest, { recursive: true });
221
+ await fs.writeFile(filePath, parseData.content, 'utf-8');
222
+ return filePath;
223
+ };
@@ -1,26 +1,66 @@
1
- /**
2
- * Runs through the given tree structure and creates a vuepress config
3
- * @param {object} data Information to build config
4
- * @param {array} data.fileTree tree structure
5
- * @param {string} data.codeFolder ./code/ folder
6
- * @param {string} data.srcFolder ./src/ folder
7
- * @param {string} data.docsFolder ./documentation/ folder
8
- * @param {string} data.title title string
9
- * @returns {object} returns the vuepress menu structure
10
- */
11
- export declare const generateVueSidebar: ({ fileTree, codeFolder, srcFolder, docsFolder, title }: {
12
- fileTree: any;
1
+ import { FileTree } from '../interfaces.js';
2
+ export interface SidebarGroup {
3
+ text: string;
4
+ collapsible: boolean;
5
+ children: SidebarItem[];
6
+ }
7
+ export type SidebarItem = string | {
8
+ text: string;
9
+ link: string;
10
+ } | SidebarGroup;
11
+ export interface SidebarOptions {
12
+ /** tree structure, created by `listFolder` */
13
+ fileTree: FileTree[];
14
+ /** name of the folder inside the vuepress folder, e.g. `code` */
13
15
  codeFolder: string;
16
+ /** source folder, e.g. `src` */
14
17
  srcFolder: string;
18
+ /** folder of the generated markdown files, e.g. `documentation/code` */
15
19
  docsFolder: string;
20
+ /** title of the first sidebar group */
16
21
  title: string;
17
- }) => {
18
- [x: string]: {
19
- text: string;
20
- collapsable: boolean;
21
- children: {
22
- link: string;
23
- text: string;
24
- }[];
25
- }[];
22
+ /** sidebar format of the generated `config.js` */
23
+ format?: SiteFormat;
24
+ /** text of the first entry, which links to the readme */
25
+ readmeTitle?: string;
26
+ /** make the top level groups collapsible. Nested folders are always collapsible. */
27
+ collapsible?: boolean;
28
+ }
29
+ export type SiteFormat = 'vuepress' | 'vitepress';
30
+ /**
31
+ * Get the route of the generated folder, e.g. `/code/`
32
+ * @param {string} codeFolder
33
+ * @returns {string}
34
+ */
35
+ export declare const getSidebarBase: (codeFolder: string) => string;
36
+ export interface VitepressSidebarItem {
37
+ text: string;
38
+ link?: string;
39
+ collapsed?: boolean;
40
+ items?: VitepressSidebarItem[];
41
+ }
42
+ /**
43
+ * Runs through the given tree structure and creates a vuepress 2 sidebar config.
44
+ * Only pages that were generated are linked. Folders become nested sidebar groups.
45
+ * @param {SidebarOptions} data Information to build config
46
+ * @returns {object} returns the vuepress sidebar structure
47
+ */
48
+ export declare const generateVueSidebar: ({ fileTree, codeFolder, srcFolder, docsFolder, title, readmeTitle, collapsible }: SidebarOptions) => {
49
+ [x: string]: SidebarGroup[];
26
50
  };
51
+ /**
52
+ * Runs through the given tree structure and creates a vitepress sidebar config.
53
+ * Vitepress needs the text of every link, so it is read from the generated page.
54
+ * @param {SidebarOptions} data Information to build config
55
+ * @returns {object} returns the vitepress sidebar structure
56
+ */
57
+ export declare const generateVitepressSidebar: (data: SidebarOptions) => {
58
+ [x: string]: VitepressSidebarItem[];
59
+ };
60
+ /**
61
+ * Create the content of the `config.js`, which is placed into the generated folder.
62
+ * It exports the file tree and a `sidebarTree(title)` function, which returns the vuepress or vitepress sidebar.
63
+ * @param {SidebarOptions} options
64
+ * @returns {string} javascript code (ES module)
65
+ */
66
+ export declare const createSidebarConfig: (options: SidebarOptions) => string;
@@ -1,59 +1,125 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.generateVueSidebar = void 0;
7
1
  /*
8
2
  * @vuepress
9
3
  * ---
10
4
  * headline: lib/vue-sidebar.ts
11
5
  * ---
12
6
  */
13
- const node_fs_1 = __importDefault(require("node:fs"));
14
- const node_path_1 = require("node:path");
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { SIDEBAR_TITLE_PLACEHOLDER } from '../constants.js';
10
+ import { getPageName, normalizePath } from './list-folder.js';
11
+ /**
12
+ * Get the route of the generated folder, e.g. `/code/`
13
+ * @param {string} codeFolder
14
+ * @returns {string}
15
+ */
16
+ export const getSidebarBase = (codeFolder) => `/${normalizePath(codeFolder)}/`;
15
17
  /**
16
- * Runs through the given tree structure and creates a vuepress config
17
- * @param {object} data Information to build config
18
- * @param {array} data.fileTree tree structure
19
- * @param {string} data.codeFolder ./code/ folder
20
- * @param {string} data.srcFolder ./src/ folder
21
- * @param {string} data.docsFolder ./documentation/ folder
22
- * @param {string} data.title title string
23
- * @returns {object} returns the vuepress menu structure
18
+ * Runs through the given tree structure and creates a vuepress 2 sidebar config.
19
+ * Only pages that were generated are linked. Folders become nested sidebar groups.
20
+ * @param {SidebarOptions} data Information to build config
21
+ * @returns {object} returns the vuepress sidebar structure
24
22
  */
25
- const generateVueSidebar = ({ fileTree, codeFolder, srcFolder, docsFolder, title }) => {
26
- let rootFiles = [{ link: `/${codeFolder}/`, text: '::vuepress-jsdoc-title::' }];
27
- rootFiles = rootFiles.concat(fileTree.filter((file) => !file.children).map((file) => `/${(0, node_path_1.join)(codeFolder, file.name)}`));
28
- const rootFolder = fileTree.filter((file) => file.children && file.children.length > 0);
29
- const buildChildren = (children, name, depth) => {
30
- let newChildren = [];
23
+ export const generateVueSidebar = ({ fileTree, codeFolder, srcFolder, docsFolder, title, readmeTitle = SIDEBAR_TITLE_PLACEHOLDER, collapsible = false }) => {
24
+ const base = getSidebarBase(codeFolder);
25
+ const root = normalizePath(srcFolder);
26
+ const toLink = (file) => {
27
+ if (!file.fullPath)
28
+ return null;
29
+ const relativePath = path.posix.relative(root, normalizePath(file.fullPath));
30
+ if (!fs.existsSync(`${path.join(docsFolder, relativePath)}.md`))
31
+ return null;
32
+ return `${base}${relativePath}`;
33
+ };
34
+ const buildChildren = (children) => {
35
+ const items = [];
31
36
  for (const child of children) {
32
- if (child.children && child.children.length > 0) {
33
- newChildren = newChildren.concat(buildChildren(child.children, child.name, depth + 1));
34
- }
35
- else if (child.fullPath) {
36
- const path = child.fullPath.replace(srcFolder, '');
37
- if (node_fs_1.default.existsSync(`${(0, node_path_1.join)(docsFolder, path)}.md`)) {
38
- newChildren.push(`/${(0, node_path_1.join)(codeFolder, path)}`);
37
+ if (child.children) {
38
+ const nested = buildChildren(child.children);
39
+ if (nested.length > 0) {
40
+ items.push({ text: child.name, collapsible: true, children: nested });
39
41
  }
40
42
  }
43
+ else {
44
+ const link = toLink(child);
45
+ if (link)
46
+ items.push(link);
47
+ }
41
48
  }
42
- return newChildren;
49
+ return items;
43
50
  };
44
- const tree = rootFolder.map((folder) => ({
45
- text: folder.name,
46
- collapsable: false,
47
- children: buildChildren(folder.children, folder.name, 0)
48
- }));
51
+ const rootFiles = [
52
+ { text: readmeTitle, link: base },
53
+ ...buildChildren(fileTree.filter((file) => !file.children))
54
+ ];
55
+ const folders = fileTree
56
+ .filter((file) => file.children)
57
+ .map((folder) => ({ text: folder.name, collapsible, children: buildChildren(folder.children) }))
58
+ .filter((group) => group.children.length > 0);
49
59
  return {
50
- [`/${codeFolder}/`]: [
51
- {
52
- text: title,
53
- collapsable: false,
54
- children: rootFiles
55
- }
56
- ].concat(tree)
60
+ [base]: [{ text: title, collapsible, children: rootFiles }, ...folders]
61
+ };
62
+ };
63
+ /**
64
+ * Get the `title` of the frontmatter of a generated page (set with the `@vuepress` block)
65
+ * @param {string} mdPath path to the generated markdown file
66
+ * @returns {string|undefined}
67
+ */
68
+ const readPageTitle = (mdPath) => {
69
+ const title = /^---\r?\n[\s\S]*?^title:\s*(.+?)\s*$[\s\S]*?^---/m.exec(fs.readFileSync(mdPath, 'utf-8'))?.[1];
70
+ return title?.replace(/^(['"])(.*)\1$/, '$2');
71
+ };
72
+ /**
73
+ * Runs through the given tree structure and creates a vitepress sidebar config.
74
+ * Vitepress needs the text of every link, so it is read from the generated page.
75
+ * @param {SidebarOptions} data Information to build config
76
+ * @returns {object} returns the vitepress sidebar structure
77
+ */
78
+ export const generateVitepressSidebar = (data) => {
79
+ const base = getSidebarBase(data.codeFolder);
80
+ const root = normalizePath(data.srcFolder);
81
+ // the frontmatter title or the file name. Index files are shown as `index` instead of their prefixed page name.
82
+ const getText = (link) => {
83
+ const relativePath = link.slice(base.length);
84
+ const name = path.posix.basename(relativePath);
85
+ const isIndex = name === getPageName(path.posix.join(root, path.posix.dirname(relativePath)), 'index');
86
+ return readPageTitle(`${path.join(data.docsFolder, relativePath)}.md`) ?? (isIndex ? 'index' : name);
87
+ };
88
+ const toVitepressItem = (item, nested = true) => {
89
+ if (typeof item === 'string') {
90
+ return { text: getText(item), link: item };
91
+ }
92
+ if ('link' in item) {
93
+ return { text: item.text, link: item.link };
94
+ }
95
+ return {
96
+ text: item.text,
97
+ // nested folders are collapsed, vitepress opens groups with the active page.
98
+ // collapsible top level groups stay open.
99
+ ...(item.collapsible ? { collapsed: nested } : {}),
100
+ items: item.children.map((child) => toVitepressItem(child))
101
+ };
57
102
  };
103
+ return { [base]: generateVueSidebar(data)[base].map((group) => toVitepressItem(group, false)) };
104
+ };
105
+ /**
106
+ * Create the content of the `config.js`, which is placed into the generated folder.
107
+ * It exports the file tree and a `sidebarTree(title)` function, which returns the vuepress or vitepress sidebar.
108
+ * @param {SidebarOptions} options
109
+ * @returns {string} javascript code (ES module)
110
+ */
111
+ export const createSidebarConfig = (options) => {
112
+ // the placeholder is replaced with the `title` parameter of `sidebarTree`
113
+ const data = { ...options, readmeTitle: SIDEBAR_TITLE_PLACEHOLDER };
114
+ const tree = options.format === 'vitepress' ? generateVitepressSidebar(data) : generateVueSidebar(data);
115
+ const sidebar = JSON.stringify(tree, null, 2).replace(JSON.stringify(SIDEBAR_TITLE_PLACEHOLDER), 'title');
116
+ return [
117
+ '// This file is generated by vuepress-jsdoc. Do not edit it manually.',
118
+ `export const fileTree = ${JSON.stringify(options.fileTree, null, 2)};`,
119
+ '',
120
+ `export const sidebarTree = (title = 'Mainpage') => (${sidebar});`,
121
+ '',
122
+ 'export default sidebarTree;',
123
+ ''
124
+ ].join('\n');
58
125
  };
59
- exports.generateVueSidebar = generateVueSidebar;