umberto 8.3.4 → 8.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,46 +1,49 @@
1
1
  Changelog
2
2
  =========
3
3
 
4
- ## [8.3.4](https://github.com/cksource/umberto/compare/v8.3.3...v8.3.4) (October 20, 2025)
4
+ ## [8.4.0](https://github.com/cksource/umberto/compare/v8.3.5...v8.4.0) (October 30, 2025)
5
5
 
6
- ### Other changes
6
+ ### Features
7
7
 
8
- * Bump dependencies (including `hexo` to version `8.0.0`).
8
+ * Minify output HTML to reduce file size and improve load performance.
9
9
 
10
+ The output is automatically minified during production builds to remove unnecessary whitespace and comments.
10
11
 
11
- ## [8.3.3](https://github.com/cksource/umberto/compare/v8.3.2...v8.3.3) (October 15, 2025)
12
+ Minification is **disabled when using the `--dev` modifier**, ensuring the generated HTML remains readable for easier debugging and inspection.
12
13
 
13
- ### Bug fixes
14
+ ### Other changes
14
15
 
15
- * Link validator should not fail on links containing the word "latest", if they are not part of a version number.
16
- * Fixed links being unstyled in Kapa.ai answers.
17
- * Fix broken page sidebar layout on `959px` resolution.
16
+ * Umberto reads a project configuration once and passes it through the pipeline. Thanks to that, hooks can modify it, which affects a project's build.
18
17
 
19
18
 
20
- ## [8.3.2](https://github.com/cksource/umberto/compare/v8.3.1...v8.3.2) (September 16, 2025)
19
+ ## [8.3.5](https://github.com/cksource/umberto/compare/v8.3.4...v8.3.5) (October 29, 2025)
21
20
 
22
21
  ### Other changes
23
22
 
24
- * Update to TypeScript 5.3.
23
+ * Hook scripts now receive the full project configuration object when executed.
25
24
 
26
25
 
27
- ## [8.3.1](https://github.com/cksource/umberto/compare/v8.3.0...v8.3.1) (September 4, 2025)
26
+ ## [8.3.4](https://github.com/cksource/umberto/compare/v8.3.3...v8.3.4) (October 20, 2025)
28
27
 
29
- ### Bug fixes
28
+ ### Other changes
29
+
30
+ * Bump dependencies (including `hexo` to version `8.0.0`).
30
31
 
31
- * Fixed link styles not being applied to followup questions in `Kapa.ai` integration.
32
32
 
33
- ### Other changes
33
+ ## [8.3.3](https://github.com/cksource/umberto/compare/v8.3.2...v8.3.3) (October 15, 2025)
34
+
35
+ ### Bug fixes
34
36
 
35
- * Increased size of the modal in `Kapa.ai` integration.
37
+ * Link validator should not fail on links containing the word "latest", if they are not part of a version number.
38
+ * Fixed links being unstyled in Kapa.ai answers.
39
+ * Fix broken page sidebar layout on `959px` resolution.
36
40
 
37
41
 
38
- ## [8.3.0](https://github.com/cksource/umberto/compare/v8.2.0...v8.3.0) (September 2, 2025)
42
+ ## [8.3.2](https://github.com/cksource/umberto/compare/v8.3.1...v8.3.2) (September 16, 2025)
39
43
 
40
- ### Features
44
+ ### Other changes
41
45
 
42
- * Added custom styling for the [Kapa.ai](https://www.kapa.ai/) service.
43
- * The source script of the [Kapa.ai](https://www.kapa.ai/) service must be defined in the `kapa.widgetOptions.src` key in the Umberto configuration.
46
+ * Update to TypeScript 5.3.
44
47
 
45
48
  ---
46
49
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "umberto",
3
- "version": "8.3.4",
3
+ "version": "8.4.0",
4
4
  "description": "CKSource Documentation builder",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -17,6 +17,7 @@
17
17
  "@babel/core": "^7.18.10",
18
18
  "@babel/preset-env": "^7.18.10",
19
19
  "@ckeditor/jsdoc-plugins": "^43.0.0",
20
+ "@minify-html/node": "^0.17.1",
20
21
  "babel-loader": "^10.0.0",
21
22
  "cheerio": "^1.0.0",
22
23
  "escape-string-regexp": "^4.0.0",
@@ -28,8 +29,8 @@
28
29
  "hexo-generator-category": "^2.0.0",
29
30
  "hexo-generator-index": "^4.0.0",
30
31
  "hexo-generator-tag": "^2.0.0",
31
- "hexo-renderer-pug": "^3.0.0",
32
32
  "hexo-renderer-markdown-it-plus": "^1.0.5",
33
+ "hexo-renderer-pug": "^3.0.0",
33
34
  "htmlparser2": "^10.0.0",
34
35
  "javascript-stringify": "^2.1.0",
35
36
  "jquery": "~3.7.1",
@@ -46,6 +47,7 @@
46
47
  "sass": "^1.54.0",
47
48
  "shiki": "^3.4.0",
48
49
  "sitemap": "^8.0.0",
50
+ "tinypool": "^2.0.0",
49
51
  "tippy.js": "^6.3.7",
50
52
  "tree-model": "^1.0.7",
51
53
  "upath": "^2.0.1",
@@ -71,6 +73,6 @@
71
73
  ]
72
74
  },
73
75
  "hexo": {
74
- "version": "7.3.0"
76
+ "version": "8.0.0"
75
77
  }
76
78
  }
@@ -26,6 +26,7 @@ const createSitemapStep = require( './create-sitemap-step' );
26
26
  const buildSnippets = require( './build-snippets' );
27
27
  const copyProjectIcons = require( './copy-project-icons' );
28
28
  const executeHooks = require( './execute-hooks' );
29
+ const minifyHtml = require( './minify-html' );
29
30
  const umbertoVersion = require( '../../package.json' ).version;
30
31
 
31
32
  const { parseLinks } = require( '../../scripts/utils/parselinks' );
@@ -49,6 +50,7 @@ const getFilePatternsToProcess = require( '../helpers/get-file-patterns-to-proce
49
50
  * clean option can be used to clear the build directory or not.
50
51
  * @param {Boolean} options.dev If true, it indicates a local build, skips minification, reuses already built files, allows to skip webpack.
51
52
  * @param {Boolean} options.skipApi Skip rendering API docs.
53
+ * @param {Boolean} options.skipSdk Skip building SDK docs.
52
54
  * @param {Boolean} options.skipLiveSnippets Skip building live code snippets.
53
55
  * @param {Array.<String>|String} options.extraStyles Path/s to extra css.
54
56
  * @param {Array.<String>|String} options.extraScripts Path/s to extra js.
@@ -69,6 +71,7 @@ module.exports = options => {
69
71
  clean = !options.dev,
70
72
  dev = false,
71
73
  skipApi = false,
74
+ skipSdk = false,
72
75
  skipLiveSnippets = false,
73
76
  extraStyles = [],
74
77
  extraScripts = [],
@@ -127,13 +130,14 @@ module.exports = options => {
127
130
  } );
128
131
  } )
129
132
  .then( async () => {
130
- const configs = await getProjectConfigs( rootPath, projectPaths );
133
+ const configs = await getProjectConfigs( rootPath, projectPaths, { skipLiveSnippets } );
131
134
 
132
135
  return executeHooks( configs, 'beforeHexo' );
133
136
  } )
134
137
  .then( () => {
135
138
  return buildProjects( rootPath, projectPaths, {
136
139
  skipApi,
140
+ skipSdk,
137
141
  skipLiveSnippets,
138
142
  dev,
139
143
  docSearch: mainConfig.docsearch,
@@ -205,6 +209,16 @@ module.exports = options => {
205
209
 
206
210
  return Promise.resolve();
207
211
  } )
212
+ // Minify HTML files.
213
+ .then( async () => {
214
+ if ( options.dev ) {
215
+ return Promise.resolve();
216
+ }
217
+
218
+ const outputDir = upath.relative( process.cwd(), hexoManager.getPublicDir() );
219
+
220
+ return minifyHtml( outputDir );
221
+ } )
208
222
  // Links validation.
209
223
  .then( async () => {
210
224
  const projectConfigs = await getProjectConfigs( rootPath, projectPaths, {
@@ -233,12 +247,12 @@ module.exports = options => {
233
247
  } );
234
248
  } )
235
249
  .then( async () => {
236
- const projectConfigs = await getProjectConfigs( rootPath, projectPaths );
250
+ const projectConfigs = await getProjectConfigs( rootPath, projectPaths, { skipLiveSnippets } );
237
251
 
238
252
  return createSitemapStep( { projectConfigs, verbose, mainConfig } );
239
253
  } )
240
254
  .then( async () => {
241
- const configs = await getProjectConfigs( rootPath, projectPaths );
255
+ const configs = await getProjectConfigs( rootPath, projectPaths, { skipLiveSnippets } );
242
256
 
243
257
  return executeHooks( configs, 'afterHexo' );
244
258
  } )
@@ -396,15 +410,9 @@ async function getProjectConfigs( rootPath, projectPaths, options = {} ) {
396
410
  const promises = projectPaths.map( async pPath => {
397
411
  const projectRootPath = upath.join( rootPath, pPath );
398
412
 
399
- return Object.assign(
400
- {},
401
- {
402
- ...await getProjectConfig( projectRootPath, {
403
- skipLiveSnippets: options.skipLiveSnippets
404
- } ),
405
- projectRootPath
406
- }
407
- );
413
+ return getProjectConfig( projectRootPath, {
414
+ skipLiveSnippets: options.skipLiveSnippets
415
+ } );
408
416
  } );
409
417
 
410
418
  return Promise.all( promises );
@@ -34,7 +34,7 @@ module.exports = async function executeHooks( configs, hookName ) {
34
34
  }
35
35
 
36
36
  for ( const scriptPath of config.hooks[ hookName ] ) {
37
- await processSingleHook( upath.dirname( config.__configPath ), scriptPath );
37
+ await processSingleHook( config, scriptPath );
38
38
  }
39
39
  }
40
40
 
@@ -48,17 +48,18 @@ module.exports = async function executeHooks( configs, hookName ) {
48
48
  };
49
49
 
50
50
  /**
51
- * @param {String} configPath
51
+ * @param {Object} config
52
52
  * @param {String} scriptPath
53
53
  * @return {Promise}
54
54
  */
55
- async function processSingleHook( configPath, scriptPath ) {
55
+ async function processSingleHook( config, scriptPath ) {
56
+ const configPath = upath.dirname( config.__configPath );
56
57
  const callbackAbsolutePath = upath.join( configPath, scriptPath );
57
58
 
58
59
  try {
59
60
  const callback = await importModule( callbackAbsolutePath );
60
61
 
61
- await callback();
62
+ await callback( config );
62
63
  } catch ( error ) {
63
64
  // Store a path to the executed file.
64
65
  error._filePath = scriptPath;
@@ -32,6 +32,7 @@ module.exports = async ( rootPath, options = {} ) => {
32
32
 
33
33
  const config = getConfigurationFile( configPath );
34
34
 
35
+ config.projectRootPath = rootPath;
35
36
  config.__configPath = configPath;
36
37
 
37
38
  // Get the `realImportPath()` function. It displays an import path of a class below the class title.
@@ -0,0 +1,54 @@
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 { join } = require( 'path' );
9
+ const { globSync } = require( 'fs' );
10
+ const { styleText } = require( 'util' );
11
+ const { default: TinyPool } = require( 'tinypool' );
12
+
13
+ /**
14
+ * Minifies all HTML files under outputDir in parallel using tinypool.
15
+ *
16
+ * @param {string} outputDir - base directory to search for *.html files
17
+ */
18
+ module.exports = async function minifyHtml( outputDir ) {
19
+ const files = globSync( join( outputDir, '**', 'api', '**', '*.html' ) );
20
+
21
+ const pool = new TinyPool( {
22
+ runtime: 'child_process',
23
+ filename: require.resolve( './minify-worker.js' )
24
+ } );
25
+
26
+ try {
27
+ console.log( 'Started HTML minification.' );
28
+
29
+ const results = await Promise.allSettled(
30
+ files.map( async file => {
31
+ try {
32
+ return await pool.run( file );
33
+ } catch ( err ) {
34
+ err.file = file;
35
+ throw err;
36
+ }
37
+ } )
38
+ );
39
+
40
+ const failed = results.filter( r => r.status === 'rejected' );
41
+
42
+ if ( failed.length > 0 ) {
43
+ failed.forEach( ( { reason } ) => {
44
+ console.error( styleText( 'redBright', `Minification failed for "${ reason.file }": ${ reason.message }` ) );
45
+ } );
46
+
47
+ throw new Error( `Minification failed for ${ failed.length } file(s).` );
48
+ }
49
+ } finally {
50
+ await pool.destroy();
51
+
52
+ console.log( 'Finished HTML minification.' );
53
+ }
54
+ };
@@ -0,0 +1,27 @@
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 { readFileSync, writeFileSync } = require( 'fs' );
9
+ const minifier = require( '@minify-html/node' );
10
+
11
+ /**
12
+ * Minifies HTML content.
13
+ *
14
+ * @param {string} file - Path to the HTML file to minify.
15
+ * @returns {void}
16
+ */
17
+ module.exports = function( file ) {
18
+ const html = readFileSync( file );
19
+
20
+ const minified = minifier.minify( html, {
21
+ keep_comments: true,
22
+ minify_css: true,
23
+ minify_js: true
24
+ } );
25
+
26
+ writeFileSync( file, minified );
27
+ };
@@ -55,7 +55,11 @@ function collectLinks( pathsToFiles, options ) {
55
55
  links.add( upath.resolve( filePath + '#' ) );
56
56
 
57
57
  const content = fs.readFileSync( filePath, 'utf-8' );
58
- const ids = ( content.match( /id="[^"]+"/g ) || [] ).map( el => el.replace( /^id="/, '' ).replace( /"$/, '' ) );
58
+ // Extract both quoted and unquoted "id" attributes.
59
+ const ids = [
60
+ ...Array.from( content.matchAll( /id="([^"]+)"/g ), m => m[ 1 ] ),
61
+ ...Array.from( content.matchAll( /id=([^\s"'/>]+)/g ), m => m[ 1 ] )
62
+ ];
59
63
 
60
64
  for ( const id of ids ) {
61
65
  if ( !isMaskedID( id ) && !ids.includes( 'icons-' ) ) {
@@ -12,7 +12,7 @@ mixin header({ withChooseProject = true, className = '' } = {})
12
12
  .c-header__mobile.l-hide-desktop
13
13
  .c-header__mobile-brand
14
14
  a.b-link.u-display-inline-flex(
15
- title='Go to CKEditor Ecosystem Documentation home page',
15
+ title='Navigate to the CKEditor Ecosystem Documentation homepage',
16
16
  href=relative_url(page.path, 'index.html')
17
17
  )
18
18
  +inline-svg({ file: 'ckeditor-logo', ariaHidden: true, className: 'c-header__brand-logo' })
@@ -39,7 +39,7 @@ mixin header({ withChooseProject = true, className = '' } = {})
39
39
  .c-header__brand-row
40
40
  .c-header__brand-row-selector.u-flex-horizontal.u-align-items-center
41
41
  a.b-link.u-display-inline-flex(
42
- title='Go to CKEditor Ecosystem Documentation home page',
42
+ title='Navigate to the CKEditor Ecosystem Documentation homepage',
43
43
  href=relative_url(page.path, 'index.html')
44
44
  )
45
45
  +inline-svg({ file: 'ckeditor-logo', ariaHidden: true, className: 'c-header__brand-logo' })
@@ -32,7 +32,7 @@
32
32
  text-align: left;
33
33
  font-weight: var(--font-weight-bold);
34
34
  font-family: var(--font-family-heading);
35
- background-color: var(--color-gray-200);
35
+ background-color: var(--color-gray-300);
36
36
  }
37
37
 
38
38
  &-wrapper {
@@ -53,7 +53,8 @@
53
53
  @extend .b-table__row;
54
54
  }
55
55
 
56
- td, th {
56
+ td,
57
+ th {
57
58
  @extend .b-table__cell;
58
59
  }
59
60
 
@@ -61,4 +62,4 @@
61
62
  @extend .b-table__header;
62
63
  }
63
64
  }
64
- }
65
+ }
@@ -22,7 +22,7 @@ export class HashLink extends BaseComponent {
22
22
  document.addEventListener( 'click', event => {
23
23
  const target = event.target.closest( 'a[href^="#"]' );
24
24
 
25
- if ( !target ) {
25
+ if ( !target || event.defaultPrevented ) {
26
26
  return;
27
27
  }
28
28