umberto 9.1.1 → 9.1.3

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 (47) hide show
  1. package/CHANGELOG.md +20 -22
  2. package/package.json +1 -1
  3. package/scripts/filter/after-post-render/time-end.js +1 -1
  4. package/scripts/filter/before-post-render/gloria/prerender-xml-pug-components.js +1 -1
  5. package/scripts/utils/execute-and-insert-function-results.js +1 -1
  6. package/scripts/utils/has-own-favicons.js +1 -1
  7. package/scripts/utils/inline-svg.js +2 -2
  8. package/scripts/utils/logcrossprojectreference.js +1 -1
  9. package/scripts/utils/parseicontag.js +1 -1
  10. package/scripts/utils/parselinks.js +1 -1
  11. package/scripts/utils/pug-renderer/render-pug-component.js +1 -1
  12. package/scripts/utils/random-id.js +1 -1
  13. package/scripts/utils/spritesheet-svg.js +2 -2
  14. package/scripts/utils/toc.js +46 -6
  15. package/src/api-builder/build-page-worker.js +1 -1
  16. package/src/api-builder/classes/description-parser.js +1 -1
  17. package/src/helpers/get-docsearch-config.js +1 -1
  18. package/src/helpers/github-url.js +1 -1
  19. package/src/helpers/import-module.js +1 -1
  20. package/src/helpers/log-with-time.js +31 -0
  21. package/src/hexo-manager.js +1 -1
  22. package/src/index.js +5 -7
  23. package/src/sdk-builder/get-sdk-sources.js +16 -8
  24. package/src/sdk-builder/sdk-builder.js +1 -1
  25. package/src/tasks/build-api-docs.js +4 -3
  26. package/src/tasks/build-documentation.js +13 -4
  27. package/src/tasks/create-sitemap-index.js +1 -1
  28. package/src/tasks/create-sitemap-step.js +1 -1
  29. package/src/tasks/create-sitemap.js +2 -2
  30. package/src/tasks/create-sym-links.js +2 -2
  31. package/src/tasks/get-hexo-config.js +1 -1
  32. package/src/tasks/get-main-config.js +1 -1
  33. package/src/tasks/get-project-config.js +1 -1
  34. package/src/tasks/minify-html-worker.js +7 -1
  35. package/src/tasks/minify-html.js +6 -6
  36. package/src/tasks/read-doc-sources.js +1 -1
  37. package/src/tasks/run-webpack.js +1 -1
  38. package/src/tasks/validate-html-w3c.js +1 -1
  39. package/src/tasks/validate-links.js +7 -5
  40. package/src/tasks/watcher.js +1 -1
  41. package/src/template/template-collection.js +1 -1
  42. package/themes/umberto/layout/gloria/_modules/sentry/index.pug +8 -2
  43. package/themes/umberto/layout/gloria/_modules/toc/_style.scss +10 -1
  44. package/themes/umberto/src/gloria/js/components/tooltip-popover.js +16 -10
  45. package/themes/umberto/src/gloria/js/helpers/create-cleanup-registry.js +1 -0
  46. package/themes/umberto/src/gloria/js/helpers/{is-element-attached.js → is-element-detached.js} +13 -4
  47. package/themes/umberto/src/gloria/js/modules/algolia-search.js +45 -0
package/CHANGELOG.md CHANGED
@@ -1,6 +1,26 @@
1
1
  Changelog
2
2
  =========
3
3
 
4
+ ## [9.1.3](https://github.com/cksource/umberto/compare/v9.1.2...v9.1.3) (December 19, 2025)
5
+
6
+ ### Bug fixes
7
+
8
+ * Fixed the conflict where typing the forward slash in the Kapa widget would trigger the Algolia search.
9
+
10
+ ### Other changes
11
+
12
+ * Added CKBox (`CKBOX_VERSION`) and CKEditor 5 (`CKEDITOR_VERSION`) versions to events sent to Sentry integration.
13
+
14
+
15
+ ## [9.1.2](https://github.com/cksource/umberto/compare/v9.1.1...v9.1.2) (December 8, 2025)
16
+
17
+ ### Bug fixes
18
+
19
+ * Fix a race condition in the Tooltip/Popover component between `createTooltipPopover` and `TooltipPopover.attach` that could cause duplicate initialization. Tooltips now initialize only once per element and more reliably detect when trigger elements have been detached.
20
+ * Minification should preserve closing tags and output spec-compliant HTML.
21
+ * Fix encoding of SDK sources.
22
+
23
+
4
24
  ## [9.1.1](https://github.com/cksource/umberto/compare/v9.1.0...v9.1.1) (November 25, 2025)
5
25
 
6
26
  ### Other changes
@@ -28,28 +48,6 @@ Changelog
28
48
 
29
49
  * Updated the required version of Node.js to **v24.11**.
30
50
 
31
-
32
- ## [8.4.0](https://github.com/cksource/umberto/compare/v8.3.5...v8.4.0) (October 30, 2025)
33
-
34
- ### Features
35
-
36
- * Minify output HTML to reduce file size and improve load performance.
37
-
38
- The output is automatically minified during production builds to remove unnecessary whitespace and comments.
39
-
40
- Minification is **disabled when using the `--dev` modifier**, ensuring the generated HTML remains readable for easier debugging and inspection.
41
-
42
- ### Other changes
43
-
44
- * 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.
45
-
46
-
47
- ## [8.3.5](https://github.com/cksource/umberto/compare/v8.3.4...v8.3.5) (October 29, 2025)
48
-
49
- ### Other changes
50
-
51
- * Hook scripts now receive the full project configuration object when executed.
52
-
53
51
  ---
54
52
 
55
53
  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": "9.1.1",
3
+ "version": "9.1.3",
4
4
  "description": "CKSource Documentation builder",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -5,7 +5,7 @@
5
5
 
6
6
  'use strict';
7
7
 
8
- const { format, styleText } = require( 'util' );
8
+ const { format, styleText } = require( 'node:util' );
9
9
 
10
10
  hexo.extend.filter.register( 'after_post_render', page => {
11
11
  if ( !hexo.projectGlobals.common || !hexo.projectGlobals.common.verbose ) {
@@ -42,7 +42,7 @@
42
42
 
43
43
  'use strict';
44
44
 
45
- const path = require( 'path' );
45
+ const path = require( 'node:path' );
46
46
  const removeIndentation = require( '../../../utils/remove-indentation' );
47
47
  const createPrerenderPugTemplate = require( '../../../utils/pug-renderer/create-prerender-pug-template' );
48
48
  const {
@@ -6,7 +6,7 @@
6
6
  'use strict';
7
7
 
8
8
  const upath = require( 'upath' );
9
- const fs = require( 'fs' );
9
+ const fs = require( 'node:fs' );
10
10
 
11
11
  const EXEC_REGEXP = /\\?{@exec ([^}]+)\\?}/g;
12
12
  const PATH_REGEXP = /[\w.\\/-]+\.c?js$/;
@@ -6,7 +6,7 @@
6
6
  'use strict';
7
7
 
8
8
  const upath = require( 'upath' );
9
- const fs = require( 'fs' );
9
+ const fs = require( 'node:fs' );
10
10
 
11
11
  /**
12
12
  * Checks if the project has its own favicons. If so, they will be used when rendering meta tags (`<link rel="icon">`).
@@ -5,8 +5,8 @@
5
5
 
6
6
  'use strict';
7
7
 
8
- const fs = require( 'fs' );
9
- const path = require( 'path' );
8
+ const fs = require( 'node:fs' );
9
+ const path = require( 'node:path' );
10
10
  const { parseDocument } = require( 'htmlparser2' );
11
11
  const { Element, Text } = require( 'domhandler' );
12
12
  const { selectAll, selectOne } = require( 'css-select' );
@@ -5,7 +5,7 @@
5
5
 
6
6
  'use strict';
7
7
 
8
- const { styleText } = require( 'util' );
8
+ const { styleText } = require( 'node:util' );
9
9
 
10
10
  /**
11
11
  * Logs an error message about a cross-project reference found in the parsed expression.
@@ -5,7 +5,7 @@
5
5
 
6
6
  'use strict';
7
7
 
8
- const crypto = require( 'crypto' );
8
+ const crypto = require( 'node:crypto' );
9
9
  const upath = require( 'upath' );
10
10
  const logCrossProjectReference = require( './logcrossprojectreference' );
11
11
 
@@ -5,7 +5,7 @@
5
5
 
6
6
  'use strict';
7
7
 
8
- const { styleText } = require( 'util' );
8
+ const { styleText } = require( 'node:util' );
9
9
  const upath = require( 'upath' );
10
10
  const splitLongname = require( '../../src/helpers/split-longname' );
11
11
 
@@ -5,7 +5,7 @@
5
5
 
6
6
  'use strict';
7
7
 
8
- const path = require( 'path' );
8
+ const path = require( 'node:path' );
9
9
  const hexoManager = require( '../../../src/hexo-manager' );
10
10
 
11
11
  /**
@@ -5,7 +5,7 @@
5
5
 
6
6
  'use strict';
7
7
 
8
- const crypto = require( 'crypto' );
8
+ const crypto = require( 'node:crypto' );
9
9
 
10
10
  /**
11
11
  * Generates a random ID with a single segment. If a prefix is provided, it will be prepended to the ID.
@@ -5,8 +5,8 @@
5
5
 
6
6
  'use strict';
7
7
 
8
- const fs = require( 'fs' );
9
- const path = require( 'path' );
8
+ const fs = require( 'node:fs' );
9
+ const path = require( 'node:path' );
10
10
  const { parseDocument } = require( 'htmlparser2' );
11
11
  const { Element, cloneNode } = require( 'domhandler' );
12
12
  const { selectAll, selectOne } = require( 'css-select' );
@@ -43,14 +43,25 @@ module.exports = function toc( data, options = {} ) {
43
43
  for ( const heading of headings ) {
44
44
  const hLevel = Number( heading.name[ 1 ] ); // 'h2' -> 2
45
45
  const text = getHeadingText( heading ).trim();
46
+ const icons = getHeadingIcons( heading );
46
47
  const li = new Element( 'li', {} );
47
- const a = new Element( 'a', {
48
- class: 'a11y-focusable',
48
+ const aAttrs = {
49
+ class: icons.length > 0 ? 'a11y-focusable c-toc__link--with-icon' : 'a11y-focusable',
49
50
  href: '#' + getAttributeValue( heading, 'id' ),
50
51
  title: text
51
- } );
52
+ };
52
53
 
53
- appendChild( a, new Text( text ) );
54
+ const a = new Element( 'a', aAttrs );
55
+
56
+ // Append text first, then icons (only if icons exist)
57
+ if ( text ) {
58
+ appendChild( a, new Text( text ) );
59
+ }
60
+ if ( icons.length > 0 ) {
61
+ for ( const icon of icons ) {
62
+ appendChild( a, cloneNode( icon, true ) );
63
+ }
64
+ }
54
65
  appendChild( li, a );
55
66
 
56
67
  if ( tocLastLevels[ hLevel ] ) {
@@ -107,7 +118,7 @@ function hasAncestorWithClassIn( node, classNames ) {
107
118
  }
108
119
 
109
120
  /**
110
- * Returns the heading text without content from descendants with class "headerlink".
121
+ * Returns the heading text without content from descendants with class "headerlink", "editor-icon", or badge components.
111
122
  */
112
123
  function getHeadingText( root ) {
113
124
  if ( !root ) {
@@ -117,9 +128,38 @@ function getHeadingText( root ) {
117
128
  // Clone so we don't mutate the original AST.
118
129
  const clone = cloneNode( root, true );
119
130
 
120
- for ( const node of selectAll( '.headerlink', clone ) ) {
131
+ // Remove headerlinks, editor-icons, and badge components (which contain icons)
132
+ for ( const node of selectAll( '.headerlink, .editor-icon, .c-badge', clone ) ) {
121
133
  removeElement( node );
122
134
  }
123
135
 
124
136
  return textContent( clone );
125
137
  }
138
+
139
+ /**
140
+ * Returns icon elements from the heading.
141
+ * Looks for:
142
+ * - Elements with class "editor-icon" (legacy format)
143
+ * - Badge components with icons (c-badge with c-icon inside)
144
+ * - Direct icon elements (c-icon) within the heading
145
+ */
146
+ function getHeadingIcons( root ) {
147
+ if ( !root ) {
148
+ return [];
149
+ }
150
+
151
+ const icons = [];
152
+
153
+ const editorIcons = selectAll( '.editor-icon', root );
154
+ icons.push( ...editorIcons );
155
+
156
+ const badgesWithIcons = selectAll( '.c-badge .c-icon', root );
157
+ icons.push( ...badgesWithIcons );
158
+
159
+ const directIcons = selectAll( '.c-icon', root ).filter( icon => {
160
+ return !hasAncestorWithClassIn( icon, [ 'c-badge' ] );
161
+ } );
162
+ icons.push( ...directIcons );
163
+
164
+ return icons;
165
+ }
@@ -5,7 +5,7 @@
5
5
 
6
6
  'use strict';
7
7
 
8
- const fs = require( 'fs' );
8
+ const fs = require( 'node:fs' );
9
9
  const upath = require( 'upath' );
10
10
  const { parseDocument } = require( 'htmlparser2' );
11
11
  const { default: render } = require( 'dom-serializer' );
@@ -5,7 +5,7 @@
5
5
 
6
6
  'use strict';
7
7
 
8
- const { styleText } = require( 'util' );
8
+ const { styleText } = require( 'node:util' );
9
9
  const { parseDocument } = require( 'htmlparser2' );
10
10
  const { default: render } = require( 'dom-serializer' );
11
11
  const { selectAll, selectOne } = require( 'css-select' );
@@ -7,7 +7,7 @@
7
7
 
8
8
  const { cloneDeep } = require( 'lodash' );
9
9
  const { stringify } = require( 'javascript-stringify' );
10
- const fs = require( 'fs' );
10
+ const fs = require( 'node:fs' );
11
11
  const upath = require( 'upath' );
12
12
 
13
13
  const defaultScriptTemplate = fs.readFileSync( upath.join( __dirname, 'templates', 'scripts', 'default.js' ), 'utf-8' );
@@ -5,7 +5,7 @@
5
5
 
6
6
  'use strict';
7
7
 
8
- const { spawnSync } = require( 'child_process' );
8
+ const { spawnSync } = require( 'node:child_process' );
9
9
  const upath = require( 'upath' );
10
10
 
11
11
  const cachedGitPaths = new Set();
@@ -6,7 +6,7 @@
6
6
  'use strict';
7
7
 
8
8
  const upath = require( 'upath' );
9
- const { pathToFileURL } = require( 'url' );
9
+ const { pathToFileURL } = require( 'node:url' );
10
10
 
11
11
  /**
12
12
  * Loads the specified module using `import()` call.
@@ -0,0 +1,31 @@
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 { styleText } = require( 'node:util' );
9
+
10
+ module.exports = function logWithTime( message ) {
11
+ const lowercase = message.charAt( 0 ).toLowerCase() + message.slice( 1 );
12
+ const getTime = measureSeconds();
13
+
14
+ console.log( `Started ${ lowercase }.` );
15
+
16
+ return () => console.log( `Finished ${ lowercase } in ${ styleText( 'bold', getTime() ) }.` );
17
+ };
18
+
19
+ function measureSeconds() {
20
+ const start = process.hrtime();
21
+
22
+ return () => {
23
+ const [ totalSeconds ] = process.hrtime( start );
24
+ const minutes = Math.floor( totalSeconds / 60 );
25
+ const seconds = totalSeconds % 60;
26
+
27
+ return minutes ?
28
+ minutes + 'm ' + String( seconds ).padStart( 2, '0' ) + 's' :
29
+ seconds + 's';
30
+ };
31
+ }
@@ -5,7 +5,7 @@
5
5
 
6
6
  'use strict';
7
7
 
8
- const { styleText } = require( 'util' );
8
+ const { styleText } = require( 'node:util' );
9
9
  const upath = require( 'upath' );
10
10
  const fs = require( 'fs-extra' );
11
11
  const { globSync } = require( 'glob' );
package/src/index.js CHANGED
@@ -5,8 +5,8 @@
5
5
 
6
6
  'use strict';
7
7
 
8
- const { styleText } = require( 'util' );
9
- const path = require( 'path' );
8
+ const { styleText } = require( 'node:util' );
9
+ const path = require( 'node:path' );
10
10
  const compileSass = require( './tasks/compile-sass' );
11
11
  const runWebpack = require( './tasks/run-webpack' );
12
12
  const copyAssets = require( './tasks/copy-assets' );
@@ -17,6 +17,7 @@ const getProjectConfig = require( './tasks/get-project-config' );
17
17
  const watcher = require( './tasks/watcher' );
18
18
  const validateHtml = require( './tasks/validate-html-w3c' );
19
19
  const { version } = require( '../package.json' );
20
+ const logWithTime = require( './helpers/log-with-time' );
20
21
 
21
22
  module.exports = {
22
23
  /**
@@ -68,9 +69,8 @@ module.exports = {
68
69
  * @returns {Promise}
69
70
  */
70
71
  async function buildAndWatch( options ) {
71
- const timer = process.hrtime();
72
+ const logTime = logWithTime( 'Building documentation' );
72
73
  const outputDir = path.join( process.cwd(), 'build', 'docs' );
73
-
74
74
  const hexoManager = await buildDocumentation( options );
75
75
 
76
76
  if ( !options.skipThemes ) {
@@ -91,9 +91,7 @@ async function buildAndWatch( options ) {
91
91
  } );
92
92
  }
93
93
 
94
- const [ time ] = process.hrtime( timer );
95
-
96
- console.log( styleText( 'greenBright', `Building documentation complete in ${ Math.floor( time / 60 ) }m ${ time % 60 }s.` ) );
94
+ logTime();
97
95
 
98
96
  if ( options.watch ) {
99
97
  return watcher( hexoManager );
@@ -5,7 +5,7 @@
5
5
 
6
6
  'use strict';
7
7
 
8
- const fs = require( 'fs' );
8
+ const fs = require( 'node:fs' );
9
9
  const upath = require( 'upath' );
10
10
  const { globSync } = require( 'glob' );
11
11
  const { parseDocument } = require( 'htmlparser2' );
@@ -18,6 +18,7 @@ const {
18
18
  textContent,
19
19
  getName
20
20
  } = require( 'domutils' );
21
+ const decodeHtmlEntities = require( '../../scripts/utils/decode-html-entities' );
21
22
 
22
23
  const SDK_SELECTOR = 'meta[name="sdk-samples"]';
23
24
 
@@ -27,7 +28,7 @@ module.exports = sourcePath => {
27
28
 
28
29
  for ( const filePath of filePaths ) {
29
30
  const content = fs.readFileSync( filePath, { encoding: 'utf8' } );
30
- const doc = parseDocument( content );
31
+ const doc = parseDocument( content, { decodeEntities: false } );
31
32
  const sdkMeta = selectOne( SDK_SELECTOR, doc );
32
33
  const samplesNames = sdkMeta && getAttributeValue( sdkMeta, 'content' ) ?
33
34
  getAttributeValue( sdkMeta, 'content' ).split( '|' ) :
@@ -77,9 +78,16 @@ module.exports = sourcePath => {
77
78
  }
78
79
 
79
80
  if ( hasAttrib( element, 'data-sample-strip-outer-tag' ) ) {
80
- ret.source = render( element.children || [] ).replace( /^\n|\n$/g, '' );
81
+ ret.source = render( element.children || [], { encodeEntities: false } ).replace( /^\n|\n$/g, '' );
81
82
  } else {
82
- ret.source = render( element ).replace( /\s?data-sample(-[\w-]+)?="[^"]*?"/g, '' );
83
+ // Delete all data-sample* attributes.
84
+ for ( const attrib of Object.keys( element.attribs ) ) {
85
+ if ( attrib.startsWith( 'data-sample' ) ) {
86
+ delete element.attribs[ attrib ];
87
+ }
88
+ }
89
+
90
+ ret.source = render( element, { encodeEntities: false } );
83
91
  }
84
92
 
85
93
  outputArray.push( ret );
@@ -101,15 +109,15 @@ module.exports = sourcePath => {
101
109
  }
102
110
 
103
111
  const sdkContents = selectOne( '.sdk-contents', doc );
104
- const contentHtml = render( sdkContents.children || [] );
112
+ const contentHtml = render( sdkContents.children || [], { encodeEntities: false } );
105
113
  const presetVersion = getAttributeValue( sdkContents, 'data-cke-preset' );
106
114
 
107
115
  files.push( {
108
116
  name: upath.basename( filePath, '.html' ),
109
- description: getAttributeValue( selectOne( 'meta[name="description"]', doc ), 'content' ),
110
- title: textContent( selectOne( 'title', doc ) ),
117
+ description: decodeHtmlEntities( getAttributeValue( selectOne( 'meta[name="description"]', doc ), 'content' ) ),
118
+ title: decodeHtmlEntities( textContent( selectOne( 'title', doc ) ) ),
111
119
  content: contentHtml,
112
- source: render( doc ),
120
+ source: render( doc, { encodeEntities: false } ),
113
121
  path: upath.relative( sourcePath, filePath ),
114
122
  presetVersion,
115
123
  sdkSamples: samples,
@@ -7,7 +7,7 @@
7
7
 
8
8
  const HtmlFile = require( '../api-builder/classes/html-file' );
9
9
  const upath = require( 'upath' );
10
- const { URL, resolve: urlResolve } = require( 'url' );
10
+ const { URL, resolve: urlResolve } = require( 'node:url' );
11
11
  const beautifyHtml = require( 'js-beautify' ).html;
12
12
  const macroReplacer = require( '../tasks/macro-replacer' );
13
13
  const getDocSearchConfig = require( '../helpers/get-docsearch-config' );
@@ -10,6 +10,7 @@ const readDocSources = require( './read-doc-sources' );
10
10
  const DataProvider = require( '../data-converter/data-provider' );
11
11
  const ApiBuilder = require( '../api-builder/api-builder' );
12
12
  const TemplateCollection = require( '../template/template-collection' );
13
+ const logWithTime = require( '../helpers/log-with-time' );
13
14
 
14
15
  /**
15
16
  * Builds API docs.
@@ -64,10 +65,10 @@ module.exports = async config => {
64
65
  }
65
66
  );
66
67
 
67
- console.log( `Building API docs of ${ projectConfig.name } ...` );
68
- console.time( `Built API docs of ${ projectConfig.name } in` );
68
+ const logTime = logWithTime( `Building API docs for ${ projectConfig.name }` );
69
69
  const buildInfo = await apiBuilder.buildApi();
70
- console.timeEnd( `Built API docs of ${ projectConfig.name } in` );
70
+
71
+ logTime();
71
72
 
72
73
  if ( process.argv.includes( '--strict' ) && buildInfo.warningCount > 0 ) {
73
74
  console.log( `There were ${ buildInfo.warningCount } warnings reported in strict mode. Aborting.` );
@@ -31,6 +31,7 @@ const umbertoVersion = require( '../../package.json' ).version;
31
31
 
32
32
  const { parseLinks } = require( '../../scripts/utils/parselinks' );
33
33
  const getFilePatternsToProcess = require( '../helpers/get-file-patterns-to-process' );
34
+ const logWithTime = require( '../helpers/log-with-time' );
34
35
 
35
36
  /**
36
37
  * Main function building the documentation.
@@ -166,10 +167,18 @@ module.exports = options => {
166
167
  skipGuides
167
168
  } );
168
169
  } )
169
- .then( () => hexoManager.generate( {
170
- watch,
171
- concurrency: 80
172
- } ) )
170
+ .then( async () => {
171
+ const logTime = logWithTime( 'Generating pages with Hexo' );
172
+
173
+ const result = await hexoManager.generate( {
174
+ watch,
175
+ concurrency: 80
176
+ } );
177
+
178
+ logTime();
179
+
180
+ return result;
181
+ } )
173
182
  .then( () => buildSnippets( hexoManager.hexo.projectGlobals ) )
174
183
  .then( () => copyProjectIcons( hexoManager.hexo.projectGlobals, hexoManager.getPublicDir() ) )
175
184
  // A workaround for API guides when API generation is skipped.
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  const upath = require( 'upath' );
7
- const fs = require( 'fs/promises' );
7
+ const fs = require( 'node:fs/promises' );
8
8
 
9
9
  const XMLNS_STRINGS = [
10
10
  'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"',
@@ -3,7 +3,7 @@
3
3
  * For licensing, see LICENSE.md.
4
4
  */
5
5
 
6
- const { styleText } = require( 'util' );
6
+ const { styleText } = require( 'node:util' );
7
7
  const createSitemap = require( './create-sitemap' );
8
8
  const createSitemapIndex = require( './create-sitemap-index' );
9
9
 
@@ -5,10 +5,10 @@
5
5
 
6
6
  'use strict';
7
7
 
8
- const fs = require( 'fs' );
8
+ const fs = require( 'node:fs' );
9
9
  const { globSync } = require( 'glob' );
10
10
  const upath = require( 'upath' );
11
- const { URL } = require( 'url' );
11
+ const { URL } = require( 'node:url' );
12
12
  const { SitemapStream, streamToPromise } = require( 'sitemap' );
13
13
 
14
14
  /**
@@ -5,8 +5,8 @@
5
5
 
6
6
  'use strict';
7
7
 
8
- const { styleText } = require( 'util' );
9
- const fs = require( 'fs' );
8
+ const { styleText } = require( 'node:util' );
9
+ const fs = require( 'node:fs' );
10
10
  const upath = require( 'upath' );
11
11
  const getProjectConfig = require( './get-project-config' );
12
12
 
@@ -6,7 +6,7 @@
6
6
  'use strict';
7
7
 
8
8
  const upath = require( 'upath' );
9
- const fs = require( 'fs' );
9
+ const fs = require( 'node:fs' );
10
10
 
11
11
  /**
12
12
  * Reads Umberto's hexo config file.
@@ -6,7 +6,7 @@
6
6
  'use strict';
7
7
 
8
8
  const upath = require( 'upath' );
9
- const fs = require( 'fs' );
9
+ const fs = require( 'node:fs' );
10
10
 
11
11
  module.exports = rootPath => {
12
12
  const configPath = upath.join( rootPath, 'umberto-main.json' );
@@ -6,7 +6,7 @@
6
6
  'use strict';
7
7
 
8
8
  const upath = require( 'upath' );
9
- const fs = require( 'fs' );
9
+ const fs = require( 'node:fs' );
10
10
  const { globSync } = require( 'glob' );
11
11
  const importModule = require( '../helpers/import-module' );
12
12
 
@@ -5,7 +5,7 @@
5
5
 
6
6
  'use strict';
7
7
 
8
- const { readFileSync, writeFileSync } = require( 'fs' );
8
+ const { readFileSync, writeFileSync } = require( 'node:fs' );
9
9
  const minifier = require( '@minify-html/node' );
10
10
 
11
11
  /**
@@ -18,7 +18,13 @@ module.exports = function( file ) {
18
18
  const html = readFileSync( file );
19
19
 
20
20
  const minified = minifier.minify( html, {
21
+ allow_noncompliant_unquoted_attribute_values: false,
22
+ allow_optimal_entities: false,
23
+ allow_removing_spaces_between_attributes: false,
24
+ keep_closing_tags: true,
21
25
  keep_comments: true,
26
+ keep_html_and_head_opening_tags: true,
27
+ keep_input_type_text_attr: true,
22
28
  minify_css: true,
23
29
  minify_js: true
24
30
  } );
@@ -5,10 +5,11 @@
5
5
 
6
6
  'use strict';
7
7
 
8
- const { join } = require( 'path' );
9
- const { globSync } = require( 'fs' );
10
- const { styleText } = require( 'util' );
8
+ const { join } = require( 'node:path' );
9
+ const { globSync } = require( 'node:fs' );
10
+ const { styleText } = require( 'node:util' );
11
11
  const { default: TinyPool } = require( 'tinypool' );
12
+ const logWithTime = require( '../helpers/log-with-time' );
12
13
 
13
14
  /**
14
15
  * Minifies all HTML files under outputDir in parallel using tinypool.
@@ -16,6 +17,7 @@ const { default: TinyPool } = require( 'tinypool' );
16
17
  * @param {string} outputDir - base directory to search for *.html files
17
18
  */
18
19
  module.exports = async function minifyHtml( outputDir ) {
20
+ const logTime = logWithTime( 'Minifying HTML' );
19
21
  const files = globSync( join( outputDir, '**', 'api', '**', '*.html' ) );
20
22
 
21
23
  const pool = new TinyPool( {
@@ -24,8 +26,6 @@ module.exports = async function minifyHtml( outputDir ) {
24
26
  } );
25
27
 
26
28
  try {
27
- console.log( 'Started HTML minification.' );
28
-
29
29
  const results = await Promise.allSettled(
30
30
  files.map( async file => {
31
31
  try {
@@ -49,6 +49,6 @@ module.exports = async function minifyHtml( outputDir ) {
49
49
  } finally {
50
50
  await pool.destroy();
51
51
 
52
- console.log( 'Finished HTML minification.' );
52
+ logTime();
53
53
  }
54
54
  };
@@ -6,7 +6,7 @@
6
6
  'use strict';
7
7
 
8
8
  const upath = require( 'upath' );
9
- const fs = require( 'fs' );
9
+ const fs = require( 'node:fs' );
10
10
  const { globSync } = require( 'glob' );
11
11
  const DOC_FORMATS = require( '../helpers/doc-formats' );
12
12
 
@@ -5,7 +5,7 @@
5
5
 
6
6
  'use strict';
7
7
 
8
- const path = require( 'path' );
8
+ const path = require( 'node:path' );
9
9
  const webpack = require( 'webpack' );
10
10
 
11
11
  module.exports = ( sourceFile, destinationFile, {
@@ -3,7 +3,7 @@
3
3
  * For licensing, see LICENSE.md.
4
4
  */
5
5
 
6
- const { spawn } = require( 'child_process' );
6
+ const { spawn } = require( 'node:child_process' );
7
7
  const vnu = require( 'vnu-jar' );
8
8
 
9
9
  module.exports = ( buildPath, { verbose } = {} ) => new Promise( ( resolve, reject ) => {
@@ -11,11 +11,12 @@
11
11
  * especially for API documentation where the number of links can reach thousands.
12
12
  */
13
13
 
14
- const { styleText } = require( 'util' );
14
+ const { styleText } = require( 'node:util' );
15
15
  const fs = require( 'fs-extra' );
16
16
  const upath = require( 'upath' );
17
17
  const { globSync } = require( 'glob' );
18
18
  const { default: TinyPool } = require( 'tinypool' );
19
+ const logWithTime = require( '../helpers/log-with-time' );
19
20
 
20
21
  /**
21
22
  * Collects all links from the provided HTML files.
@@ -81,16 +82,17 @@ async function collectLinks( pattern, options ) {
81
82
  * @param {Object} [options={}] Validation options.
82
83
  */
83
84
  module.exports = async function validateLinks( buildPath, options = {} ) {
84
- console.info( `Gathering links in ${ styleText( 'magenta', buildPath ) } to validate...` );
85
+ const logGatheringTime = logWithTime( 'Gathering links to validate' );
85
86
 
86
- const start = Date.now();
87
87
  const { links, pathsToFiles } = await collectLinks( upath.join( buildPath, '**', '*' ), options );
88
88
  const pool = new TinyPool( {
89
89
  filename: require.resolve( './validate-links-worker.js' )
90
90
  } );
91
91
 
92
+ logGatheringTime();
93
+
92
94
  try {
93
- console.info( `Validating links in ${ styleText( 'magenta', buildPath ) }...` );
95
+ const logValidatingTime = logWithTime( 'Validating links' );
94
96
 
95
97
  const tasks = splitArrayBalanced( pathsToFiles, pool.options.maxThreads ).map( chunk => pool.run( { chunk, links, options } ) );
96
98
  const results = await Promise.all( tasks );
@@ -101,7 +103,7 @@ module.exports = async function validateLinks( buildPath, options = {} ) {
101
103
  throw new Error( `Found ${ errors.length } invalid links.` );
102
104
  }
103
105
 
104
- console.info( `Links validation finished in ${ styleText( 'green', ( Date.now() - start ) + 'ms' ) }.` );
106
+ logValidatingTime();
105
107
  } finally {
106
108
  await pool.destroy();
107
109
  }
@@ -5,7 +5,7 @@
5
5
 
6
6
  'use strict';
7
7
 
8
- const { styleText } = require( 'util' );
8
+ const { styleText } = require( 'node:util' );
9
9
  const upath = require( 'upath' );
10
10
  const chokidar = require( 'chokidar' );
11
11
  const { copy } = require( 'fs-extra' );
@@ -6,7 +6,7 @@
6
6
  'use strict';
7
7
 
8
8
  const pug = require( 'pug' );
9
- const fs = require( 'fs' );
9
+ const fs = require( 'node:fs' );
10
10
  const upath = require( 'upath' );
11
11
  const isNonEmptyArray = require( '../helpers/is-non-empty-array' );
12
12
  const createFilterAttribs = require( '../helpers/create-filtering-data-attribs' );
@@ -16,8 +16,14 @@ mixin load-sentry-script( sentry )
16
16
  'https://ckeditor.com/docs',
17
17
  'https://ckeditor5.github.io/docs/nightly'
18
18
  ],
19
- replaysSessionSampleRate: 1.0,
20
- replaysOnErrorSampleRate: 1.0
19
+ replaysSessionSampleRate: 0,
20
+ replaysOnErrorSampleRate: 0,
21
+ initialScope: scope => {
22
+ scope.setTags( {
23
+ CKEDITOR_VERSION: typeof CKEDITOR_VERSION !== 'undefined' ? CKEDITOR_VERSION : '(none)',
24
+ CKBOX_VERSION: typeof CKBox !== 'undefined' ? CKBox.version : '(none)',
25
+ } );
26
+ }
21
27
  } );
22
28
  };
23
29
 
@@ -183,12 +183,21 @@
183
183
  }
184
184
 
185
185
  ol a {
186
- display: block;
187
186
  padding: 0 var(--spacing-2);
188
187
  text-decoration: none;
189
188
  font-size: var(--font-size-sm);
190
189
  color: var(--color-secondary-700);
191
190
 
191
+ &:not(.c-toc__link--with-icon) {
192
+ display: block;
193
+ }
194
+
195
+ &.c-toc__link--with-icon {
196
+ display: flex;
197
+ align-items: center;
198
+ gap: var(--spacing-1);
199
+ }
200
+
192
201
  &:hover,
193
202
  &.is-active {
194
203
  color: var(--color-primary);
@@ -13,8 +13,9 @@ import { randomId } from '../helpers/random-id';
13
13
  import { createAnchorPositioningWatcher } from '../helpers/create-anchor-positioning-watcher';
14
14
  import { createMutationObserver } from '../helpers/create-mutation-observer';
15
15
  import { createCleanupRegistry } from '../helpers/create-cleanup-registry';
16
- import { isElementAttached } from '../helpers/is-element-attached';
16
+ import { isElementDetached } from '../helpers/is-element-detached';
17
17
  import { afterDomReady } from '../helpers/after-dom-ready';
18
+ import { timeout } from '../helpers/timeout';
18
19
 
19
20
  /**
20
21
  * Tooltip component.
@@ -71,7 +72,7 @@ export class TooltipPopover extends BaseComponent {
71
72
  const tooltips = document.querySelectorAll( '.js-tooltip-popover' );
72
73
 
73
74
  for ( const tooltip of tooltips ) {
74
- new TooltipPopover( tooltip ).init();
75
+ tooltip.tooltipPopover ||= new TooltipPopover( tooltip ).init();
75
76
  }
76
77
  }
77
78
 
@@ -123,14 +124,14 @@ export class TooltipPopover extends BaseComponent {
123
124
  // If so, and the tooltip is visible, hide it.
124
125
  // Sometimes, framework might remove the trigger element after the click event.
125
126
  // It handles such cases gracefully.
126
- this.triggerCleanup.addEventListener( window, 'mousedown', () => {
127
- setTimeout( () => {
128
- isElementAttached( trigger ).then( isDetached => {
129
- if ( isDetached && this.state.isVisible ) {
130
- this.hide();
131
- }
132
- } );
133
- }, 300 );
127
+ this.triggerCleanup.addEventListener( window, 'mousedown', async () => {
128
+ await timeout( 300 );
129
+
130
+ const isDetached = await isElementDetached( trigger );
131
+
132
+ if ( isDetached && this.state.isVisible ) {
133
+ this.hide();
134
+ }
134
135
  } );
135
136
  break;
136
137
 
@@ -397,6 +398,7 @@ export class TooltipPopover extends BaseComponent {
397
398
  arrow.style.left = `calc(50% - ${ offset }px)`;
398
399
  } else if ( position.endsWith( '-end' ) ) {
399
400
  const currentRight = parseFloat( arrow.style.right || 0 );
401
+
400
402
  arrow.style.right = `${ currentRight + offset }px`;
401
403
  }
402
404
  } else if ( left + tooltipRect.width > window.innerWidth ) {
@@ -407,9 +409,11 @@ export class TooltipPopover extends BaseComponent {
407
409
 
408
410
  if ( !position.endsWith( '-start' ) && !position.endsWith( '-end' ) ) {
409
411
  const offset = origLeft - left;
412
+
410
413
  arrow.style.left = `calc(50% + ${ offset }px)`;
411
414
  } else if ( position.endsWith( '-start' ) ) {
412
415
  const currentLeft = parseFloat( arrow.style.left || 0 );
416
+
413
417
  arrow.style.left = `${ currentLeft - overflowAmount }px`;
414
418
  }
415
419
  }
@@ -560,6 +564,8 @@ export function createTooltipPopover( config = {} ) {
560
564
  ...rest
561
565
  } );
562
566
 
567
+ tooltip.tooltipPopover = popover;
568
+
563
569
  afterDomReady( () => {
564
570
  // Add to document
565
571
  document.body.appendChild( tooltip );
@@ -32,6 +32,7 @@ export function createCleanupRegistry() {
32
32
  destroy() {
33
33
  while ( callbacks.length ) {
34
34
  const callback = callbacks.pop();
35
+
35
36
  if ( typeof callback === 'function' ) {
36
37
  callback();
37
38
  }
@@ -6,23 +6,32 @@
6
6
  /**
7
7
  * Asynchronously checks if the given element or any of its parents are detached from the document body.
8
8
  *
9
+ * This function intentionally does not use `isConnected` to ensure compatibility with environments like Shadow DOM.
10
+ * It only checks the connection to `document.body` not to any other root nodes.
11
+ *
9
12
  * @param element The element to check.
10
13
  * @returns A promise that resolves to true if the element or any parent is detached, false otherwise.
11
14
  */
12
- export async function isElementAttached( element ) {
15
+ export async function isElementDetached( element ) {
13
16
  return new Promise( resolve => {
14
17
  // Use requestAnimationFrame to ensure the check happens after any potential DOM manipulations in the same tick.
15
18
  requestAnimationFrame( () => {
16
19
  let currentElement = element;
17
- while ( currentElement ) {
20
+
21
+ for (
22
+ let watchdog = 0;
23
+ currentElement && currentElement !== document.body && watchdog < 100;
24
+ watchdog++
25
+ ) {
18
26
  if ( !document.body.contains( currentElement ) ) {
19
- resolve( true ); // Detached
27
+ resolve( true );
20
28
  return;
21
29
  }
30
+
22
31
  currentElement = currentElement.parentElement;
23
32
  }
24
33
 
25
- resolve( false ); // Attached
34
+ resolve( false );
26
35
  } );
27
36
  } );
28
37
  }
@@ -46,6 +46,11 @@ export class AlgoliaSearch extends BaseComponent {
46
46
  * Initializes the algolia search component.
47
47
  */
48
48
  async init() {
49
+ // IMPORTANT: Add the keyboard event interceptor BEFORE loading the DocSearch library
50
+ // It ensures our listener is registered first and can use `stopImmediatePropagation`
51
+ // to prevent DocSearch's listener from running when typing in the Kapa widget.
52
+ this._preventDocSearchShortcutInKapa();
53
+
49
54
  await injectScript( DOCSEARCH_JS_URL, {
50
55
  attributes: {
51
56
  crossorigin: 'anonymous'
@@ -88,6 +93,46 @@ export class AlgoliaSearch extends BaseComponent {
88
93
  }
89
94
  } );
90
95
  }
96
+
97
+ /**
98
+ * Prevents the DocSearch "/" keyboard shortcut from triggering when the user is typing in the Kapa widget.
99
+ * DocSearch library doesn't provide a configuration option to disable keyboard shortcuts,
100
+ * so we need to intercept the event and stop it completely before DocSearch's listener sees it.
101
+ *
102
+ * This listener is added in the capture phase BEFORE DocSearch is loaded, ensuring it runs first.
103
+ * We use stopImmediatePropagation() to prevent ALL other listeners (including DocSearch's) from running.
104
+ */
105
+ _preventDocSearchShortcutInKapa() {
106
+ document.addEventListener( 'keydown', event => {
107
+ // Only handle the "/" key
108
+ if ( event.key !== '/' ) {
109
+ return;
110
+ }
111
+
112
+ // Use `composedPath()` to check the entire event path, including through shadow DOM boundaries.
113
+ // This is more reliable than checking `document.activeElement` for shadow DOM content.
114
+ const path = event.composedPath();
115
+
116
+ const isTypingInInput = path.some( element => {
117
+ if ( !element || !element.tagName ) {
118
+ return false;
119
+ }
120
+
121
+ // Let's check different types of input fields.
122
+ // Kapa currently uses a textarea for the search input.
123
+ // But there's a chance that it will change in the future.
124
+ const isInputElement = element.tagName === 'TEXTAREA' ||
125
+ element.tagName === 'INPUT' ||
126
+ element.isContentEditable;
127
+
128
+ return isInputElement;
129
+ } );
130
+
131
+ if ( isTypingInInput ) {
132
+ event.stopImmediatePropagation();
133
+ }
134
+ }, true );
135
+ }
91
136
  }
92
137
 
93
138
  /**