umberto 8.3.5 → 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,6 +1,21 @@
1
1
  Changelog
2
2
  =========
3
3
 
4
+ ## [8.4.0](https://github.com/cksource/umberto/compare/v8.3.5...v8.4.0) (October 30, 2025)
5
+
6
+ ### Features
7
+
8
+ * Minify output HTML to reduce file size and improve load performance.
9
+
10
+ The output is automatically minified during production builds to remove unnecessary whitespace and comments.
11
+
12
+ Minification is **disabled when using the `--dev` modifier**, ensuring the generated HTML remains readable for easier debugging and inspection.
13
+
14
+ ### Other changes
15
+
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.
17
+
18
+
4
19
  ## [8.3.5](https://github.com/cksource/umberto/compare/v8.3.4...v8.3.5) (October 29, 2025)
5
20
 
6
21
  ### Other changes
@@ -30,17 +45,6 @@ Changelog
30
45
 
31
46
  * Update to TypeScript 5.3.
32
47
 
33
-
34
- ## [8.3.1](https://github.com/cksource/umberto/compare/v8.3.0...v8.3.1) (September 4, 2025)
35
-
36
- ### Bug fixes
37
-
38
- * Fixed link styles not being applied to followup questions in `Kapa.ai` integration.
39
-
40
- ### Other changes
41
-
42
- * Increased size of the modal in `Kapa.ai` integration.
43
-
44
48
  ---
45
49
 
46
50
  To see all releases, visit the [release page](https://github.com/cksource/umberto/releases).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "umberto",
3
- "version": "8.3.5",
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 );
@@ -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-' ) ) {
@@ -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