umberto 10.7.4 → 10.8.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,52 +1,43 @@
1
1
  Changelog
2
2
  =========
3
3
 
4
- ## [10.7.4](https://github.com/cksource/umberto/compare/v10.7.3...v10.7.4) (July 27, 2026)
4
+ ## [10.8.0](https://github.com/cksource/umberto/compare/v10.7.5...v10.8.0) (September 7, 2026)
5
5
 
6
- ### Other changes
6
+ ### Features
7
7
 
8
- * This release does not include any user-facing changes. It was published solely to verify the release process.
8
+ * Added support for the `llmsTxtProjects` configuration option, which lists the projects that publish an `llms.txt` index. Documentation pages in the Gloria theme now emit `<link rel="alternate" type="text/markdown">` for their own `.md` counterpart, and pages of a listed project also emit `<link rel="describedby">` for that project's index. The documentation landing page points at the `llms.txt` file of the website instead.
9
9
 
10
10
 
11
- ## [10.7.3](https://github.com/cksource/umberto/compare/v10.7.2...v10.7.3) (July 22, 2026)
11
+ ## [10.7.5](https://github.com/cksource/umberto/compare/v10.7.4...v10.7.5) (August 13, 2026)
12
12
 
13
13
  ### Bug fixes
14
14
 
15
- * Fixed live snippets failing to load in browsers that require import maps before module scripts.
16
-
17
-
18
- ## [10.7.2](https://github.com/cksource/umberto/compare/v10.7.1...v10.7.2) (July 16, 2026)
15
+ * The hierarchy of a class or an interface rendered in the API docs built from Typedoc output now contains the full inheritance chain instead of the first level only. Derived classes, derived interfaces, and implementing classes are also resolved transitively, so the "Subclasses", "Implemented by", and "Implements" lists match the behavior of the JSDoc-based pipeline.
19
16
 
20
17
  ### Other changes
21
18
 
22
- * Replaced webpack with Rolldown for building theme scripts. The theme bundles are now built as ES modules with code splitting and loaded via `<script type="module">`, keeping dynamically imported dependencies (such as the shiki grammars in the Gloria theme) in separate, lazily loaded chunks. The output targets ES2022 instead of the Babel-transpiled ES5, and `webpack`, `babel-loader`, `@babel/core`, and `@babel/preset-env` are no longer dependencies of the package.
23
-
19
+ * Unified the rendering of optional properties and parameters in the API documentation in the Gloria theme. Optional members are now marked with the TypeScript question mark suffix, and the separator between a member name and its type no longer contains a space before the colon, matching the TypeScript syntax (for example `options?: Object` instead of `[ options ] : Object`). Previously, optional parameters were wrapped in square brackets and optional properties had the `undefined` type appended to their type lists.
24
20
 
25
- ## [10.7.1](https://github.com/cksource/umberto/compare/v10.7.0...v10.7.1) (July 15, 2026)
26
21
 
27
- ### Bug fixes
28
-
29
- * Fixed source links for CKEditor 5 packages in API documentation so they now point to the public repository. Links for the latest documentation point to the `master` branch, and links for LTS documentation point to the matching `release-v*` branch.
22
+ ## [10.7.4](https://github.com/cksource/umberto/compare/v10.7.3...v10.7.4) (July 27, 2026)
30
23
 
31
24
  ### Other changes
32
25
 
33
- * Improved the performance of the documentation build. Pug templates of XML components used in Markdown are now compiled once and reused across pages, instead of being recompiled for every component occurrence. This shortens the page generation phase by about 20%.
26
+ * This release does not include any user-facing changes. It was published solely to verify the release process.
34
27
 
35
28
 
36
- ## [10.7.0](https://github.com/cksource/umberto/compare/v10.6.1...v10.7.0) (July 2, 2026)
29
+ ## [10.7.3](https://github.com/cksource/umberto/compare/v10.7.2...v10.7.3) (July 22, 2026)
37
30
 
38
- ### Features
31
+ ### Bug fixes
39
32
 
40
- * Enabled MCP install menu in the Kapa widget.
33
+ * Fixed live snippets failing to load in browsers that require import maps before module scripts.
41
34
 
42
- ### Bug fixes
43
35
 
44
- * Fixed scrolling to a section in the Gloria theme when navigating to an anchor without a full page reload — for example, changing the anchor in the address bar, using the browser's back/forward buttons, or opening a link to a section on the current page. The target heading is now offset below the sticky header instead of being hidden beneath it.
45
- * Moved `chokidar` from `devDependencies` to `dependencies`. It is imported at runtime by the file watcher (`src/tasks/watcher.js`), which ships with the package, so it must be installed for consumers. Previously it resolved only by chance through a hoisted copy in the consumer's tree, which fails when the consumer enables pnpm's `enableGlobalVirtualStore` and the package is linked from the global store, outside the consumer's `node_modules`.
36
+ ## [10.7.2](https://github.com/cksource/umberto/compare/v10.7.1...v10.7.2) (July 16, 2026)
46
37
 
47
38
  ### Other changes
48
39
 
49
- * The tabbed content component now sets a `data-tab-label` attribute on each tab panel, carrying the label of its tab. This exposes the label to tools that process the rendered HTML, where it otherwise lives only inside the tab button.
40
+ * Replaced webpack with Rolldown for building theme scripts. The theme bundles are now built as ES modules with code splitting and loaded via `<script type="module">`, keeping dynamically imported dependencies (such as the shiki grammars in the Gloria theme) in separate, lazily loaded chunks. The output targets ES2022 instead of the Babel-transpiled ES5, and `webpack`, `babel-loader`, `@babel/core`, and `@babel/preset-env` are no longer dependencies of the package.
50
41
 
51
42
  ---
52
43
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "umberto",
3
- "version": "10.7.4",
3
+ "version": "10.8.0",
4
4
  "description": "CKSource Documentation builder",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -89,6 +89,9 @@ export class TypedocConverter {
89
89
  this._convertChild( child );
90
90
  }
91
91
 
92
+ // Resolve full inheritance chains while all doclets still carry their original signatures.
93
+ this._buildHierarchy();
94
+
92
95
  // Then, when all doclets are specified, let's align types.
93
96
  for ( const doclet of this._doclets ) {
94
97
  // Skip source if the `file` property is not specified.
@@ -101,28 +104,6 @@ export class TypedocConverter {
101
104
  continue;
102
105
  }
103
106
 
104
- // TODO: Could we get a deep list of all inheritance/derived?
105
- // Map parent references of classes and interfaces...
106
- if ( 'augmentsNested' in doclet ) {
107
- const extendedTypes = doclet._signature.extendedTypes || [];
108
-
109
- doclet.augmentsNested = extendedTypes.flatMap( getHierarchyCallback( this._typeConverter ) );
110
- }
111
-
112
- // ...but also, the derived instances...
113
- if ( 'descendants' in doclet ) {
114
- const extendedBy = doclet._signature.extendedBy || [];
115
-
116
- doclet.descendants = extendedBy.flatMap( getHierarchyCallback( this._typeConverter ) );
117
- }
118
-
119
- // ...and implemented interfaces.
120
- if ( 'implementsNested' in doclet ) {
121
- const implementedTypes = doclet._signature.implementedTypes || [];
122
-
123
- doclet.implementsNested = implementedTypes.flatMap( getHierarchyCallback( this._typeConverter ) );
124
- }
125
-
126
107
  // Unify the `_signature` tuple, so all doclets use the same algorithm.
127
108
  if ( !Array.isArray( doclet._signature ) ) {
128
109
  doclet._signature = [ doclet._signature, doclet._signature ];
@@ -185,6 +166,92 @@ export class TypedocConverter {
185
166
  } );
186
167
  }
187
168
 
169
+ /**
170
+ * Fills the `augmentsNested`, `implementsNested`, and `descendants` properties of class and interface doclets
171
+ * with full inheritance chains. Typedoc signatures (`extendedTypes`, `implementedTypes`, `extendedBy`) describe
172
+ * only the first level of the hierarchy, so ancestor references are followed transitively through the converted
173
+ * doclets.
174
+ *
175
+ * The `augmentsNested` chain continues only through ancestors of the same kind. Ancestors of an interface mixed
176
+ * into a class hierarchy (an intersection in `extendedTypes`) extend the list of implemented interfaces instead
177
+ * of the inheritance chain rendered on a page. Descendants are calculated by inverting the resolved ancestor
178
+ * relations, so both directions of the hierarchy stay consistent.
179
+ *
180
+ * @protected
181
+ */
182
+ _buildHierarchy() {
183
+ const docletsByLongname = new Map( this._doclets.map( doclet => [ doclet.longname, doclet ] ) );
184
+ const hierarchyDoclets = this._doclets.filter( doclet => doclet._signature && 'augmentsNested' in doclet );
185
+ const relationsCache = new Map();
186
+
187
+ const resolveRelations = doclet => {
188
+ if ( relationsCache.has( doclet ) ) {
189
+ return relationsCache.get( doclet );
190
+ }
191
+
192
+ const relations = { augmentsNested: [], implementsNested: [] };
193
+
194
+ // Fill the cache upfront to break potential reference cycles.
195
+ relationsCache.set( doclet, relations );
196
+
197
+ const collect = ( references, relationName ) => {
198
+ for ( const reference of references.flatMap( flattenHierarchyReference ) ) {
199
+ const longname = this._typeConverter.convertReference( reference );
200
+
201
+ relations[ relationName ].push( longname );
202
+
203
+ const ancestor = docletsByLongname.get( longname );
204
+
205
+ if ( !ancestor || !ancestor._signature || !( 'augmentsNested' in ancestor ) ) {
206
+ continue;
207
+ }
208
+
209
+ const ancestorRelations = resolveRelations( ancestor );
210
+
211
+ if ( relationName === 'augmentsNested' && ancestor.kind === doclet.kind ) {
212
+ relations.augmentsNested.push( ...ancestorRelations.augmentsNested );
213
+ relations.implementsNested.push( ...ancestorRelations.implementsNested );
214
+ } else {
215
+ relations.implementsNested.push( ...ancestorRelations.augmentsNested, ...ancestorRelations.implementsNested );
216
+ }
217
+ }
218
+ };
219
+
220
+ collect( doclet._signature.extendedTypes || [], 'augmentsNested' );
221
+ collect( doclet._signature.implementedTypes || [], 'implementsNested' );
222
+
223
+ relations.augmentsNested = [ ...new Set( relations.augmentsNested ) ].filter( name => name !== doclet.longname );
224
+ relations.implementsNested = [ ...new Set( relations.implementsNested ) ].filter( name => name !== doclet.longname );
225
+
226
+ return relations;
227
+ };
228
+
229
+ for ( const doclet of hierarchyDoclets ) {
230
+ const relations = resolveRelations( doclet );
231
+
232
+ doclet.augmentsNested = relations.augmentsNested;
233
+
234
+ if ( 'implementsNested' in doclet ) {
235
+ doclet.implementsNested = relations.implementsNested;
236
+ }
237
+
238
+ doclet.descendants = [];
239
+ }
240
+
241
+ for ( const doclet of hierarchyDoclets ) {
242
+ const relations = resolveRelations( doclet );
243
+ const ancestors = new Set( [ ...relations.augmentsNested, ...relations.implementsNested ] );
244
+
245
+ for ( const longname of ancestors ) {
246
+ const ancestor = docletsByLongname.get( longname );
247
+
248
+ if ( ancestor && 'descendants' in ancestor && ancestor.descendants ) {
249
+ ancestor.descendants.push( doclet.longname );
250
+ }
251
+ }
252
+ }
253
+ }
254
+
188
255
  /**
189
256
  * @protected
190
257
  * @param {BaseReflection} reflection
@@ -964,17 +1031,18 @@ class TypeConverter {
964
1031
  }
965
1032
 
966
1033
  /**
967
- * @param {TypeConverter} converter
968
- * @returns {Function}
1034
+ * Flattens an intersection found in a hierarchy relation (for example, `class Foo extends Mixin( Base )` produces
1035
+ * `Base & SomeInterface` in `extendedTypes`) into a list of plain references.
1036
+ *
1037
+ * @param {AvailableTypes} reference
1038
+ * @returns {Array.<ReferenceType>}
969
1039
  */
970
- function getHierarchyCallback( converter ) {
971
- return reference => {
972
- if ( reference.type === 'intersection' ) {
973
- return reference.types.map( ref => converter.convertReference( ref ) );
974
- }
1040
+ function flattenHierarchyReference( reference ) {
1041
+ if ( reference.type === 'intersection' ) {
1042
+ return reference.types;
1043
+ }
975
1044
 
976
- return converter.convertReference( reference );
977
- };
1045
+ return [ reference ];
978
1046
  }
979
1047
 
980
1048
  function mapIndexSignatureObject( subItem, index ) {
@@ -107,6 +107,7 @@ export const projectLocals = ( ctx, {
107
107
  disableSearch,
108
108
  shouldInjectNoIndexMeta,
109
109
  og,
110
+ llmsTxtProjects,
110
111
  quickNavigationProjects,
111
112
  navigationShowEmptyCategories
112
113
  } = {} ) => {
@@ -197,5 +198,6 @@ export const projectLocals = ( ctx, {
197
198
  locals.isLts = /\/ckeditor5\/(lts-v\d+)(?:\/|$)/.test( `/${ locals.path }` );
198
199
 
199
200
  locals.mainOg = og || {};
201
+ locals.llmsTxtProjects = llmsTxtProjects || [];
200
202
  } );
201
203
  };
@@ -156,6 +156,7 @@ export const buildDocumentation = options => {
156
156
  extraStyles,
157
157
  extraScripts,
158
158
  og: mainConfig.og,
159
+ llmsTxtProjects: mainConfig.llmsTxtProjects || [],
159
160
  additionalDocumentation: mainConfig.additionalDocumentation,
160
161
  canonicalUrlBeginning: mainConfig.canonicalUrlBeginning,
161
162
  macrosVariables: mainConfig.variables,
@@ -321,6 +322,7 @@ async function buildProjects( rootPath, projectPaths, options = {} ) {
321
322
  vwo: options.vwo,
322
323
  feedbackWidget: options.feedbackWidget,
323
324
  og: options.og,
325
+ llmsTxtProjects: options.llmsTxtProjects,
324
326
  canonicalUrlBeginning: options.canonicalUrlBeginning,
325
327
  macrosVariables: options.macrosVariables,
326
328
  quickNavigationProjects: options.quickNavigationProjects
@@ -343,6 +345,7 @@ async function buildProjects( rootPath, projectPaths, options = {} ) {
343
345
  vwo: options.vwo,
344
346
  feedbackWidget: options.feedbackWidget,
345
347
  og: options.og,
348
+ llmsTxtProjects: options.llmsTxtProjects,
346
349
  canonicalUrlBeginning: options.canonicalUrlBeginning,
347
350
  macrosVariables: options.macrosVariables,
348
351
  quickNavigationProjects: options.quickNavigationProjects
@@ -405,6 +408,7 @@ async function buildProjects( rootPath, projectPaths, options = {} ) {
405
408
  extraScriptsPaths: extraScripts,
406
409
  disableSearch: options.disableSearch,
407
410
  og: options.og,
411
+ llmsTxtProjects: options.llmsTxtProjects,
408
412
  // Force displaying empty categories in the navigation tree when the documentation is being built partially.
409
413
  navigationShowEmptyCategories: Array.isArray( guidesToProcess )
410
414
  } );
@@ -515,6 +519,7 @@ async function buildApis( projectConfigs, options = {} ) {
515
519
  feedbackWidget: options.feedbackWidget,
516
520
  pathJoin: upath.join,
517
521
  mainOg: options.og || {},
522
+ llmsTxtProjects: options.llmsTxtProjects || [],
518
523
  isNightly: options.docSearch?.indexName?.includes( 'nightly' ) || false,
519
524
  isLts: /\/ckeditor5\/(lts-v\d+)(?:\/|$)/.test( `/${ BASE_PATH }` )
520
525
  },
@@ -596,7 +601,9 @@ function buildSdks( projectConfigs, options = {} ) {
596
601
  vwo: options.vwo,
597
602
  feedbackWidget: options.feedbackWidget,
598
603
  pathJoin: upath.join,
599
- mainOg: options.og || {}
604
+ mainOg: options.og || {},
605
+ llmsTxtProjects: options.llmsTxtProjects || [],
606
+ isLts: /\/ckeditor5\/(lts-v\d+)(?:\/|$)/.test( `/${ BASE_PATH }` )
600
607
  },
601
608
  projectConfig: config,
602
609
  canonicalUrlBeginning: getCanonicalBeginning( { config, options, hexoManager } )
@@ -26,17 +26,19 @@ mixin method( met )
26
26
  code.api-item-heading__code
27
27
  a.doc.b-link( class="member-name" href="#" + id ) #{ met.name }
28
28
 
29
- //- params ( param1, [param2] )
29
+ //- params ( param1, param2? )
30
30
  | (
31
31
  if isNonEmptyArray( met.params )
32
32
  each param, index in met.params
33
33
  if param.optional
34
- | [ #{ param.name } ]
34
+ | #{ param.name }?
35
35
  else
36
36
  | #{ param.name }
37
37
  if ( param.subParamsString )
38
38
  //- Allow rendering HTML from the `subParamsString` as it may contain links to other pages.
39
- | !{ param.subParamsString }
39
+ //- The string is prebuilt by the shared `api-builder` with the legacy `[name]`
40
+ //- convention for optional members, so convert it to the `name?` syntax here.
41
+ | !{ param.subParamsString.replace( /\[([\w$.]+)\]/g, '$1?' ) }
40
42
  if index < met.params.length - 1
41
43
  |,
42
44
  else
@@ -14,21 +14,28 @@ mixin paramsMixin ( params, level, title )
14
14
 
15
15
  dt
16
16
  code
17
+ //- Consecutive piped text lines are joined with a newline, which renders as
18
+ //- a space. To keep the TypeScript-style `name?: Type` notation, the colon
19
+ //- must live in the same text node as the name.
20
+ - const hasTypes = isNonEmptyArray( param.types );
21
+
17
22
  if isComputedProperty
18
23
  - const [ parentParam, nestedParam ] = param.name.split( '.' );
19
24
 
20
25
  | #{ parentParam }
21
26
  i
22
27
  | #{ nestedParam }
23
- else if param.optional
24
- | [ #{ param.name } ]
28
+ if hasTypes
29
+ | :!{ ' ' }
30
+ +type( param.types )
25
31
  else
26
- | #{ param.name }
32
+ - const displayName = param.optional ? `${ param.name }?` : param.name;
27
33
 
28
- if isNonEmptyArray( param.types )
29
- | :
30
- | !{ ' ' }
31
- +type( param.types )
34
+ if hasTypes
35
+ | #{ displayName }:!{ ' ' }
36
+ +type( param.types )
37
+ else
38
+ | #{ displayName }
32
39
 
33
40
  dd
34
41
  if ( param.description )
@@ -19,11 +19,11 @@ mixin property( prop )
19
19
  h3
20
20
  div.api-item-heading
21
21
  code.api-item-heading__code
22
- a.doc.b-link( class="member-name" href="#" + prop.id ) #{ prop.name }
22
+ a.doc.b-link( class="member-name" href="#" + prop.id ) #{ prop.optional ? prop.name + '?' : prop.name }
23
23
 
24
24
  if isNonEmptyArray( prop.types )
25
- | :!{' '}
26
- +type( prop.types, prop.optional )
25
+ | :!{' '}
26
+ +type( prop.types )
27
27
  if isNonEmptyArray( prop.badges )
28
28
  +tagWrapper()
29
29
  each badge in prop.badges
@@ -1,16 +1,12 @@
1
1
  include ./_link-or-text.pug
2
2
 
3
- mixin type( types, isOptional = false )
3
+ mixin type( types )
4
4
  each nameOrObject, index in types
5
5
  +renderType( nameOrObject )
6
6
 
7
7
  if index < types.length - 1
8
8
  | !{ ' | ' }
9
9
 
10
- if ( isOptional && !types.includes( 'undefined' ) )
11
- | !{ ' | ' }
12
- +renderType( 'undefined' )
13
-
14
10
  //- TODO: Description.
15
11
  //-
16
12
  //- @param {String|*} inlineObjectOrType
@@ -8,7 +8,7 @@ mixin typeParameter( type )
8
8
  //- We mark most TypeScript types as `object`, as their structure is complex.
9
9
  //- Hence, it does not really make sense to show that a type extends an unknown `object`.
10
10
  if ( type.value && type.value[ 0 ] !== 'object' )
11
- | !{' : '}
11
+ | !{': '}
12
12
  if ( type.isExtending )
13
13
  i !{ 'extends ' }
14
14
  +type( type.value )
@@ -18,7 +18,7 @@ if isNonEmptyArray( data.typeParameters )
18
18
  //- We mark most TypeScript types as `object`, as their structure is complex.
19
19
  //- Hence, it does not really make sense to show that a type extends an unknown `object`.
20
20
  if ( type.value && type.value[ 0 ] !== 'object' )
21
- | !{' : '}
21
+ | !{': '}
22
22
  if ( type.isExtending )
23
23
  i !{ 'extends ' }
24
24
  +type( type.value )
@@ -49,6 +49,26 @@ if ( page[ 'dc-description' ] || ogConfig.description )
49
49
  if page.BASE_PATH && page.BASE_PATH !== '.' && !page.BASE_PATH.includes( 'latest' )
50
50
  link( rel = 'canonical' href = page.canonicalUrlBeginning + page.path.replace( page.BASE_PATH, projectLocals.latestBasePath ) )
51
51
 
52
+ //- The `docs` build writes a Markdown counterpart for `*/latest/**` pages only. That rules out the LTS output,
53
+ //- and it is why these relations cannot sit inside the `canonical` guard above, which skips `latest` projects.
54
+ - const projects = llmsTxtProjects || [];
55
+ - const buildGeneratesMarkdown = projects.length > 0 && !isLts;
56
+ - const markdownProjectKey = page.BASE_PATH && page.BASE_PATH !== '.' ? page.BASE_PATH.split( '/' )[ 0 ] : null;
57
+ - const hasMarkdownVersion = markdownProjectKey && buildGeneratesMarkdown && !page.path.includes( '/getting-started/legacy/' );
58
+
59
+ if hasMarkdownVersion
60
+ link( rel = 'alternate' type = 'text/markdown' href = relative_url( page.path, page.path.replace( /\.html$/, '.md' ) ) )
61
+
62
+ if projects.includes( markdownProjectKey )
63
+ if page.groupId === 'api-reference'
64
+ link( rel = 'describedby' href = 'llms.txt' )
65
+ else
66
+ link( rel = 'describedby' href = relative_url( page.path, markdownProjectKey + '/llms.txt' ) )
67
+
68
+ //- The way in for an agent that arrives from the website rather than from an article.
69
+ else if !markdownProjectKey && mainOg && mainOg.hostname
70
+ link( rel = 'describedby' href = mainOg.hostname + '/llms.txt' )
71
+
52
72
  if page.isSnippetPage
53
73
  //UMBERTO: SNIPPET: HEAD
54
74