bimplus-webclient 0.1.70 → 0.1.71

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,13 +1,13 @@
1
1
  ---
2
- applyTo: "src/**/*.js, doc_config.yml, documentation/**"
2
+ applyTo: "src/**/*.js, scripts/generate-api-markdown.js, jsdoc.json, documentation/**"
3
3
  ---
4
4
 
5
5
  # Generating API Documentation
6
6
 
7
- This project uses the [`documentation`](https://github.com/documentationjs/documentation) npm package (JSDoc-based) to generate API docs from the JSDoc comments in `src/*.js`. Output is written to `./documentation`.
7
+ This project uses [`jsdoc`](https://jsdoc.app/) to generate HTML API docs and parse doclets. The repository-owned `scripts/generate-api-markdown.js` renders those doclets into Markdown without a separate Markdown-generation dependency. `jsdoc.json` controls source discovery and HTML output to `./documentation`; the default JSDoc template creates `index.html` and API pages there, using `README.md` as landing-page content.
8
8
 
9
9
  ## Prerequisites
10
- - Node dependencies installed (`npm install`) — `documentation` is a devDependency.
10
+ - Node dependencies installed (`npm install`) — `jsdoc` is a devDependency.
11
11
  - Python 3 available on PATH (only required for the Confluence conversion step, which runs `documentation/convert_to_confluence_md.py`).
12
12
  - All exported functions/classes in `src/*.js` should have valid JSDoc comments; `lint-doc` will fail the build on invalid JSDoc.
13
13
 
@@ -15,11 +15,11 @@ This project uses the [`documentation`](https://github.com/documentationjs/docum
15
15
 
16
16
  | Script | Purpose |
17
17
  | --- | --- |
18
- | `npm run lint-doc` | Runs `documentation lint ./src/*.js` to validate JSDoc comments before generating docs. |
19
- | `npm run build-doc` | Lints, then builds **HTML** docs into `./documentation` using `doc_config.yml` (defines the table of contents). |
20
- | `npm run build-docMd` | Lints, then builds a single **Markdown** file at `./documentation/Bimplus_WebClient_doc.md`. |
18
+ | `npm run lint-doc` | Runs `jsdoc --pedantic --configure jsdoc.json --explain` to parse source and validate JSDoc comments. |
19
+ | `npm run build-doc` | Lints, then generates **HTML** docs into `./documentation` with `jsdoc.json`. |
20
+ | `npm run build-docMd` | Lints, then renders JSDoc doclets into `./documentation/Bimplus_WebClient_doc.md` with the repository-owned generator. |
21
21
  | `npm run build-docConf` | Runs `build-docMd`, then runs `documentation/convert_to_confluence_md.py` to produce `Bimplus_WebClient_doc_confluence.md`, a Confluence-friendly variant with fixed anchor links and an injected version header. |
22
- | `npm run build-docAll` | Runs `build-doc`, `build-docConf`, `build-docMd`, `build-doc` in sequence — regenerates HTML, Markdown, and Confluence docs together. |
22
+ | `npm run build-docAll` | Runs `build-doc`, `build-docMd`, and `build-docConf` to regenerate HTML, Markdown, and Confluence docs. |
23
23
 
24
24
  ## Typical Workflow
25
25
 
@@ -28,14 +28,18 @@ This project uses the [`documentation`](https://github.com/documentationjs/docum
28
28
  3. Run `npm run build-docAll` to regenerate all doc formats at once (recommended before a release/publish).
29
29
  - Or run an individual script (`build-doc`, `build-docMd`, `build-docConf`) if you only need one output format.
30
30
  4. Review generated output:
31
- - `./documentation/index.html` (and per-file `.html` pages) for the HTML site.
31
+ - `./documentation/index.html` and the generated API pages for the HTML site.
32
32
  - `./documentation/Bimplus_WebClient_doc.md` for the raw Markdown API doc.
33
33
  - `./documentation/Bimplus_WebClient_doc_confluence.md` for the Confluence-paste-ready version.
34
34
  5. Commit the regenerated files under `./documentation` along with your source changes.
35
35
 
36
- ## Notes on `doc_config.yml`
37
- - Controls the HTML build's table of contents (`toc:` entries map to sections/pages).
38
- - Add a new `toc` entry here when introducing a new top-level `src/*.js` module you want listed separately in the HTML docs.
36
+ ## Notes on `jsdoc.json`
37
+ - Controls recursive JavaScript source discovery, ES module parsing, HTML destination, and README landing-page content.
38
+ - Update this configuration if the source layout or generated HTML destination changes.
39
+
40
+ ## Notes on the Markdown renderer
41
+ - `scripts/generate-api-markdown.js` consumes JSDoc's structured `--explain` output and renders documented namespaces, methods, callbacks, parameters, returns, and explicit ID anchors. It fails if an internal link has no matching ID.
42
+ - The generator checks that each documented symbol has an anchor. Preserve the `BimExplorer.BimObjectProperties+load` anchor convention because Confluence links depend on it.
39
43
 
40
44
  ## Notes on the Confluence conversion (`convert_to_confluence_md.py`)
41
45
  - Reads `./Bimplus_WebClient_doc.md` (must be generated first via `build-docMd`) and writes `./Bimplus_WebClient_doc_confluence.md`.
@@ -47,3 +51,5 @@ This project uses the [`documentation`](https://github.com/documentationjs/docum
47
51
  - **`lint-doc` fails**: fix the JSDoc syntax/tag errors it reports; the `build-*` scripts depend on it succeeding first.
48
52
  - **Confluence conversion fails with "file not found"**: run `npm run build-docMd` first, or use `npm run build-docConf` which does it automatically.
49
53
  - **Python not found**: only `build-docConf`/`build-docAll` need Python; ensure `python` resolves on PATH (or update the script invocation to `python3` if needed on your OS).
54
+
55
+ Generated HTML and Markdown must be regenerated after changing source JSDoc comments, then reviewed and committed with the source changes.
@@ -18,15 +18,23 @@ Files expected to change:
18
18
  - `package.json`
19
19
  - `package-lock.json`
20
20
  - New root-level `jsdoc.json` configuration
21
+ - New `scripts/generate-api-markdown.js` renderer
21
22
  - `.github/documentation.instructions.md`
23
+ - `README.md`
24
+ - `documentation/convert_to_confluence_md.py`
22
25
  - Generated files under `documentation/`
23
26
 
24
27
  The following should remain functionally unchanged:
25
28
 
26
29
  - JSDoc comments in `src/*.js`
27
- - `documentation/convert_to_confluence_md.py`
28
30
  - Application source, build, and test configuration
29
31
 
32
+ ## Porting This Plan to Other Projects
33
+
34
+ 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.
35
+
36
+ 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.
37
+
30
38
  ## Implementation Steps
31
39
 
32
40
  ### 1. Replace development dependencies
@@ -34,14 +42,17 @@ The following should remain functionally unchanged:
34
42
  Remove `documentation` from `devDependencies` and add:
35
43
 
36
44
  - `jsdoc` for parsing source comments and generating HTML
37
- - `jsdoc-to-markdown` for Markdown API output
45
+ - A repository-owned Markdown renderer using JSDoc's structured doclets
38
46
 
39
- Install the dependencies with:
47
+ Remove any old generator dependencies and install JSDoc:
40
48
 
41
49
  ```powershell
42
- npm install --save-dev jsdoc jsdoc-to-markdown
50
+ npm uninstall documentation jsdoc-to-markdown
51
+ npm install --save-dev jsdoc
43
52
  ```
44
53
 
54
+ Do not add a third-party Markdown generator. `scripts/generate-api-markdown.js` renders JSDoc doclets directly and validates symbol anchors.
55
+
45
56
  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.
46
57
 
47
58
  ### 2. Add `jsdoc.json`
@@ -53,7 +64,7 @@ Create a root-level JSDoc configuration with these requirements:
53
64
  - Parse ES module syntax
54
65
  - Write HTML output to `./documentation`
55
66
  - Use `README.md` as the documentation landing-page content when supported
56
- - Enable Markdown processing and recursive source discovery
67
+ - Enable JSDoc's Markdown plugin and recursive source discovery
57
68
 
58
69
  Proposed configuration:
59
70
 
@@ -74,13 +85,12 @@ Proposed configuration:
74
85
  "monospaceLinks": true
75
86
  },
76
87
  "plugins": [
77
- "plugins/markdown",
78
- "plugins/recurse"
88
+ "plugins/markdown"
79
89
  ]
80
90
  }
81
91
  ```
82
92
 
83
- Validate the exact configuration against the installed JSDoc version before committing it.
93
+ `opts.recurse: true` enables recursion. 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 exact configuration against the installed JSDoc version before committing it.
84
94
 
85
95
  ### 3. Update npm scripts
86
96
 
@@ -90,13 +100,24 @@ Replace the current `documentation` commands with JSDoc equivalents while retain
90
100
  {
91
101
  "lint-doc": "jsdoc --pedantic --configure jsdoc.json --explain",
92
102
  "build-doc": "npm run lint-doc && jsdoc --configure jsdoc.json",
93
- "build-docMd": "npm run lint-doc && jsdoc2md --files \"src/*.js\" > documentation/Bimplus_WebClient_doc.md",
103
+ "build-docMd": "npm run lint-doc && node scripts/generate-api-markdown.js",
94
104
  "build-docConf": "npm run build-docMd && cd ./documentation && python convert_to_confluence_md.py",
95
105
  "build-docAll": "npm run build-doc && npm run build-docMd && npm run build-docConf"
96
106
  }
97
107
  ```
98
108
 
99
- The `lint-doc` command should be tested with the installed JSDoc CLI. If JSDoc does not support the selected validation flags, replace it with the supported parse-only validation command while keeping the script name.
109
+ `scripts/generate-api-markdown.js` replaces `jsdoc2md`; do not add a third-party Markdown renderer. The renderer should:
110
+
111
+ 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.
112
+ 2. Parse the JSON doclets and include documented public 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.
113
+ 3. Render the information present on doclets: namespace 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.
114
+ 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.
115
+ 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.
116
+ 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.
117
+
118
+ 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.
119
+
120
+ The `lint-doc` command should be tested with the installed JSDoc CLI. `--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.
100
121
 
101
122
  ### 4. Regenerate documentation
102
123
 
@@ -115,7 +136,14 @@ Then run the complete workflow:
115
136
  npm run build-docAll
116
137
  ```
117
138
 
118
- The Confluence converter should continue reading the generated Markdown from its current location. No Python changes are expected unless the new Markdown headings or anchors expose a compatibility issue.
139
+ The Confluence converter should continue reading the generated Markdown from its current location. It must:
140
+
141
+ - Read and write UTF-8 explicitly so Unicode output works on Windows.
142
+ - Rewrite links case-insensitively when JSDoc preserves case in fragments.
143
+ - When rewriting a target to `WebClientApidescription-{fragment}`, emit a matching `<a id="WebClientApidescription-{fragment}" name="WebClientApidescription-{fragment}"></a>` alias. Rewriting links without creating matching targets leaves the converted document's links dead.
144
+ - Validate every converted internal Markdown link against an emitted ID before writing the final Confluence file.
145
+
146
+ Keep the raw Markdown IDs and Confluence aliases separate: the raw file uses the API fragments; only the Confluence variant uses the required `WebClientApidescription-` prefix.
119
147
 
120
148
  ### 5. Review generated output
121
149
 
@@ -124,32 +152,38 @@ Check the following:
124
152
  - `documentation/index.html` opens successfully
125
153
  - HTML assets load without missing-file errors
126
154
  - All public classes, methods, namespaces, callbacks, parameters, and return values are represented
155
+ - Every rendered API symbol has a generated Markdown anchor, and internal Markdown links resolve
127
156
  - Markdown headings and links are readable
128
157
  - Confluence anchors contain the expected `WebClientApidescription-` prefix
158
+ - Confluence-prefixed links have matching prefixed target IDs
129
159
  - No stale files from the previous generator remain in the generated output
130
160
 
161
+ 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 namespace, an instance method containing `+`, a static callback containing `.`, and the nested `BimObjectProperties` 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.
162
+
131
163
  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.
132
164
 
133
165
  ### 6. Update repository guidance
134
166
 
135
- Update `.github/documentation.instructions.md` to describe:
167
+ Update `.github/documentation.instructions.md` and `README.md` to describe:
136
168
 
137
169
  - `jsdoc.json`
138
- - `jsdoc` and `jsdoc-to-markdown`
170
+ - `jsdoc` and the repository-owned Markdown renderer
139
171
  - The revised npm scripts
140
172
  - The new HTML output structure
141
173
  - The continued Markdown-to-Confluence conversion
142
174
  - The fact that generated output must be regenerated after source JSDoc changes
175
+ - The repository-owned Markdown renderer and its symbol/link validation
176
+ - The distinction between raw Markdown IDs and Confluence-prefixed alias IDs
143
177
 
144
178
  ### 7. Remove obsolete dependency output
145
179
 
146
180
  After a successful install, verify that the old packages are gone:
147
181
 
148
182
  ```powershell
149
- npm ls documentation vue-template-compiler
183
+ npm ls documentation jsdoc-to-markdown vue-template-compiler
150
184
  ```
151
185
 
152
- The command should report that neither package is installed, or that they are not found in the dependency tree.
186
+ The command should report that none of these packages is installed, or that they are not found in the dependency tree. Search scripts, instructions, READMEs, and the lockfile for stale `jsdoc2md`/`jsdoc-to-markdown` references; a removal-check command in this plan may name the package intentionally.
153
187
 
154
188
  Review `package-lock.json` for removal of the obsolete dependency chain. Do not remove unrelated lockfile changes produced by the package manager.
155
189
 
@@ -166,6 +200,12 @@ npm test
166
200
  npm audit
167
201
  ```
168
202
 
203
+ Also lint the renderer directly if the repository ESLint script does not include it:
204
+
205
+ ```powershell
206
+ npx eslint scripts/generate-api-markdown.js
207
+ ```
208
+
169
209
  Expected results:
170
210
 
171
211
  - Documentation commands complete successfully
@@ -173,6 +213,8 @@ Expected results:
173
213
  - The application build remains successful
174
214
  - Existing tests remain successful
175
215
  - The `vue-template-compiler` audit finding is no longer introduced by this documentation toolchain
216
+ - `jsdoc-to-markdown` is absent from the manifest, lockfile dependency tree, and generation scripts
217
+ - Rendered raw Markdown and Confluence Markdown links navigate to their intended targets in their actual consumers
176
218
 
177
219
  `npm audit` may still report unrelated vulnerabilities. Record those separately instead of applying a forced dependency downgrade.
178
220
 
@@ -181,9 +223,9 @@ Expected results:
181
223
  If the generated output loses API content or the Confluence conversion cannot be adapted safely:
182
224
 
183
225
  1. Restore the previous `package.json` and `package-lock.json` documentation dependencies.
184
- 2. Remove `jsdoc.json`.
226
+ 2. Remove `jsdoc.json` and `scripts/generate-api-markdown.js`.
185
227
  3. Restore the previous generated `documentation/` output.
186
- 4. Reassess the JSDoc configuration and Markdown template before retrying.
228
+ 4. Reassess the JSDoc configuration and renderer before retrying.
187
229
 
188
230
  Do not modify application source code as part of rollback.
189
231
 
@@ -193,7 +235,7 @@ The migration is complete when:
193
235
 
194
236
  - `documentation` is no longer a development dependency
195
237
  - JSDoc generates readable HTML from `src/*.js`
196
- - `jsdoc-to-markdown` generates the API Markdown file
238
+ - The repository-owned renderer generates the API Markdown file
197
239
  - The existing Confluence conversion succeeds
198
240
  - `build-doc`, `build-docMd`, `build-docConf`, and `build-docAll` all work
199
241
  - Generated documentation is reviewed and committed
package/README.md CHANGED
@@ -74,19 +74,22 @@ goto your application folder which uses the webclient
74
74
  npm install
75
75
 
76
76
  ### Unit testing
77
- The unit tests run with Vitest and jsdom. Run them with coverage using:
77
+ The tests run with Vitest in jsdom, Chromium, and Firefox. Install the browser
78
+ binaries with `npx playwright install` before the first run. Run the tests
79
+ with coverage using:
78
80
 
79
- npm test
81
+ npm run test
82
+
83
+ The browser test API binds to `127.0.0.1`, starting at port `41315`, outside the
84
+ Windows default dynamic port range. If the port is in use, Vitest selects the
85
+ next available port so multiple test runs can execute concurrently.
80
86
 
81
87
  Run Vitest in watch mode during development with:
82
88
 
83
89
  npm run test-watch
84
90
 
85
91
  ### Documentation
86
- Bimplus webclient uses npm documentation plugin for easy documentation generation.
87
- To generate a documentation please install documentation plugin via npm :
88
-
89
- npm install -g documentation
92
+ Bimplus webclient uses JSDoc for HTML documentation and a repository-owned renderer for Markdown API documentation. Install project dependencies with `npm install` before generating docs.
90
93
 
91
94
  To create documentation in html format go to your webclient folder and run :
92
95
 
@@ -98,7 +101,7 @@ To create documentation in markdown format go to your webclient folder and run :
98
101
 
99
102
  npm run build-docMd
100
103
 
101
- Documentation output will be in webclient/documentation/Bimplus_WebSDK_doc.md file.
104
+ Documentation output will be in `webclient/documentation/Bimplus_WebClient_doc.md`.
102
105
 
103
106
  To create a markdown suitable for Confluence run script:
104
107
 
package/jsdoc.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "source": {
3
+ "include": ["src"],
4
+ "includePattern": ".*\\.js$",
5
+ "excludePattern": "(^|[\\\\/])node_modules[\\\\/]"
6
+ },
7
+ "opts": {
8
+ "destination": "./documentation",
9
+ "recurse": true,
10
+ "readme": "./README.md"
11
+ },
12
+ "templates": {
13
+ "cleverLinks": false,
14
+ "monospaceLinks": true
15
+ },
16
+ "plugins": [
17
+ "plugins/markdown"
18
+ ]
19
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bimplus-webclient",
3
- "version": "0.1.70",
3
+ "version": "0.1.71",
4
4
  "description": "bim+ web client",
5
5
  "types": "types/bimplus-webclient.d.ts",
6
6
  "keywords": [
@@ -24,28 +24,28 @@
24
24
  "bump": "npm run bump:patch",
25
25
  "bump:patch": "npm version patch --no-git-tag-version",
26
26
  "npm-pack": "npm run build-prod && npm pack",
27
- "lint-doc": "documentation lint ./src/*.js",
28
- "build-doc": "npm run lint-doc && documentation build ./src/*.js --config doc_config.yml -f html -o ./documentation",
29
- "build-docMd": "npm run lint-doc && documentation build ./src/*.js -f md -o ./documentation/Bimplus_WebClient_doc.md",
27
+ "lint-doc": "jsdoc --pedantic --configure jsdoc.json --explain",
28
+ "build-doc": "npm run lint-doc && jsdoc --configure jsdoc.json",
29
+ "build-docMd": "npm run lint-doc && node scripts/generate-api-markdown.js",
30
30
  "build-docConf": "npm run build-docMd && cd ./documentation && python convert_to_confluence_md.py",
31
- "build-docAll": "npm run build-doc && npm run build-docConf && npm run build-docMd && npm run build-doc",
31
+ "build-docAll": "npm run build-doc && npm run build-docMd && npm run build-docConf",
32
32
  "test": "vitest run --coverage",
33
33
  "test-watch": "vitest"
34
34
  },
35
35
  "devDependencies": {
36
- "@babel/core": "^8.0.6",
37
- "@babel/preset-env": "^8.0.6",
36
+ "@babel/core": "^8.0.7",
37
+ "@babel/preset-env": "^8.0.7",
38
38
  "@eslint/js": "^10.0.1",
39
- "@vitest/browser-playwright": "^5.0.3",
40
- "@vitest/coverage-istanbul": "^5.0.3",
39
+ "@vitest/browser-playwright": "^5.0.4",
40
+ "@vitest/coverage-istanbul": "^5.0.4",
41
41
  "babel-loader": "^10.1.1",
42
- "documentation": "^14.0.3",
43
42
  "eslint": "^10.12.0",
44
43
  "globals": "17.13.0",
44
+ "jsdoc": "^4.0.5",
45
45
  "jsdom": "^30.1.2",
46
- "playwright": "^1.63.0",
46
+ "playwright": "^1.64.0",
47
47
  "terser-webpack-plugin": "^5.6.1",
48
- "vitest": "^5.0.3",
48
+ "vitest": "^5.0.4",
49
49
  "webpack": "^5.111.1",
50
50
  "webpack-cli": "^7.2.3",
51
51
  "webpack-merge": "^6.0.1"
@@ -59,5 +59,5 @@
59
59
  "version": "^22.22.2 || ^24.15.0 || >=26.0.0",
60
60
  "onFail": "error"
61
61
  }
62
- }
62
+ }
63
63
  }
@@ -0,0 +1,344 @@
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_WebClient_doc.md");
8
+ const namespaceKinds = new Set(["function", "member", "constant", "event", "typedef"]);
9
+
10
+ function escapeCode(value) {
11
+ return String(value).replace(/[&<>]/g, character => ({
12
+ "&": "&amp;",
13
+ "<": "&lt;",
14
+ ">": "&gt;"
15
+ })[character]);
16
+ }
17
+
18
+ function formatType(type) {
19
+ const names = type && type.names ? type.names : [];
20
+ return names.map(name => `<code>${escapeCode(name)}</code>`).join(" | ") || "";
21
+ }
22
+
23
+ function formatSignature(doclet) {
24
+ const parameters = (doclet.params || []).map(parameter => parameter.name).filter(Boolean);
25
+ return `(${parameters.join(", ")})`;
26
+ }
27
+
28
+ function getAnchor(doclet) {
29
+ if (doclet.kind !== "function" || !doclet.longname.includes("#")) {
30
+ return doclet.longname || doclet.name;
31
+ }
32
+
33
+ const parts = doclet.longname.split("#");
34
+ if (parts.length === 2) {
35
+ return `${parts[0]}+${parts[1]}`;
36
+ }
37
+
38
+ return `${parts.slice(0, -1).join(".")}+${parts.at(-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 (doclet.kind === "function" && doclet.scope === "instance") {
67
+ return `.${doclet.name.split("#").at(-1)}${formatSignature(doclet)}`;
68
+ }
69
+
70
+ if (doclet.kind === "member" && doclet.scope === "static") {
71
+ return `.${doclet.name}`;
72
+ }
73
+
74
+ return getDisplayName(doclet);
75
+ }
76
+
77
+ function renderParameterTable(parameters) {
78
+ if (!parameters || parameters.length === 0) {
79
+ return "";
80
+ }
81
+
82
+ const rows = parameters.map(parameter => {
83
+ const name = String(parameter.name || "").replace(/\|/g, "\\|");
84
+ const description = String(parameter.description || "").replace(/\|/g, "\\|");
85
+ return `| ${name} | ${formatType(parameter.type)} | ${description} |`;
86
+ });
87
+
88
+ return [
89
+ "| Param | Type | Description |",
90
+ "| --- | --- | --- |",
91
+ ...rows
92
+ ].join("\n");
93
+ }
94
+
95
+ function renderReturnValues(returns) {
96
+ return (returns || []).map(result => {
97
+ const type = formatType(result.type);
98
+ const description = result.description ? ` - ${result.description}` : "";
99
+ return `**Returns**: ${type}${description}`;
100
+ }).join("\n\n");
101
+ }
102
+
103
+ function renderExamples(examples) {
104
+ return (examples || []).map(example => `\`\`\`js\n${example}\n\`\`\``).join("\n\n");
105
+ }
106
+
107
+ function renderExceptions(exceptions) {
108
+ return (exceptions || []).map(exception => {
109
+ const type = formatType(exception.type);
110
+ const description = exception.description ? ` - ${exception.description}` : "";
111
+ return `- ${type}${description}`;
112
+ }).join("\n");
113
+ }
114
+
115
+ function renderKind(doclet, owner) {
116
+ if (!owner) {
117
+ return "";
118
+ }
119
+
120
+ const target = `[<code>${owner}</code>](#${owner})`;
121
+ if (doclet.kind === "function" && doclet.scope === "instance") {
122
+ return `**Kind**: instance method of ${target}`;
123
+ }
124
+ if (doclet.kind === "function" && doclet.scope === "static") {
125
+ return `**Kind**: static method of ${target}`;
126
+ }
127
+ if (doclet.kind === "member" && doclet.scope === "static") {
128
+ return `**Kind**: static property of ${target}`;
129
+ }
130
+ return `**Kind**: ${doclet.kind} of ${target}`;
131
+ }
132
+
133
+ function renderEntry(doclet, owner) {
134
+ const anchor = getAnchor(doclet);
135
+ const sections = [`<a id="${anchor}" name="${anchor}"></a>`, `### ${getDisplayName(doclet)}`];
136
+
137
+ if (doclet.description) {
138
+ sections.push(doclet.description);
139
+ }
140
+
141
+ const kind = renderKind(doclet, owner);
142
+ if (kind) {
143
+ sections.push(kind);
144
+ }
145
+
146
+ if (doclet.type) {
147
+ sections.push(`**Type**: ${formatType(doclet.type)}`);
148
+ }
149
+
150
+ if (doclet.defaultvalue !== undefined) {
151
+ const value = typeof doclet.defaultvalue === "string"
152
+ ? doclet.defaultvalue
153
+ : JSON.stringify(doclet.defaultvalue);
154
+ sections.push(`**Default**: <code>${escapeCode(value)}</code>`);
155
+ }
156
+
157
+ if (doclet.readonly) {
158
+ sections.push("**Read-only**: Yes");
159
+ }
160
+
161
+ const returns = renderReturnValues(doclet.returns);
162
+ if (returns) {
163
+ sections.push(returns);
164
+ }
165
+
166
+ const parameters = renderParameterTable(doclet.params);
167
+ if (parameters) {
168
+ sections.push(parameters);
169
+ }
170
+
171
+ if (doclet.deprecated) {
172
+ sections.push(`**Deprecated**: ${typeof doclet.deprecated === "string" ? doclet.deprecated : "Yes"}`);
173
+ }
174
+
175
+ if (doclet.since) {
176
+ sections.push(`**Since**: ${doclet.since}`);
177
+ }
178
+
179
+ const exceptions = renderExceptions(doclet.exceptions);
180
+ if (exceptions) {
181
+ sections.push(`**Throws**:\n\n${exceptions}`);
182
+ }
183
+
184
+ const examples = renderExamples(doclet.examples);
185
+ if (examples) {
186
+ sections.push(`**Example**:\n\n${examples}`);
187
+ }
188
+
189
+ if (doclet.see && doclet.see.length) {
190
+ sections.push(`**See**: ${doclet.see.join(", ")}`);
191
+ }
192
+
193
+ return sections.join("\n\n");
194
+ }
195
+
196
+ function renderNamespaceIndex(namespace, symbols) {
197
+ const instanceSymbols = symbols.filter(symbol => symbol.kind === "function" && symbol.scope === "instance");
198
+ const staticSymbols = symbols.filter(symbol => !instanceSymbols.includes(symbol));
199
+ const lines = [`- [${namespace.name}](#${namespace.name}) : <code>object</code>`];
200
+
201
+ if (instanceSymbols.length) {
202
+ lines.push(" - _instance_");
203
+ for (const symbol of instanceSymbols) {
204
+ lines.push(` - [${getTocLabel(symbol)}](#${getAnchor(symbol)})`);
205
+ }
206
+ }
207
+
208
+ if (staticSymbols.length) {
209
+ lines.push(" - _static_");
210
+ for (const symbol of staticSymbols) {
211
+ lines.push(` - [${getTocLabel(symbol)}](#${getAnchor(symbol)})`);
212
+ }
213
+ }
214
+
215
+ return lines.join("\n");
216
+ }
217
+
218
+ function renderNamespace(namespace, symbols) {
219
+ const sections = [
220
+ `<a id="${namespace.name}" name="${namespace.name}"></a>`,
221
+ `## ${namespace.name} : <code>object</code>`,
222
+ "**Kind**: global namespace"
223
+ ];
224
+
225
+ if (namespace.description) {
226
+ sections.push(namespace.description);
227
+ }
228
+
229
+ const parameters = renderParameterTable(namespace.params);
230
+ if (parameters) {
231
+ sections.push(parameters);
232
+ }
233
+
234
+ for (const symbol of symbols) {
235
+ sections.push(renderEntry(symbol, namespace.name));
236
+ }
237
+
238
+ return sections.join("\n\n");
239
+ }
240
+
241
+ function getDoclets() {
242
+ const jsdocCliPath = path.join(rootDirectory, "node_modules", "jsdoc", "jsdoc.js");
243
+ const result = spawnSync(process.execPath, [jsdocCliPath, "--configure", "jsdoc.json", "--explain"], {
244
+ cwd: rootDirectory,
245
+ encoding: "utf8",
246
+ maxBuffer: 32 * 1024 * 1024
247
+ });
248
+
249
+ if (result.error) {
250
+ throw result.error;
251
+ }
252
+ if (result.status !== 0) {
253
+ throw new Error(result.stderr || `JSDoc exited with status ${result.status}`);
254
+ }
255
+
256
+ return JSON.parse(result.stdout);
257
+ }
258
+
259
+ function renderMarkdown(doclets) {
260
+ const namespaces = [];
261
+ const namespaceNames = new Set();
262
+
263
+ for (const doclet of doclets) {
264
+ if (doclet.kind === "namespace" && doclet.name && !namespaceNames.has(doclet.name)) {
265
+ namespaces.push(doclet);
266
+ namespaceNames.add(doclet.name);
267
+ }
268
+ }
269
+
270
+ const symbols = doclets.filter(doclet => {
271
+ const documented = doclet.comment && doclet.comment.trim() && !doclet.undocumented;
272
+ const supportedKind = namespaceKinds.has(doclet.kind);
273
+ const supportedOwner = namespaceNames.has(doclet.memberof) || (!doclet.memberof && doclet.kind === "function");
274
+ return documented && supportedKind && supportedOwner;
275
+ });
276
+
277
+ const symbolsByNamespace = new Map(namespaces.map(namespace => [namespace.name, []]));
278
+ const globalFunctions = [];
279
+ const uniqueSymbols = new Map();
280
+
281
+ for (const symbol of symbols) {
282
+ const key = symbol.longname || `${symbol.kind}:${symbol.name}`;
283
+ if (uniqueSymbols.has(key)) {
284
+ continue;
285
+ }
286
+ uniqueSymbols.set(key, symbol);
287
+
288
+ if (symbol.memberof) {
289
+ symbolsByNamespace.get(symbol.memberof).push(symbol);
290
+ } else {
291
+ globalFunctions.push(symbol);
292
+ }
293
+ }
294
+
295
+ const output = ["## Objects", namespaces.map(namespace =>
296
+ renderNamespaceIndex(namespace, symbolsByNamespace.get(namespace.name))
297
+ ).join("\n")];
298
+
299
+ if (globalFunctions.length) {
300
+ output.push("## Functions", globalFunctions.map(symbol =>
301
+ `- [${getTocLabel(symbol)}](#${getAnchor(symbol)})`
302
+ ).join("\n"));
303
+ }
304
+
305
+ for (const namespace of namespaces) {
306
+ output.push(renderNamespace(namespace, symbolsByNamespace.get(namespace.name)));
307
+ }
308
+
309
+ for (const symbol of globalFunctions) {
310
+ output.push(renderEntry(symbol));
311
+ }
312
+
313
+ const markdown = `${output.join("\n\n")}\n`;
314
+ const anchors = [...markdown.matchAll(/<a id="([^"]+)" name="\1"><\/a>/g)].map(match => match[1]);
315
+ const anchorSet = new Set(anchors);
316
+
317
+ for (const symbol of uniqueSymbols.values()) {
318
+ if (!anchorSet.has(getAnchor(symbol))) {
319
+ throw new Error(`Missing Markdown anchor for ${symbol.longname}`);
320
+ }
321
+ }
322
+
323
+ const linkTargets = [...markdown.matchAll(/\]\(#([^)]+)\)/g)].map(match => match[1]);
324
+ const missingTargets = linkTargets.filter(target => !anchorSet.has(target));
325
+ if (missingTargets.length) {
326
+ throw new Error(`Markdown link target has no ID anchor: ${missingTargets[0]}`);
327
+ }
328
+
329
+ if (namespaceNames.size !== namespaces.length) {
330
+ throw new Error("Namespace deduplication failed");
331
+ }
332
+
333
+ return { markdown, namespaceCount: namespaces.length, symbolCount: uniqueSymbols.size };
334
+ }
335
+
336
+ try {
337
+ const result = renderMarkdown(getDoclets());
338
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
339
+ fs.writeFileSync(outputPath, result.markdown, "utf8");
340
+ console.log(`Generated Markdown for ${result.namespaceCount} namespaces and ${result.symbolCount} API symbols.`);
341
+ } catch (error) {
342
+ console.error(error.message);
343
+ process.exitCode = 1;
344
+ }
package/vitest.config.mjs CHANGED
@@ -4,6 +4,11 @@ import { playwright } from '@vitest/browser-playwright';
4
4
  export default defineConfig({
5
5
  test: {
6
6
  globals: true,
7
+ api: {
8
+ host: '127.0.0.1',
9
+ port: 41315,
10
+ strictPort: false,
11
+ },
7
12
  silent: 'passed-only',
8
13
  restoreMocks: true,
9
14
  setupFiles: ['./test/vitest.setup.js'],