umberto 9.1.1 → 9.1.2
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 +9 -7
- package/package.json +1 -1
- package/src/helpers/log-with-time.js +31 -0
- package/src/index.js +3 -5
- package/src/sdk-builder/get-sdk-sources.js +15 -7
- package/src/tasks/build-api-docs.js +4 -3
- package/src/tasks/build-documentation.js +13 -4
- package/src/tasks/minify-html-worker.js +6 -0
- package/src/tasks/minify-html.js +3 -3
- package/src/tasks/validate-links.js +6 -4
- package/themes/umberto/layout/gloria/_modules/sentry/index.pug +2 -2
- package/themes/umberto/src/gloria/js/components/tooltip-popover.js +16 -10
- package/themes/umberto/src/gloria/js/helpers/create-cleanup-registry.js +1 -0
- package/themes/umberto/src/gloria/js/helpers/{is-element-attached.js → is-element-detached.js} +13 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
Changelog
|
|
2
2
|
=========
|
|
3
3
|
|
|
4
|
+
## [9.1.2](https://github.com/cksource/umberto/compare/v9.1.1...v9.1.2) (December 8, 2025)
|
|
5
|
+
|
|
6
|
+
### Bug fixes
|
|
7
|
+
|
|
8
|
+
* 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.
|
|
9
|
+
* Minification should preserve closing tags and output spec-compliant HTML.
|
|
10
|
+
* Fix encoding of SDK sources.
|
|
11
|
+
|
|
12
|
+
|
|
4
13
|
## [9.1.1](https://github.com/cksource/umberto/compare/v9.1.0...v9.1.1) (November 25, 2025)
|
|
5
14
|
|
|
6
15
|
### Other changes
|
|
@@ -43,13 +52,6 @@ Changelog
|
|
|
43
52
|
|
|
44
53
|
* 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
54
|
|
|
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
55
|
---
|
|
54
56
|
|
|
55
57
|
To see all releases, visit the [release page](https://github.com/cksource/umberto/releases).
|
package/package.json
CHANGED
|
@@ -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( '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
|
+
}
|
package/src/index.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 );
|
|
@@ -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
|
-
|
|
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,
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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( () =>
|
|
170
|
-
|
|
171
|
-
|
|
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.
|
|
@@ -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
|
} );
|
package/src/tasks/minify-html.js
CHANGED
|
@@ -9,6 +9,7 @@ const { join } = require( 'path' );
|
|
|
9
9
|
const { globSync } = require( 'fs' );
|
|
10
10
|
const { styleText } = require( '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
|
-
|
|
52
|
+
logTime();
|
|
53
53
|
}
|
|
54
54
|
};
|
|
@@ -16,6 +16,7 @@ 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
106
|
+
logValidatingTime();
|
|
105
107
|
} finally {
|
|
106
108
|
await pool.destroy();
|
|
107
109
|
}
|
|
@@ -16,8 +16,8 @@ mixin load-sentry-script( sentry )
|
|
|
16
16
|
'https://ckeditor.com/docs',
|
|
17
17
|
'https://ckeditor5.github.io/docs/nightly'
|
|
18
18
|
],
|
|
19
|
-
replaysSessionSampleRate:
|
|
20
|
-
replaysOnErrorSampleRate:
|
|
19
|
+
replaysSessionSampleRate: 0,
|
|
20
|
+
replaysOnErrorSampleRate: 0
|
|
21
21
|
} );
|
|
22
22
|
};
|
|
23
23
|
|
|
@@ -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 {
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
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 );
|
package/themes/umberto/src/gloria/js/helpers/{is-element-attached.js → is-element-detached.js}
RENAMED
|
@@ -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
|
|
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
|
-
|
|
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 );
|
|
27
|
+
resolve( true );
|
|
20
28
|
return;
|
|
21
29
|
}
|
|
30
|
+
|
|
22
31
|
currentElement = currentElement.parentElement;
|
|
23
32
|
}
|
|
24
33
|
|
|
25
|
-
resolve( false );
|
|
34
|
+
resolve( false );
|
|
26
35
|
} );
|
|
27
36
|
} );
|
|
28
37
|
}
|