bimplus-renderer 1.8.38 → 1.8.39
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.
- package/JSDOC_MIGRATION_PLAN.md +244 -0
- package/README.md +2 -3
- package/jsdoc.json +23 -0
- package/package.json +9 -9
- package/scripts/generate-api-markdown.js +371 -0
- package/scripts/jsdoc-class-ownership-plugin.js +107 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# JSDoc Migration Plan
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Replace the `documentation` package with JSDoc tooling while preserving the existing documentation workflows:
|
|
6
|
+
|
|
7
|
+
- HTML documentation in `./documentation`
|
|
8
|
+
- Markdown API documentation in `./documentation/Bimplus_Renderer_doc.md`
|
|
9
|
+
- Confluence-compatible Markdown in `./documentation/Bimplus_Renderer_doc_confluence.md`
|
|
10
|
+
- The existing `build-doc*` npm script interface
|
|
11
|
+
|
|
12
|
+
The migration should also remove the vulnerable transitive `vue-template-compiler` dependency.
|
|
13
|
+
|
|
14
|
+
## Scope
|
|
15
|
+
|
|
16
|
+
Files expected to change:
|
|
17
|
+
|
|
18
|
+
- `package.json`
|
|
19
|
+
- `package-lock.json`
|
|
20
|
+
- New root-level `jsdoc.json` configuration
|
|
21
|
+
- Existing `scripts/generate-api-markdown.js` renderer
|
|
22
|
+
- `eslint.config.mjs` for scoped Node.js globals when linting the renderer
|
|
23
|
+
- `README.md`
|
|
24
|
+
- `documentation/Bimplus Renderer basics.md` as the HTML landing-page source
|
|
25
|
+
- `documentation/convert_to_confluence_md.py`
|
|
26
|
+
- Remove obsolete `doc_config.yml`
|
|
27
|
+
- Generated files under `documentation/`
|
|
28
|
+
|
|
29
|
+
The following should remain functionally unchanged:
|
|
30
|
+
|
|
31
|
+
- JSDoc comments in `src/*.js`
|
|
32
|
+
- Application source, build, and test configuration
|
|
33
|
+
|
|
34
|
+
## Porting This Plan to Other Projects
|
|
35
|
+
|
|
36
|
+
Use this file as a migration template for each repository, not as a blind file-copy checklist. Before implementation, record the current source glob, generated output paths, existing `build-doc*` script names, Markdown heading/anchor conventions, and any downstream Confluence or publishing workflow. Preserve established output names and script interfaces when consumers depend on them; adapt paths and symbol grouping to that repository's JSDoc conventions.
|
|
37
|
+
|
|
38
|
+
The reference custom renderer in [scripts/generate-api-markdown.js](scripts/generate-api-markdown.js) is the implementation model. Copy it only after checking that the other repository's doclets use compatible `kind`, `memberof`, `scope`, and `longname` values. Add project-specific filters or display rules only when required by its documented public API.
|
|
39
|
+
|
|
40
|
+
## Implementation Steps
|
|
41
|
+
|
|
42
|
+
### 1. Replace development dependencies
|
|
43
|
+
|
|
44
|
+
Remove `documentation` from `devDependencies` and add:
|
|
45
|
+
|
|
46
|
+
- `jsdoc` for parsing source comments and generating HTML
|
|
47
|
+
- A repository-owned Markdown renderer using JSDoc's structured doclets
|
|
48
|
+
|
|
49
|
+
Remove any old generator dependencies and install JSDoc:
|
|
50
|
+
|
|
51
|
+
```powershell
|
|
52
|
+
npm uninstall documentation jsdoc-to-markdown
|
|
53
|
+
npm install --save-dev jsdoc
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Do not add a third-party Markdown generator. `scripts/generate-api-markdown.js` renders JSDoc doclets directly and validates symbol anchors.
|
|
57
|
+
|
|
58
|
+
Do not run `npm audit fix --force`; it may apply a breaking downgrade and does not provide a patched version of the vulnerable Vue 2 package.
|
|
59
|
+
|
|
60
|
+
### 2. Add `jsdoc.json`
|
|
61
|
+
|
|
62
|
+
Create a root-level JSDoc configuration with these requirements:
|
|
63
|
+
|
|
64
|
+
- Include JavaScript files under `src/`
|
|
65
|
+
- Recurse through the source directory
|
|
66
|
+
- Parse ES module syntax
|
|
67
|
+
- Write HTML output to `./documentation`
|
|
68
|
+
- Use `documentation/Bimplus Renderer basics.md` as the documentation landing-page content when supported
|
|
69
|
+
- Enable JSDoc's Markdown plugin and recursive source discovery
|
|
70
|
+
|
|
71
|
+
Proposed configuration:
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"source": {
|
|
76
|
+
"include": ["src"],
|
|
77
|
+
"includePattern": ".*\\.js$",
|
|
78
|
+
"excludePattern": "(^|[\\\\/])node_modules[\\\\/]"
|
|
79
|
+
},
|
|
80
|
+
"opts": {
|
|
81
|
+
"destination": "./documentation",
|
|
82
|
+
"recurse": true,
|
|
83
|
+
"readme": "./documentation/Bimplus Renderer basics.md"
|
|
84
|
+
},
|
|
85
|
+
"templates": {
|
|
86
|
+
"cleverLinks": false,
|
|
87
|
+
"monospaceLinks": true
|
|
88
|
+
},
|
|
89
|
+
"plugins": [
|
|
90
|
+
"plugins/markdown"
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`opts.recurse: true` includes deeper source directories than the previous shallow globs. Review the additional doclets and exclude non-public implementation modules as needed. Do not add `plugins/recurse`: it is not shipped by JSDoc 4 and makes the command fail with "Unable to find the plugin". Validate the configuration against the installed JSDoc version.
|
|
96
|
+
|
|
97
|
+
### 3. Update npm scripts
|
|
98
|
+
|
|
99
|
+
Replace the current `documentation` commands with JSDoc equivalents while retaining the existing script names:
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"lint-doc": "jsdoc --configure jsdoc.json --explain",
|
|
104
|
+
"build-doc": "npm run lint-doc && jsdoc --configure jsdoc.json",
|
|
105
|
+
"build-docMd": "npm run lint-doc && node scripts/generate-api-markdown.js",
|
|
106
|
+
"build-docConf": "npm run build-docMd && cd ./documentation && python convert_to_confluence_md.py",
|
|
107
|
+
"build-docAll": "npm run build-doc && npm run build-docConf"
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`scripts/generate-api-markdown.js` replaces `jsdoc2md`; do not add a third-party Markdown renderer. The renderer should:
|
|
112
|
+
|
|
113
|
+
1. Invoke the installed JSDoc CLI with `process.execPath`, the local `node_modules/jsdoc/jsdoc.js` entrypoint, `--configure jsdoc.json`, and `--explain`. Run with the repository root as `cwd`, capture stdout/stderr, and fail clearly on a non-zero exit or invalid JSON.
|
|
114
|
+
2. Parse the JSON doclets and include documented public classes, modules, namespaces, and supported API doclets only. Exclude undocumented, private, anonymous, and implementation-only symbols. Deduplicate repeated doclets by `longname`; group members by `memberof`; retain documented global functions.
|
|
115
|
+
3. Render the information present on doclets: container parameters, method signatures, descriptions, parameter tables, return values, callbacks/properties, types/defaults, deprecation/since/throws/see tags, and examples where present. Preserve the Markdown/HTML descriptions produced by JSDoc.
|
|
116
|
+
4. Emit both a useful API index and a section for each documented symbol. Escape values placed in code or Markdown tables without altering trusted JSDoc description markup.
|
|
117
|
+
5. Emit explicit `<a id="…" name="…"></a>` targets. Keep each fragment used in a Markdown link identical to an emitted ID. Normalize instance-method `#` separators to the existing `+` anchor form; preserve dots for static members/callbacks. For example, `BimExplorer#getUrl` maps to `BimExplorer+getUrl`, while `BimExplorer.this.onObjectSelected` keeps its dotted name. Handle nested names such as `BimExplorer.BimObjectProperties#load` explicitly.
|
|
118
|
+
6. Before writing the file, fail if a rendered API symbol has no anchor or if any generated `](#target)` link has no matching anchor ID. Create the destination directory if needed and write UTF-8.
|
|
119
|
+
|
|
120
|
+
The checks in the renderer protect the source Markdown structure. They do not replace rendering the Markdown and testing real click navigation in the target preview/browser.
|
|
121
|
+
|
|
122
|
+
The `lint-doc` command should be tested with the installed JSDoc CLI. Existing `@private` tags with values warn under JSDoc; do not use `--pedantic` unless those source comments are corrected. `--explain` emits a large doclet JSON report; suppress or capture that output in automated logs if it becomes unwieldy, while preserving the command's exit status.
|
|
123
|
+
|
|
124
|
+
### 4. Regenerate documentation
|
|
125
|
+
|
|
126
|
+
Run the formats individually first:
|
|
127
|
+
|
|
128
|
+
```powershell
|
|
129
|
+
npm run lint-doc
|
|
130
|
+
npm run build-doc
|
|
131
|
+
npm run build-docMd
|
|
132
|
+
npm run build-docConf
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Then run the complete workflow:
|
|
136
|
+
|
|
137
|
+
```powershell
|
|
138
|
+
npm run build-docAll
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The Confluence converter should continue reading `Bimplus_Renderer_doc.md` from `documentation/` and writing `Bimplus_Renderer_doc_confluence.md`. It must:
|
|
142
|
+
|
|
143
|
+
- Read and write UTF-8 explicitly so Unicode output works on Windows.
|
|
144
|
+
- Rewrite links case-insensitively when JSDoc preserves case in fragments.
|
|
145
|
+
- When rewriting a target to `RendererApidescription-{fragment}`, emit a matching `<a id="RendererApidescription-{fragment}" name="RendererApidescription-{fragment}"></a>` alias. Rewriting links without creating matching targets leaves the converted document's links dead.
|
|
146
|
+
- Validate every converted internal Markdown link against an emitted ID before writing the final Confluence file.
|
|
147
|
+
|
|
148
|
+
Keep the raw Markdown IDs and Confluence aliases separate: the raw file uses the API fragments; only the Confluence variant uses the required `RendererApidescription-` prefix.
|
|
149
|
+
|
|
150
|
+
### 5. Review generated output
|
|
151
|
+
|
|
152
|
+
Check the following:
|
|
153
|
+
|
|
154
|
+
- `documentation/index.html` opens successfully
|
|
155
|
+
- HTML assets load without missing-file errors
|
|
156
|
+
- All documented public classes, modules, namespaces, methods, callbacks, parameters, and return values are represented
|
|
157
|
+
- Every rendered API symbol has a generated Markdown anchor, and internal Markdown links resolve
|
|
158
|
+
- Markdown headings and links are readable
|
|
159
|
+
- Confluence anchors contain the expected `RendererApidescription-` prefix
|
|
160
|
+
- Confluence-prefixed links have matching prefixed target IDs
|
|
161
|
+
- No stale files from the previous generator remain in the generated output
|
|
162
|
+
|
|
163
|
+
Do not treat matching fragment strings in source files as proof of working links. Render both Markdown files with the parser/preview used by their consumers, inspect the resulting `href` and `id` values, and click representative links: a class/module, an instance method containing `+`, a dotted member, and a nested method. If the consumer is VS Code Markdown Preview, test there; if the Confluence Markdown is pasted into Confluence, verify links in Confluence's preview/page because its anchor generation rules differ from local Markdown renderers.
|
|
164
|
+
|
|
165
|
+
JSDoc will produce a different HTML structure and may use different anchor names than documentation.js. This is expected, but no public API should disappear.
|
|
166
|
+
|
|
167
|
+
### 6. Update repository guidance
|
|
168
|
+
|
|
169
|
+
Update `README.md` to describe:
|
|
170
|
+
|
|
171
|
+
- `jsdoc.json`
|
|
172
|
+
- `jsdoc` and the repository-owned Markdown renderer
|
|
173
|
+
- The revised npm scripts
|
|
174
|
+
- The new HTML output structure
|
|
175
|
+
- The continued Markdown-to-Confluence conversion
|
|
176
|
+
- The fact that generated output must be regenerated after source JSDoc changes
|
|
177
|
+
- The repository-owned Markdown renderer and its symbol/link validation
|
|
178
|
+
- The distinction between raw Markdown IDs and Confluence-prefixed alias IDs
|
|
179
|
+
|
|
180
|
+
### 7. Remove obsolete dependency output
|
|
181
|
+
|
|
182
|
+
After a successful install, verify that the old packages are gone:
|
|
183
|
+
|
|
184
|
+
```powershell
|
|
185
|
+
npm ls documentation jsdoc-to-markdown vue-template-compiler
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The command should report that none of these packages is installed, or that they are not found in the dependency tree. Search scripts, READMEs, and the lockfile for stale `jsdoc2md`/`jsdoc-to-markdown` references; the removal-check command in this plan may name the package intentionally.
|
|
189
|
+
|
|
190
|
+
Review `package-lock.json` for removal of the obsolete dependency chain. Do not remove unrelated lockfile changes produced by the package manager.
|
|
191
|
+
|
|
192
|
+
## Verification Checklist
|
|
193
|
+
|
|
194
|
+
Run the documentation workflow and existing project checks:
|
|
195
|
+
|
|
196
|
+
```powershell
|
|
197
|
+
npm install
|
|
198
|
+
npm run lint-doc
|
|
199
|
+
npm run build-docAll
|
|
200
|
+
npm run build
|
|
201
|
+
npm test
|
|
202
|
+
npm audit
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Also lint the renderer directly if the repository ESLint script does not include it:
|
|
206
|
+
|
|
207
|
+
```powershell
|
|
208
|
+
npx eslint scripts/generate-api-markdown.js
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Expected results:
|
|
212
|
+
|
|
213
|
+
- Documentation commands complete successfully
|
|
214
|
+
- Generated HTML, Markdown, and Confluence files exist
|
|
215
|
+
- The application build remains successful
|
|
216
|
+
- Existing tests remain successful
|
|
217
|
+
- The `vue-template-compiler` audit finding is no longer introduced by this documentation toolchain
|
|
218
|
+
- `jsdoc-to-markdown` is absent from the manifest, lockfile dependency tree, and generation scripts
|
|
219
|
+
- Rendered raw Markdown and Confluence Markdown links navigate to their intended targets in their actual consumers
|
|
220
|
+
|
|
221
|
+
`npm audit` may still report unrelated vulnerabilities. Record those separately instead of applying a forced dependency downgrade.
|
|
222
|
+
|
|
223
|
+
## Rollback
|
|
224
|
+
|
|
225
|
+
If the generated output loses API content or the Confluence conversion cannot be adapted safely:
|
|
226
|
+
|
|
227
|
+
1. Restore the previous `package.json` and `package-lock.json` documentation dependencies.
|
|
228
|
+
2. Remove `jsdoc.json` and restore `scripts/generate-api-markdown.js` to its pre-migration contents if rollback is required.
|
|
229
|
+
3. Restore the previous generated `documentation/` output.
|
|
230
|
+
4. Reassess the JSDoc configuration and renderer before retrying.
|
|
231
|
+
|
|
232
|
+
Do not modify application source code as part of rollback.
|
|
233
|
+
|
|
234
|
+
## Acceptance Criteria
|
|
235
|
+
|
|
236
|
+
The migration is complete when:
|
|
237
|
+
|
|
238
|
+
- `documentation` is no longer a development dependency
|
|
239
|
+
- JSDoc generates readable HTML from the reviewed JavaScript files under `src/`
|
|
240
|
+
- The repository-owned renderer generates the API Markdown file
|
|
241
|
+
- The existing Confluence conversion succeeds
|
|
242
|
+
- `build-doc`, `build-docMd`, `build-docConf`, and `build-docAll` all work
|
|
243
|
+
- Generated documentation is reviewed and committed
|
|
244
|
+
- Application build and tests remain unaffected
|
package/README.md
CHANGED
|
@@ -154,10 +154,9 @@ update reference data automatically. See
|
|
|
154
154
|
[the migration plan](KARMA_TO_VITEST_MIGRATION_PLAN.md) for remaining gates.
|
|
155
155
|
|
|
156
156
|
### Documentation
|
|
157
|
-
Bimplus
|
|
158
|
-
To generate a documentation please install documentation plugin via npm :
|
|
157
|
+
Bimplus Renderer documentation is generated with the local JSDoc dependency and the repository-owned API Markdown renderer. Install the project dependencies with `npm install` before running the documentation scripts.
|
|
159
158
|
|
|
160
|
-
|
|
159
|
+
JSDoc source discovery and HTML output are configured in `jsdoc.json`; `scripts/generate-api-markdown.js` renders the API doclets and checks Markdown anchors. Regenerate the documentation after changing source JSDoc comments. Raw Markdown uses API anchors, while the Confluence output adds matching `RendererApidescription-` aliases.
|
|
161
160
|
|
|
162
161
|
To create documentation in html format go to your renderer folder and run :
|
|
163
162
|
|
package/jsdoc.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"source": {
|
|
3
|
+
"include": ["src"],
|
|
4
|
+
"includePattern": ".*\\.js$",
|
|
5
|
+
"excludePattern": "(^|[\\\\/])node_modules[\\\\/]"
|
|
6
|
+
},
|
|
7
|
+
"opts": {
|
|
8
|
+
"destination": "./documentation",
|
|
9
|
+
"recurse": true,
|
|
10
|
+
"readme": "./documentation/Bimplus Renderer basics.md"
|
|
11
|
+
},
|
|
12
|
+
"templates": {
|
|
13
|
+
"cleverLinks": false,
|
|
14
|
+
"monospaceLinks": true,
|
|
15
|
+
"default": {
|
|
16
|
+
"includeDate": false
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"plugins": [
|
|
20
|
+
"./scripts/jsdoc-class-ownership-plugin.js",
|
|
21
|
+
"plugins/markdown"
|
|
22
|
+
]
|
|
23
|
+
}
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
" set there the full version string! ": "",
|
|
7
7
|
" e.g.: semverRange: '1.8.20-my-tag-name-01' ": "",
|
|
8
8
|
"================================================================================================-": "",
|
|
9
|
-
"version": "1.8.
|
|
9
|
+
"version": "1.8.39",
|
|
10
10
|
"description": "bim+ renderer",
|
|
11
11
|
"types": "types/bimplus-renderer.d.ts",
|
|
12
12
|
"keywords": [
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
"bump": "npm run bump:patch",
|
|
39
39
|
"bump:patch": "npm version patch --no-git-tag-version",
|
|
40
40
|
"npm-pack": "npm run build-prod && npm pack",
|
|
41
|
-
"lint-doc": "
|
|
42
|
-
"build-doc": "npm run lint-doc &&
|
|
43
|
-
"build-docMd": "npm run lint-doc &&
|
|
41
|
+
"lint-doc": "jsdoc --configure jsdoc.json --explain",
|
|
42
|
+
"build-doc": "npm run lint-doc && jsdoc --configure jsdoc.json",
|
|
43
|
+
"build-docMd": "npm run lint-doc && node scripts/generate-api-markdown.js",
|
|
44
44
|
"build-docConf": "npm run build-docMd && cd ./documentation && python convert_to_confluence_md.py",
|
|
45
45
|
"build-docAll": "npm run build-doc && npm run build-docConf",
|
|
46
46
|
"build-wasm": "emcc -O3 ./src/assembly/process_partition_vertices.cpp -s EXPORT_ES6=1 -s MODULARIZE=1 -s USE_ES6_IMPORT_META=0 -s ENVIRONMENT='worker' -s SINGLE_FILE=1 -s INITIAL_MEMORY=64MB -s ALLOW_MEMORY_GROWTH=1 -s EXTRA_EXPORTED_RUNTIME_METHODS=['ccall'] -s EXPORTED_FUNCTIONS=['_process_vertices','_inplace_transform_vertices','_malloc','_free'] -o ./bin/processPartitionVertices.js",
|
|
@@ -125,21 +125,21 @@
|
|
|
125
125
|
"@pollyjs/cli": "^6.0.6",
|
|
126
126
|
"@pollyjs/core": "^6.0.6",
|
|
127
127
|
"@pollyjs/persister-rest": "^6.0.6",
|
|
128
|
-
"@vitest/browser-playwright": "5.0.
|
|
129
|
-
"@vitest/coverage-istanbul": "5.0.
|
|
128
|
+
"@vitest/browser-playwright": "5.0.4",
|
|
129
|
+
"@vitest/coverage-istanbul": "5.0.4",
|
|
130
130
|
"babel-loader": "^10.1.1",
|
|
131
|
-
"bimplus-websdk": "^1.0.
|
|
131
|
+
"bimplus-websdk": "^1.0.71",
|
|
132
132
|
"clean-webpack-plugin": "^4.0.0",
|
|
133
133
|
"copy-webpack-plugin": "^14.0.0",
|
|
134
134
|
"cpy-cli": "^7.0.0",
|
|
135
|
-
"documentation": "^14.0.3",
|
|
136
135
|
"eslint": "^10.12.0",
|
|
137
136
|
"file-loader": "6.2.0",
|
|
138
137
|
"globals": "17.13.0",
|
|
138
|
+
"jsdoc": "^4.0.5",
|
|
139
139
|
"moment-timezone": "^0.6.4",
|
|
140
140
|
"npm-run-all": "4.1.5",
|
|
141
141
|
"object-hash": "^3.0.0",
|
|
142
|
-
"playwright": "1.64.
|
|
142
|
+
"playwright": "1.64.1",
|
|
143
143
|
"raw-loader": "4.0.2",
|
|
144
144
|
"rimraf": "^6.1.3",
|
|
145
145
|
"shader-loader": "1.3.1",
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const { spawnSync } = require("node:child_process");
|
|
4
|
+
const process = require("node:process");
|
|
5
|
+
|
|
6
|
+
const rootDirectory = path.resolve(__dirname, "..");
|
|
7
|
+
const outputPath = path.join(rootDirectory, "documentation", "Bimplus_Renderer_doc.md");
|
|
8
|
+
const containerKinds = new Set(["namespace", "class", "module"]);
|
|
9
|
+
const symbolKinds = new Set(["function", "member", "constant", "event", "typedef", "callback"]);
|
|
10
|
+
|
|
11
|
+
function escapeCode(value) {
|
|
12
|
+
return String(value).replace(/[&<>]/g, character => ({
|
|
13
|
+
"&": "&",
|
|
14
|
+
"<": "<",
|
|
15
|
+
">": ">"
|
|
16
|
+
})[character]);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function formatType(type) {
|
|
20
|
+
const names = type && type.names ? type.names : [];
|
|
21
|
+
return names.map(name => `<code>${escapeCode(name)}</code>`).join(" | ") || "";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isInstanceMember(doclet) {
|
|
25
|
+
return doclet.scope === "instance" || Boolean(doclet.name && doclet.name.startsWith("this."));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function formatSignature(doclet) {
|
|
29
|
+
const parameters = (doclet.params || []).map(parameter => parameter.name).filter(Boolean);
|
|
30
|
+
return `(${parameters.join(", ")})`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getAnchor(doclet) {
|
|
34
|
+
if (doclet.scope !== "instance" || !doclet.longname.includes("#")) {
|
|
35
|
+
return doclet.longname || doclet.name;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return doclet.longname.replace(/#([^#]*)$/, "+$1");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getDisplayName(doclet) {
|
|
42
|
+
if (doclet.kind !== "function") {
|
|
43
|
+
return doclet.longname || doclet.name;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const signature = formatSignature(doclet);
|
|
47
|
+
const owner = doclet.memberof;
|
|
48
|
+
const name = doclet.name || doclet.longname;
|
|
49
|
+
|
|
50
|
+
if (!owner) {
|
|
51
|
+
return `${name}${signature}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (name.includes("#")) {
|
|
55
|
+
return `${owner}.${name}${signature}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (doclet.scope === "instance") {
|
|
59
|
+
return `${owner[0].toLowerCase()}${owner.slice(1)}.${name}${signature}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return `${doclet.longname}${signature}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getTocLabel(doclet) {
|
|
66
|
+
if (isInstanceMember(doclet)) {
|
|
67
|
+
const name = doclet.name.split("#").at(-1).replace(/^this\./, "");
|
|
68
|
+
return `.${name}${doclet.kind === "function" ? formatSignature(doclet) : ""}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (doclet.kind === "member" && doclet.scope === "static") {
|
|
72
|
+
return `.${doclet.name}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return getDisplayName(doclet);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function renderParameterTable(parameters) {
|
|
79
|
+
if (!parameters || parameters.length === 0) {
|
|
80
|
+
return "";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const rows = parameters.map(parameter => {
|
|
84
|
+
const name = String(parameter.name || "").replace(/\|/g, "\\|");
|
|
85
|
+
const type = formatType(parameter.type).replace(/\|/g, "\\|");
|
|
86
|
+
const description = String(parameter.description || "")
|
|
87
|
+
.replace(/[\r\n]+/g, " ")
|
|
88
|
+
.replace(/\|/g, "\\|");
|
|
89
|
+
return `| ${name} | ${type} | ${description} |`;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return [
|
|
93
|
+
"| Param | Type | Description |",
|
|
94
|
+
"| --- | --- | --- |",
|
|
95
|
+
...rows
|
|
96
|
+
].join("\n");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function renderReturnValues(returns) {
|
|
100
|
+
return (returns || []).map(result => {
|
|
101
|
+
const type = formatType(result.type);
|
|
102
|
+
const description = result.description ? ` - ${result.description}` : "";
|
|
103
|
+
return `**Returns**: ${type}${description}`;
|
|
104
|
+
}).join("\n\n");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function renderExamples(examples) {
|
|
108
|
+
return (examples || []).map(example => `\`\`\`js\n${example}\n\`\`\``).join("\n\n");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function renderExceptions(exceptions) {
|
|
112
|
+
return (exceptions || []).map(exception => {
|
|
113
|
+
const type = formatType(exception.type);
|
|
114
|
+
const description = exception.description ? ` - ${exception.description}` : "";
|
|
115
|
+
return `- ${type}${description}`;
|
|
116
|
+
}).join("\n");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function renderKind(doclet, owner) {
|
|
120
|
+
if (!owner) {
|
|
121
|
+
return "";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const target = `[<code>${owner.name || owner.longname}</code>](#${getAnchor(owner)})`;
|
|
125
|
+
if (doclet.kind === "function" && isInstanceMember(doclet)) {
|
|
126
|
+
return `**Kind**: instance method of ${target}`;
|
|
127
|
+
}
|
|
128
|
+
if (doclet.kind === "function" && doclet.scope === "static") {
|
|
129
|
+
return `**Kind**: static method of ${target}`;
|
|
130
|
+
}
|
|
131
|
+
if (doclet.kind === "member" && doclet.scope === "static") {
|
|
132
|
+
return `**Kind**: static property of ${target}`;
|
|
133
|
+
}
|
|
134
|
+
if (doclet.kind === "member" && isInstanceMember(doclet)) {
|
|
135
|
+
return `**Kind**: instance property of ${target}`;
|
|
136
|
+
}
|
|
137
|
+
return `**Kind**: ${doclet.kind} of ${target}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function renderEntry(doclet, owner) {
|
|
141
|
+
const anchor = getAnchor(doclet);
|
|
142
|
+
const sections = [`<a id="${anchor}" name="${anchor}"></a>`, `### ${getDisplayName(doclet)}`];
|
|
143
|
+
|
|
144
|
+
if (doclet.description) {
|
|
145
|
+
sections.push(doclet.description);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const kind = renderKind(doclet, owner);
|
|
149
|
+
if (kind) {
|
|
150
|
+
sections.push(kind);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (doclet.type) {
|
|
154
|
+
sections.push(`**Type**: ${formatType(doclet.type)}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (doclet.defaultvalue !== undefined) {
|
|
158
|
+
const value = typeof doclet.defaultvalue === "string"
|
|
159
|
+
? doclet.defaultvalue
|
|
160
|
+
: JSON.stringify(doclet.defaultvalue);
|
|
161
|
+
sections.push(`**Default**: <code>${escapeCode(value)}</code>`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (doclet.readonly) {
|
|
165
|
+
sections.push("**Read-only**: Yes");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const returns = renderReturnValues(doclet.returns);
|
|
169
|
+
if (returns) {
|
|
170
|
+
sections.push(returns);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const parameters = renderParameterTable(doclet.params);
|
|
174
|
+
if (parameters) {
|
|
175
|
+
sections.push(parameters);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (doclet.deprecated) {
|
|
179
|
+
sections.push(`**Deprecated**: ${typeof doclet.deprecated === "string" ? doclet.deprecated : "Yes"}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (doclet.since) {
|
|
183
|
+
sections.push(`**Since**: ${doclet.since}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const exceptions = renderExceptions(doclet.exceptions);
|
|
187
|
+
if (exceptions) {
|
|
188
|
+
sections.push(`**Throws**:\n\n${exceptions}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const examples = renderExamples(doclet.examples);
|
|
192
|
+
if (examples) {
|
|
193
|
+
sections.push(`**Example**:\n\n${examples}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (doclet.see && doclet.see.length) {
|
|
197
|
+
sections.push(`**See**: ${doclet.see.join(", ")}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return sections.join("\n\n");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function renderContainerIndex(container, symbols) {
|
|
204
|
+
const instanceSymbols = symbols.filter(isInstanceMember);
|
|
205
|
+
const staticSymbols = symbols.filter(symbol => !instanceSymbols.includes(symbol));
|
|
206
|
+
const containerType = container.kind === "namespace" ? "object" : container.kind;
|
|
207
|
+
const lines = [`- [${container.name}](#${getAnchor(container)}) : <code>${containerType}</code>`];
|
|
208
|
+
|
|
209
|
+
if (instanceSymbols.length) {
|
|
210
|
+
lines.push(" - _instance_");
|
|
211
|
+
for (const symbol of instanceSymbols) {
|
|
212
|
+
lines.push(` - [${getTocLabel(symbol)}](#${getAnchor(symbol)})`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (staticSymbols.length) {
|
|
217
|
+
lines.push(" - _static_");
|
|
218
|
+
for (const symbol of staticSymbols) {
|
|
219
|
+
lines.push(` - [${getTocLabel(symbol)}](#${getAnchor(symbol)})`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return lines.join("\n");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function renderContainer(container, symbols) {
|
|
227
|
+
const containerType = container.kind === "namespace" ? "object" : container.kind;
|
|
228
|
+
const sections = [
|
|
229
|
+
`<a id="${getAnchor(container)}" name="${getAnchor(container)}"></a>`,
|
|
230
|
+
`## ${container.name} : <code>${containerType}</code>`,
|
|
231
|
+
`**Kind**: global ${container.kind}`
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
if (container.description || container.classdesc) {
|
|
235
|
+
sections.push(container.description || container.classdesc);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (container.kind === "class") {
|
|
239
|
+
sections.push(`**Constructor**: \`new ${container.name}${formatSignature(container)}\``);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const parameters = renderParameterTable(container.params);
|
|
243
|
+
if (parameters) {
|
|
244
|
+
sections.push(parameters);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
for (const symbol of symbols) {
|
|
248
|
+
sections.push(renderEntry(symbol, container));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return sections.join("\n\n");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function getDoclets() {
|
|
255
|
+
const jsdocCliPath = path.join(rootDirectory, "node_modules", "jsdoc", "jsdoc.js");
|
|
256
|
+
const result = spawnSync(process.execPath, [jsdocCliPath, "--configure", "jsdoc.json", "--explain"], {
|
|
257
|
+
cwd: rootDirectory,
|
|
258
|
+
encoding: "utf8",
|
|
259
|
+
maxBuffer: 32 * 1024 * 1024
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
if (result.error) {
|
|
263
|
+
throw result.error;
|
|
264
|
+
}
|
|
265
|
+
if (result.status !== 0) {
|
|
266
|
+
throw new Error(result.stderr || `JSDoc exited with status ${result.status}`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return JSON.parse(result.stdout);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function renderMarkdown(doclets) {
|
|
273
|
+
const containers = [];
|
|
274
|
+
const containerByName = new Map();
|
|
275
|
+
const containerNames = new Set();
|
|
276
|
+
|
|
277
|
+
for (const doclet of doclets) {
|
|
278
|
+
const documented = doclet.name && !doclet.name.startsWith("_") && doclet.comment && doclet.comment.trim()
|
|
279
|
+
&& !doclet.undocumented && doclet.access !== "private";
|
|
280
|
+
const key = doclet.longname || doclet.name;
|
|
281
|
+
if (documented && containerKinds.has(doclet.kind) && key && !containerNames.has(key)) {
|
|
282
|
+
containers.push(doclet);
|
|
283
|
+
containerNames.add(key);
|
|
284
|
+
containerByName.set(key, doclet);
|
|
285
|
+
if (!containerByName.has(doclet.name)) {
|
|
286
|
+
containerByName.set(doclet.name, doclet);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const symbols = doclets.filter(doclet => {
|
|
292
|
+
const documented = doclet.name && !doclet.name.startsWith("_") && doclet.comment && doclet.comment.trim()
|
|
293
|
+
&& !doclet.undocumented && doclet.access !== "private";
|
|
294
|
+
const supportedKind = symbolKinds.has(doclet.kind);
|
|
295
|
+
const supportedOwner = containerByName.has(doclet.memberof) || (!doclet.memberof && doclet.kind === "function");
|
|
296
|
+
return documented && supportedKind && supportedOwner;
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const symbolsByContainer = new Map(containers.map(container => [container.longname || container.name, []]));
|
|
300
|
+
const globalFunctions = [];
|
|
301
|
+
const uniqueSymbols = new Map();
|
|
302
|
+
|
|
303
|
+
for (const symbol of symbols) {
|
|
304
|
+
const key = symbol.longname || `${symbol.kind}:${symbol.name}`;
|
|
305
|
+
if (uniqueSymbols.has(key)) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
uniqueSymbols.set(key, symbol);
|
|
309
|
+
|
|
310
|
+
if (symbol.memberof) {
|
|
311
|
+
const owner = containerByName.get(symbol.memberof);
|
|
312
|
+
symbolsByContainer.get(owner.longname || owner.name).push(symbol);
|
|
313
|
+
} else {
|
|
314
|
+
globalFunctions.push(symbol);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const output = ["## Objects", containers.map(container =>
|
|
319
|
+
renderContainerIndex(container, symbolsByContainer.get(container.longname || container.name))
|
|
320
|
+
).join("\n")];
|
|
321
|
+
|
|
322
|
+
if (globalFunctions.length) {
|
|
323
|
+
output.push("## Functions", globalFunctions.map(symbol =>
|
|
324
|
+
`- [${getTocLabel(symbol)}](#${getAnchor(symbol)})`
|
|
325
|
+
).join("\n"));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
for (const container of containers) {
|
|
329
|
+
output.push(renderContainer(container, symbolsByContainer.get(container.longname || container.name)));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
for (const symbol of globalFunctions) {
|
|
333
|
+
output.push(renderEntry(symbol));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const markdown = `${output.join("\n\n")}\n`;
|
|
337
|
+
const anchors = [...markdown.matchAll(/<a id="([^"]+)" name="\1"><\/a>/g)].map(match => match[1]);
|
|
338
|
+
const anchorSet = new Set(anchors);
|
|
339
|
+
|
|
340
|
+
if (anchors.length !== anchorSet.size) {
|
|
341
|
+
throw new Error("Duplicate Markdown anchor ID generated");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
for (const symbol of uniqueSymbols.values()) {
|
|
345
|
+
if (!anchorSet.has(getAnchor(symbol))) {
|
|
346
|
+
throw new Error(`Missing Markdown anchor for ${symbol.longname}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const linkTargets = [...markdown.matchAll(/\]\(#([^)]+)\)/g)].map(match => match[1]);
|
|
351
|
+
const missingTargets = linkTargets.filter(target => !anchorSet.has(target));
|
|
352
|
+
if (missingTargets.length) {
|
|
353
|
+
throw new Error(`Markdown link target has no ID anchor: ${missingTargets[0]}`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (containerNames.size !== containers.length) {
|
|
357
|
+
throw new Error("Container deduplication failed");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return { markdown, namespaceCount: containers.length, symbolCount: uniqueSymbols.size };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
const result = renderMarkdown(getDoclets());
|
|
365
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
366
|
+
fs.writeFileSync(outputPath, result.markdown, "utf8");
|
|
367
|
+
console.log(`Generated Markdown for ${result.namespaceCount} containers and ${result.symbolCount} API symbols.`);
|
|
368
|
+
} catch (error) {
|
|
369
|
+
console.error(error.message);
|
|
370
|
+
process.exitCode = 1;
|
|
371
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const { parse } = require("@babel/parser");
|
|
4
|
+
|
|
5
|
+
const classInfoByFile = new Map();
|
|
6
|
+
|
|
7
|
+
function getClassInfo(doclet) {
|
|
8
|
+
const filePath = path.resolve(doclet.meta.path, doclet.meta.filename);
|
|
9
|
+
if (classInfoByFile.has(filePath)) {
|
|
10
|
+
return classInfoByFile.get(filePath);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const source = fs.readFileSync(filePath, "utf8");
|
|
14
|
+
const ast = parse(source, { sourceType: "module" });
|
|
15
|
+
const declaration = ast.program.body.find(statement =>
|
|
16
|
+
statement.type === "ExportDefaultDeclaration" && statement.declaration.type === "ClassDeclaration"
|
|
17
|
+
)?.declaration;
|
|
18
|
+
|
|
19
|
+
if (!declaration) {
|
|
20
|
+
classInfoByFile.set(filePath, null);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const fallbackName = path.basename(filePath, path.extname(filePath));
|
|
25
|
+
const name = declaration.id?.name || fallbackName[0].toUpperCase() + fallbackName.slice(1);
|
|
26
|
+
const staticFunctionMembers = new Set(
|
|
27
|
+
declaration.body.body
|
|
28
|
+
.filter(member => member.static && member.type === "ClassProperty"
|
|
29
|
+
&& ["ArrowFunctionExpression", "FunctionExpression"].includes(member.value?.type))
|
|
30
|
+
.map(member => member.key.name)
|
|
31
|
+
);
|
|
32
|
+
const members = new Map(
|
|
33
|
+
declaration.body.body
|
|
34
|
+
.filter(member => member.key?.name && ["ClassMethod", "ClassProperty"].includes(member.type))
|
|
35
|
+
.map(member => [member.key.name, { isStatic: Boolean(member.static) }])
|
|
36
|
+
);
|
|
37
|
+
const lineRange = { start: declaration.loc.start.line, end: declaration.loc.end.line };
|
|
38
|
+
const classInfo = { name, staticFunctionMembers, members, lineRange };
|
|
39
|
+
classInfoByFile.set(filePath, classInfo);
|
|
40
|
+
return classInfo;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isClassBodyMember(doclet, classInfo) {
|
|
44
|
+
const line = doclet.meta.lineno;
|
|
45
|
+
return classInfo.members.has(doclet.name)
|
|
46
|
+
&& line >= classInfo.lineRange.start && line <= classInfo.lineRange.end;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeDefaultClass(doclet) {
|
|
50
|
+
if (!doclet.meta || !doclet.meta.path || !doclet.meta.filename) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const classInfo = getClassInfo(doclet);
|
|
55
|
+
if (!classInfo) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (doclet.kind === "class" && doclet.longname === "module.exports") {
|
|
60
|
+
doclet.name = classInfo.name;
|
|
61
|
+
doclet.longname = classInfo.name;
|
|
62
|
+
doclet.scope = "global";
|
|
63
|
+
delete doclet.memberof;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!doclet.memberof && doclet.scope === "global" && isClassBodyMember(doclet, classInfo)) {
|
|
68
|
+
const member = classInfo.members.get(doclet.name);
|
|
69
|
+
doclet.scope = member.isStatic ? "static" : "instance";
|
|
70
|
+
doclet.memberof = classInfo.name;
|
|
71
|
+
doclet.longname = `${classInfo.name}${member.isStatic ? "." : "#"}${doclet.name}`;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (doclet.memberof !== "module.exports") {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const name = String(doclet.name || "").replace(/^this\./, "");
|
|
80
|
+
const isStaticFunction = classInfo.staticFunctionMembers.has(name);
|
|
81
|
+
if (isStaticFunction) {
|
|
82
|
+
doclet.scope = "static";
|
|
83
|
+
doclet.kind = "function";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
doclet.memberof = classInfo.name;
|
|
87
|
+
doclet.longname = doclet.scope === "static"
|
|
88
|
+
? `${classInfo.name}.${name}`
|
|
89
|
+
: `${classInfo.name}#${name}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
exports.handlers = {
|
|
93
|
+
parseComplete(event) {
|
|
94
|
+
const doclets = [];
|
|
95
|
+
for (const doclet of event.doclets) {
|
|
96
|
+
if (doclet.kind === "class" && doclet.longname === "module.exports#module.exports"
|
|
97
|
+
&& getClassInfo(doclet)) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
normalizeDefaultClass(doclet);
|
|
102
|
+
doclets.push(doclet);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
event.doclets.splice(0, event.doclets.length, ...doclets);
|
|
106
|
+
}
|
|
107
|
+
};
|