umberto 9.0.0 → 9.1.1

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.
Files changed (38) hide show
  1. package/CHANGELOG.md +21 -16
  2. package/package.json +8 -13
  3. package/scripts/filter/after-post-render/fix-code-samples.js +82 -18
  4. package/scripts/filter/after-post-render/gloria.js +27 -0
  5. package/scripts/filter/after-post-render/insert-error-codes.js +34 -26
  6. package/scripts/filter/after-post-render/validate-after-render.js +27 -6
  7. package/scripts/filter/after-render/process-svg.js +21 -0
  8. package/scripts/filter/before-post-render/gloria/render-post-render-pug-components.js +46 -18
  9. package/scripts/helper/u-extract-and-cache-title.js +27 -8
  10. package/scripts/helper/u-split-to-title-and-content.js +32 -8
  11. package/scripts/utils/gloria-after-post-render/append-copy-heading-buttons.js +119 -0
  12. package/scripts/utils/gloria-after-post-render/apply-design-doc-classes.js +157 -0
  13. package/scripts/utils/gloria-after-post-render/wrap-table-into-wrappers.js +25 -0
  14. package/scripts/utils/inline-svg.js +63 -94
  15. package/scripts/utils/spritesheet-svg.js +82 -102
  16. package/scripts/utils/toc.js +85 -31
  17. package/src/api-builder/api-builder.js +53 -40
  18. package/src/api-builder/build-page-worker.js +35 -0
  19. package/src/api-builder/classes/description-parser.js +77 -38
  20. package/src/data-converter/converters/jsduck2umberto.js +43 -15
  21. package/src/hexo/filter/project-locals.js +3 -0
  22. package/src/sdk-builder/get-sdk-sources.js +81 -44
  23. package/src/tasks/build-documentation.js +4 -0
  24. package/src/tasks/minify-html.js +1 -1
  25. package/src/tasks/validate-links-collect-worker.js +34 -0
  26. package/src/tasks/validate-links-worker.js +127 -0
  27. package/src/tasks/validate-links.js +61 -259
  28. package/themes/umberto/layout/gloria/_head/head.pug +3 -0
  29. package/themes/umberto/layout/gloria/_modules/index.pug +1 -0
  30. package/themes/umberto/layout/gloria/_modules/kapa/index.pug +0 -1
  31. package/themes/umberto/layout/gloria/_modules/sentry/index.pug +28 -0
  32. package/scripts/filter/after-post-render/gloria/append-copy-heading-buttons.js +0 -90
  33. package/scripts/filter/after-post-render/gloria/apply-design-doc-classes.js +0 -96
  34. package/scripts/filter/after-post-render/gloria/wrap-table-into-wrappers.js +0 -36
  35. package/scripts/filter/after-render/gloria/inline-svg.js +0 -14
  36. package/scripts/filter/after-render/gloria/spritesheet-svg.js +0 -14
  37. package/scripts/utils/apply-design-doc-classes.js +0 -82
  38. /package/src/tasks/{minify-worker.js → minify-html-worker.js} +0 -0
@@ -5,179 +5,159 @@
5
5
 
6
6
  'use strict';
7
7
 
8
- const cheerio = require( 'cheerio' );
9
8
  const fs = require( 'fs' );
10
9
  const path = require( 'path' );
10
+ const { parseDocument } = require( 'htmlparser2' );
11
+ const { Element, cloneNode } = require( 'domhandler' );
12
+ const { selectAll, selectOne } = require( 'css-select' );
13
+ const { getAttributeValue, removeElement, appendChild, prependChild } = require( 'domutils' );
11
14
 
12
- const HexoManager = require( '../../src/hexo-manager' );
15
+ module.exports = function spritesheetSvg( doc, themeDir ) {
16
+ const elements = selectAll( '[data-spritesheet-svg]', doc );
13
17
 
14
- /**
15
- * Cache for parsed SVG elements.
16
- */
17
- const SVG_CACHE = new Map();
18
-
19
- module.exports = function spritesheetSvg( str, isDocument = true ) {
20
- const $ = cheerio.load( str, null, isDocument );
21
- const elements = $( '[data-spritesheet-svg]' );
22
-
23
- // Skip if there are no elements with data-spritesheet-svg
18
+ // Skip if there are no elements with data-spritesheet-svg.
24
19
  if ( elements.length === 0 ) {
25
- return str;
20
+ return doc;
26
21
  }
27
22
 
28
- // Collect all unique SVG files to include in the spritesheet
23
+ // Collect all unique SVG files to include in the spritesheet.
29
24
  const svgFiles = new Map();
30
25
 
31
- // Helper function to extract SVG file information
32
- function collectSvgFileInfo( element ) {
33
- const svgPath = element.attr( 'data-spritesheet-svg' );
34
- const iconId = element.attr( 'data-spritesheet-id' );
26
+ // Helper function to extract SVG file information.
27
+ function collectSvgFileInfo( el ) {
28
+ const svgPath = getAttributeValue( el, 'data-spritesheet-svg' );
29
+ const iconId = getAttributeValue( el, 'data-spritesheet-id' );
35
30
 
36
- if ( !svgFiles.has( iconId ) ) {
31
+ if ( iconId && !svgFiles.has( iconId ) ) {
37
32
  svgFiles.set( iconId, svgPath );
38
33
  }
39
34
  }
40
35
 
41
- // Process normal SVG elements
42
- elements.each( function() {
43
- const $element = $( this );
44
-
45
- collectSvgFileInfo( $element );
46
- } );
47
-
48
- // Process and remove preload elements
49
- const preloadElements = $( '[data-spritesheet-preload="true"]' );
50
- preloadElements.each( function() {
51
- const $element = $( this );
36
+ // Process normal SVG elements.
37
+ for ( const el of elements ) {
38
+ collectSvgFileInfo( el );
39
+ }
52
40
 
53
- collectSvgFileInfo( $element );
54
- $element.remove();
55
- } );
41
+ // Process and remove preload elements.
42
+ for ( const el of selectAll( '[data-spritesheet-preload="true"]', doc ) ) {
43
+ collectSvgFileInfo( el );
44
+ removeElement( el );
45
+ }
56
46
 
57
47
  if ( svgFiles.size === 0 ) {
58
- return str;
48
+ return doc;
59
49
  }
60
50
 
61
- // Create spritesheet and get viewBox data
62
- const { spritesheetElement, iconViewBoxes } = createSpritesheet( $, svgFiles );
51
+ // Create spritesheet and get viewBox data.
52
+ const { spritesheetElement, iconViewBoxes } = createSpritesheet( svgFiles, themeDir );
63
53
 
64
- // Add spritesheet to the beginning of the body
65
- if ( $( 'body' ).length ) {
66
- $( 'body' ).prepend( spritesheetElement );
67
- }
54
+ // Add spritesheet to the beginning of the body.
55
+ prependChild( selectOne( 'body', doc ), spritesheetElement );
68
56
 
69
- // Set viewBox for original SVG elements
70
- elements.each( function() {
71
- const element = $( this );
72
- const iconId = element.attr( 'data-spritesheet-id' );
57
+ // Set viewBox for original SVG elements.
58
+ for ( const el of elements ) {
59
+ const iconId = getAttributeValue( el, 'data-spritesheet-id' );
73
60
  const viewBox = iconViewBoxes.get( iconId );
74
61
 
75
- if ( viewBox && this.tagName.toLowerCase() === 'svg' ) {
76
- element.attr( 'viewBox', viewBox );
62
+ if ( viewBox && el.name === 'svg' ) {
63
+ el.attribs.viewBox = viewBox;
77
64
  }
78
- } );
65
+ }
79
66
 
80
- $( '[data-spritesheet-svg]' ).removeAttr( 'data-spritesheet-svg' );
81
- $( '[data-spritesheet-id]' ).removeAttr( 'data-spritesheet-id' );
67
+ for ( const el of selectAll( '[data-spritesheet-svg]', doc ) ) {
68
+ delete el.attribs[ 'data-spritesheet-svg' ];
69
+ }
70
+
71
+ for ( const el of selectAll( '[data-spritesheet-id]', doc ) ) {
72
+ delete el.attribs[ 'data-spritesheet-id' ];
73
+ }
82
74
 
83
- return $.html();
75
+ return doc;
84
76
  };
85
77
 
86
78
  /**
87
79
  * Creates an SVG spritesheet from the provided SVG files.
88
80
  * Returns an object containing the spritesheet element and a map of icon IDs to their viewBox strings.
89
81
  */
90
- function createSpritesheet( $, svgFiles ) {
82
+ function createSpritesheet( svgFiles, themeDir ) {
91
83
  const symbols = [];
92
84
  const iconViewBoxes = new Map();
93
85
 
94
86
  for ( const [ iconId, svgPath ] of svgFiles.entries() ) {
95
- const absolutePath = path.join( HexoManager.hexo.theme_dir, svgPath );
87
+ const absolutePath = path.join( themeDir, svgPath );
96
88
 
97
89
  if ( !fs.existsSync( absolutePath ) ) {
98
90
  console.warn( `SVG file not found: ${ absolutePath }` );
99
91
  continue;
100
92
  }
101
93
 
102
- // Check if we have the SVG in cache
103
- if ( !SVG_CACHE.has( absolutePath ) ) {
104
- try {
105
- const svgContent = fs.readFileSync( absolutePath, 'utf8' );
106
- const $svg = cheerio.load( svgContent, { xmlMode: true } )( 'svg' );
107
- SVG_CACHE.set( absolutePath, $svg );
108
- } catch ( error ) {
109
- console.error( `Error reading SVG file: ${ absolutePath }`, error );
110
- continue;
111
- }
112
- }
94
+ const svgContent = fs.readFileSync( absolutePath, 'utf8' );
95
+ const svgDoc = parseDocument( svgContent, { xmlMode: true } );
96
+ const svgRoot = selectOne( 'svg', svgDoc );
113
97
 
114
- const $svg = SVG_CACHE.get( absolutePath ).clone();
98
+ if ( !svgRoot ) {
99
+ console.warn( `No <svg> root element found in: ${ absolutePath }` );
100
+ continue;
101
+ }
115
102
 
116
- // Create symbol element from SVG
117
- const $symbol = $( '<symbol></symbol>' );
118
- $symbol.attr( 'id', iconId );
103
+ // Create <symbol> element from SVG.
104
+ const symbolEl = new Element( 'symbol', { id: iconId } );
119
105
 
120
- // Copy viewBox and other relevant attributes
106
+ // Compute and set viewBox on <symbol>, also store for consumers.
121
107
  let viewBoxValue;
122
- if ( $svg.attr( 'viewBox' ) ) {
123
- viewBoxValue = $svg.attr( 'viewBox' );
124
- } else if ( $svg.attr( 'width' ) && $svg.attr( 'height' ) ) {
125
- viewBoxValue = `0 0 ${ $svg.attr( 'width' ) } ${ $svg.attr( 'height' ) }`;
108
+
109
+ if ( svgRoot.attribs.viewBox ) {
110
+ viewBoxValue = svgRoot.attribs.viewBox;
111
+ } else if ( svgRoot.attribs.width && svgRoot.attribs.height ) {
112
+ viewBoxValue = `0 0 ${ svgRoot.attribs.width } ${ svgRoot.attribs.height }`;
126
113
  }
127
114
 
128
115
  if ( viewBoxValue ) {
129
- $symbol.attr( 'viewBox', viewBoxValue );
116
+ symbolEl.attribs.viewBox = viewBoxValue;
130
117
  iconViewBoxes.set( iconId, viewBoxValue );
131
118
  }
132
119
 
133
- // Copy all child elements from SVG to symbol
134
- $svg.children().each( function() {
135
- const $child = $( this );
136
- // Skip title elements as they'll be provided by the consuming SVG
137
- if ( $child[ 0 ].tagName.toLowerCase() !== 'title' ) {
138
- $symbol.append( $child.clone() );
120
+ // Copy element children (<title> is skipped).
121
+ for ( const child of svgRoot.children ) {
122
+ if ( child && child.type === 'tag' && child.name.toLowerCase() !== 'title' ) {
123
+ appendChild( symbolEl, cloneNode( child, true ) );
139
124
  }
140
- } );
125
+ }
141
126
 
142
- symbols.push(
143
- resetFillColor( $, $symbol )
144
- );
127
+ symbols.push( resetFillColor( symbolEl ) );
145
128
  }
146
129
 
147
- // Create the spritesheet container
148
- const $spritesheet = $( '<svg></svg>' );
149
- $spritesheet.attr( {
130
+ // Create the spritesheet container.
131
+ const spritesheet = new Element( 'svg', {
150
132
  'class': 'svg-spritesheet',
151
133
  'xmlns': 'http://www.w3.org/2000/svg',
152
134
  'style': 'display: none;',
153
135
  'aria-hidden': 'true'
154
136
  } );
155
137
 
156
- // Add all symbols to the spritesheet
157
- symbols.forEach( $symbol => {
158
- $spritesheet.append( $symbol );
159
- } );
138
+ // Add all symbols to the spritesheet.
139
+ for ( const symbol of symbols ) {
140
+ appendChild( spritesheet, symbol );
141
+ }
160
142
 
161
143
  return {
162
- spritesheetElement: $spritesheet,
144
+ spritesheetElement: spritesheet,
163
145
  iconViewBoxes
164
146
  };
165
147
  }
166
148
 
167
149
  /**
168
- * Resets the fill color of an SVG element to 'currentColor'.
169
- * This is useful for ensuring that the SVG inherits the text color from its parent.
150
+ * Resets the fill color of an SVG container (e.g., <symbol>) to 'currentColor'.
151
+ * Ensures descendants that explicitly set 'fill' will inherit from text color in consumers.
170
152
  */
171
- function resetFillColor( $, $svg ) {
172
- // Replace all fill attributes with the specified color.
173
- $svg.find( '[fill]' ).each( function() {
174
- $( this ).attr( 'fill', 'currentColor' );
175
- } );
153
+ function resetFillColor( el ) {
154
+ for ( const node of selectAll( '[fill]', el ) ) {
155
+ node.attribs.fill = 'currentColor';
156
+ }
176
157
 
177
- // Also check the root SVG for fill attribute.
178
- if ( $svg.attr( 'fill' ) ) {
179
- $svg.attr( 'fill', 'currentColor' );
158
+ if ( el.attribs?.fill !== undefined ) {
159
+ el.attribs.fill = 'currentColor';
180
160
  }
181
161
 
182
- return $svg;
162
+ return el;
183
163
  }
@@ -5,10 +5,14 @@
5
5
 
6
6
  'use strict';
7
7
 
8
- const cheerio = require( 'cheerio' );
8
+ const { parseDocument } = require( 'htmlparser2' );
9
+ const { Element, Text, cloneNode } = require( 'domhandler' );
10
+ const { default: render } = require( 'dom-serializer' );
11
+ const { selectAll } = require( 'css-select' );
12
+ const { appendChild, getAttributeValue, removeElement, textContent } = require( 'domutils' );
9
13
 
10
14
  module.exports = function toc( data, options = {} ) {
11
- const $ = cheerio.load( data, null, false );
15
+ const doc = parseDocument( data );
12
16
 
13
17
  let usedHeadings = [ 'h2', 'h3', 'h4', 'h5', 'h6' ];
14
18
 
@@ -16,56 +20,106 @@ module.exports = function toc( data, options = {} ) {
16
20
  usedHeadings = usedHeadings.slice( 0, options.tocLimit );
17
21
  }
18
22
 
19
- usedHeadings = usedHeadings.join( ',' );
23
+ const selector = usedHeadings.join( ',' );
24
+
25
+ const headings = selectAll( selector, doc ).filter( heading => {
26
+ return !hasAncestorWithClassIn( heading, [ 'live-snippet', 'collapsing-list__item' ] );
27
+ } );
20
28
 
21
- const headings = $( usedHeadings )
22
- .filter( function() {
23
- return !$( this ).parents( '.live-snippet, .collapsing-list__item' ).length;
24
- } );
25
29
  const className = options.class || 'secondary-navigation';
26
30
 
27
31
  if ( !headings.length ) {
28
32
  return '';
29
33
  }
30
34
 
31
- const $r = cheerio.load( `<nav class=${ className }><h3>Table of contents</h3></nav>`, null, false ); // result object
32
- const tocLastLevels = [ $r( 'nav' ), 0, 0, 0, 0, 0, 0 ];
33
-
34
- headings.each( function() {
35
- const hLevel = Number( this.name[ 1 ] );
36
- const id = $r( this ).attr( 'id' );
37
- const text = $r( this ).find( '.headerlink' ).remove().end().text().trim()
38
- // Replace the chevrons with their HTML escape characters to avoid rendering HTML in ToC.
39
- .replace( /</g, '&lt;' )
40
- .replace( />/g, '&gt;' );
35
+ const nav = new Element( 'nav', { class: className } );
36
+ const h3 = new Element( 'h3', {} );
37
+ appendChild( h3, new Text( 'Table of contents' ) );
38
+ appendChild( nav, h3 );
39
+
40
+ const tocLastLevels = [ nav, null, null, null, null, null, null ];
41
+ let lastLi = null;
42
+
43
+ for ( const heading of headings ) {
44
+ const hLevel = Number( heading.name[ 1 ] ); // 'h2' -> 2
45
+ const text = getHeadingText( heading ).trim();
46
+ const li = new Element( 'li', {} );
47
+ const a = new Element( 'a', {
48
+ class: 'a11y-focusable',
49
+ href: '#' + getAttributeValue( heading, 'id' ),
50
+ title: text
51
+ } );
41
52
 
42
- const newItem = `<li><a class="a11y-focusable" href="#${ id }" title="${ text }">${ text }</a></li>`;
53
+ appendChild( a, new Text( text ) );
54
+ appendChild( li, a );
43
55
 
44
56
  if ( tocLastLevels[ hLevel ] ) {
45
- // If there already is a parent node for current heading level, append the new toc item there.
46
- tocLastLevels[ hLevel ].append( newItem );
57
+ // Append to existing <ol> for this heading level.
58
+ appendChild( tocLastLevels[ hLevel ], li );
47
59
  } else {
48
- // If there's no parent node for current heading level, create it and append.
49
- if ( $r( 'li' ).length ) {
50
- $r( 'li' ).last().append( `<ol>${ newItem }</ol>` );
60
+ // No <ol> for this level yet create it and attach to last <li> or <nav>.
61
+ const ol = new Element( 'ol', {} );
62
+ appendChild( ol, li );
63
+
64
+ if ( lastLi ) {
65
+ appendChild( lastLi, ol );
51
66
  } else {
52
- $r( 'nav' ).append( `<ol>${ newItem }</ol>` );
67
+ appendChild( nav, ol );
53
68
  }
54
69
 
55
- // Assign new created <ol> element to the helper array.
56
- tocLastLevels[ hLevel ] = $r( 'ol' ).last();
70
+ tocLastLevels[ hLevel ] = ol;
57
71
  }
58
72
 
59
- // After adding new toc item, lower heading levels in helper array must be cleared.
60
- // If e.g. level 2 <ol> was added, then level 3 nd lower <ol>s are no longer needed because they belong to upper toc nodes.
73
+ lastLi = li;
74
+
75
+ // Clear deeper levels; they no longer apply after adding an item at this level.
61
76
  clearLowerLevels( tocLastLevels, hLevel );
62
- } );
77
+ }
63
78
 
64
- return $r.html();
79
+ return render( nav );
65
80
  };
66
81
 
67
82
  function clearLowerLevels( levels, current ) {
68
83
  for ( let i = current + 1; i < levels.length; i++ ) {
69
- levels[ i ] = 0;
84
+ levels[ i ] = null;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Returns true if `node` has an ancestor with any of the given classes.
90
+ */
91
+ function hasAncestorWithClassIn( node, classNames ) {
92
+ for ( let p = node && node.parent; p; p = p.parent ) {
93
+ const cls = getAttributeValue( p, 'class' );
94
+
95
+ if ( !cls ) {
96
+ continue;
97
+ }
98
+
99
+ const classes = cls.split( /\s+/ ).filter( Boolean );
100
+
101
+ if ( classes.some( c => classNames.includes( c ) ) ) {
102
+ return true;
103
+ }
70
104
  }
105
+
106
+ return false;
107
+ }
108
+
109
+ /**
110
+ * Returns the heading text without content from descendants with class "headerlink".
111
+ */
112
+ function getHeadingText( root ) {
113
+ if ( !root ) {
114
+ return '';
115
+ }
116
+
117
+ // Clone so we don't mutate the original AST.
118
+ const clone = cloneNode( root, true );
119
+
120
+ for ( const node of selectAll( '.headerlink', clone ) ) {
121
+ removeElement( node );
122
+ }
123
+
124
+ return textContent( clone );
71
125
  }
@@ -7,7 +7,10 @@
7
7
 
8
8
  const upath = require( 'upath' );
9
9
  const fs = require( 'fs-extra' );
10
- const cheerio = require( 'cheerio' );
10
+ const { parseDocument } = require( 'htmlparser2' );
11
+ const { selectAll } = require( 'css-select' );
12
+ const { textContent, removeElement } = require( 'domutils' );
13
+ const { default: TinyPool } = require( 'tinypool' );
11
14
  const FileNameManager = require( './classes/file-name-manager' );
12
15
  const HtmlFile = require( './classes/html-file' );
13
16
  const DocDataFactory = require( './classes/doc-data-factory' );
@@ -21,11 +24,8 @@ const githubUrlUtils = require( '../helpers/github-url' );
21
24
  const getReportIssueWidgetUrl = require( '../../scripts/utils/getreportissuewidgeturl' );
22
25
  const getPageGroupHelper = require( '../../src/hexo/helper/get-page-group' );
23
26
  const findTargetDoclet = require( './utils/findtargetdoclet' );
24
- const writeHtmlFiles = require( '../../src/tasks/write-html-files' );
25
27
 
26
28
  const { randomId } = require( '../../scripts/utils/random-id' );
27
- const spritesheetSvg = require( '../../scripts/utils/spritesheet-svg' );
28
- const inlineSvg = require( '../../scripts/utils/inline-svg' );
29
29
  const parseHref = require( '../../scripts/utils/parse-href' );
30
30
  const hexoManager = require( '../hexo-manager' );
31
31
 
@@ -204,12 +204,18 @@ module.exports = class ApiBuilder {
204
204
  this._buildNavTree( concated );
205
205
  this._filterHtml = this._tmplCol.renderTemplate( '_partial/filter' );
206
206
 
207
- await this._getPages( modulesData, 'module' );
208
- await this._getPages( classesData, 'class' );
209
- await this._getPages( interfacesData, 'interface' );
210
- await this._getPages( typedefsData, 'typedef' );
211
- await this._getPages( mixinsData, 'mixin' );
212
- await this._getPages( namespacesData, 'namespace' );
207
+ const pool = new TinyPool( {
208
+ filename: require.resolve( './build-page-worker.js' )
209
+ } );
210
+
211
+ await this._getPages( pool, modulesData, 'module' );
212
+ await this._getPages( pool, classesData, 'class' );
213
+ await this._getPages( pool, interfacesData, 'interface' );
214
+ await this._getPages( pool, typedefsData, 'typedef' );
215
+ await this._getPages( pool, mixinsData, 'mixin' );
216
+ await this._getPages( pool, namespacesData, 'namespace' );
217
+
218
+ await pool.destroy();
213
219
 
214
220
  this._renderedFiles = [];
215
221
 
@@ -379,34 +385,30 @@ module.exports = class ApiBuilder {
379
385
  }
380
386
  }
381
387
 
382
- this._navTreeHtml = spritesheetSvg( this._tmplCol.renderTemplate(
383
- '_partial/navtree',
384
- {
385
- projectLocals: {
386
- apiTree: this._navTree
387
- }
388
+ this._navTreeHtml = this._tmplCol.renderTemplate( '_partial/navtree', {
389
+ projectLocals: {
390
+ apiTree: this._navTree
388
391
  }
389
- ), false );
392
+ } );
390
393
  }
391
394
 
392
395
  /**
393
396
  * Renders HTML of each API docs page and creates an HtmlFile instance for that page.
394
397
  */
395
- async _getPages( items, type ) {
396
- const views = [];
398
+ async _getPages( pool, items, type ) {
399
+ const jobs = [];
400
+ const groups = this._groups.map( g => {
401
+ g._url = upath.join( this._BASE_PATH, g.slug, 'index.html' );
397
402
 
398
- for ( const i of items ) {
399
- const data = this._getPageData( i, type );
400
- const filename = this._fileNameManager.getFilename( i.longname );
403
+ return g;
404
+ } );
405
+
406
+ for ( const item of items ) {
407
+ const data = this._getPageData( item, type );
408
+ const filename = this._fileNameManager.getFilename( item.longname );
401
409
 
402
410
  if ( filename ) {
403
411
  const filePath = upath.join( this._destinationDir, filename );
404
- const groups = this._groups.map( g => {
405
- g._url = upath.join( this._BASE_PATH, g.slug, 'index.html' );
406
-
407
- return g;
408
- } );
409
-
410
412
  const shortModulePath = getShortModulePath( data.longname );
411
413
  const split = splitLongname( data.longname );
412
414
  const splitParts = [ split.packageName, ...split.directoryNames ];
@@ -420,10 +422,20 @@ module.exports = class ApiBuilder {
420
422
  title = `${ capitalize( data.kind ) } ${ capitalize( split.name ) }`;
421
423
  }
422
424
 
423
- const $ = data.description ? cheerio.load( data.description.content, null, false ) : cheerio.load( '', null, false );
424
- const html = $.root();
425
- html.find( 'pre' ).remove();
426
- const desc = html.text().trim().replace( /\n/g, ' ' ).replace( / +/g, ' ' );
425
+ let desc = '';
426
+
427
+ if ( data.description ) {
428
+ const descDoc = parseDocument( data.description.content );
429
+
430
+ for ( const pre of selectAll( 'pre', descDoc ) ) {
431
+ removeElement( pre );
432
+ }
433
+
434
+ desc = textContent( descDoc )
435
+ .trim()
436
+ .replace( /\n/g, ' ' )
437
+ .replace( / +/g, ' ' );
438
+ }
427
439
 
428
440
  // eslint-disable-next-line @stylistic/max-len
429
441
  const metaDescription = `${ this._projectName } API Documentation. The ${ capitalize( data.kind ) } ${ data.kind === 'module' ? data.longname.replace( 'module:', '' ) : capitalize( split.name ) }.${ desc.length ? ' ' + desc : '' }`;
@@ -483,17 +495,18 @@ module.exports = class ApiBuilder {
483
495
  filePath
484
496
  );
485
497
 
486
- let content = inlineSvg( view.content );
487
-
488
- content = spritesheetSvg( content );
489
-
490
- view.content = content.replaceAll( '%%_RENDERED_NAV_TREE_%%', this._navTreeHtml );
491
-
492
- await writeHtmlFiles( [ view ] );
498
+ jobs.push(
499
+ pool.run( {
500
+ content: view.content,
501
+ navTreeHtml: this._navTreeHtml,
502
+ outputPath: upath.join( view.dirname, view.basename ),
503
+ themeDir: hexoManager.hexo.theme_dir
504
+ } )
505
+ );
493
506
  }
494
507
  }
495
508
 
496
- return views;
509
+ await Promise.all( jobs );
497
510
 
498
511
  /**
499
512
  * Function trim text to the last sentence.
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @license Copyright (c) 2017-2025, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md.
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const fs = require( 'fs' );
9
+ const upath = require( 'upath' );
10
+ const { parseDocument } = require( 'htmlparser2' );
11
+ const { default: render } = require( 'dom-serializer' );
12
+ const inlineSvg = require( '../../scripts/utils/inline-svg' );
13
+ const spritesheetSvg = require( '../../scripts/utils/spritesheet-svg' );
14
+
15
+ module.exports = function( {
16
+ content,
17
+ navTreeHtml,
18
+ outputPath,
19
+ themeDir
20
+ } ) {
21
+ let doc = parseDocument( content );
22
+ doc = inlineSvg( doc, themeDir );
23
+ doc = spritesheetSvg( doc, themeDir );
24
+ content = render( doc ).replaceAll( '%%_RENDERED_NAV_TREE_%%', navTreeHtml );
25
+
26
+ const dirname = upath.dirname( outputPath );
27
+
28
+ try {
29
+ fs.accessSync( dirname );
30
+ } catch {
31
+ fs.mkdirSync( dirname, { recursive: true } );
32
+ }
33
+
34
+ fs.writeFileSync( outputPath, content, 'utf-8' );
35
+ };