umberto 10.3.0 → 10.4.1

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,20 @@
1
1
  Changelog
2
2
  =========
3
3
 
4
+ ## [10.4.1](https://github.com/cksource/umberto/compare/v10.4.0...v10.4.1) (April 13, 2026)
5
+
6
+ ### Bug fixes
7
+
8
+ * Fixed URL in the dropdown for LTS item to have absolute URL to pass documentation validation.
9
+
10
+
11
+ ## [10.4.0](https://github.com/cksource/umberto/compare/v10.3.0...v10.4.0) (April 9, 2026)
12
+
13
+ ### Features
14
+
15
+ * Added the LTS edition to the docs product picker.
16
+
17
+
4
18
  ## [10.3.0](https://github.com/cksource/umberto/compare/v10.2.0...v10.3.0) (April 3, 2026)
5
19
 
6
20
  ### Features
@@ -33,20 +47,6 @@ Changelog
33
47
 
34
48
  * Filter search results from Algolia to match the current docs version: `latest` or `lts-v47`. Filtering remains backward-compatible: hits without the docs version are still allowed.
35
49
 
36
-
37
- ## [10.1.3](https://github.com/cksource/umberto/compare/v10.1.2...v10.1.3) (March 10, 2026)
38
-
39
- ### Bug fixes
40
-
41
- * Fix regression introduced in `v10.1.2` affecting slugs generated for headings. Slugs generated now should be the same as in `v10.1.1` and previous versions of Umberto.
42
-
43
-
44
- ## [10.1.2](https://github.com/cksource/umberto/compare/v10.1.1...v10.1.2) (March 9, 2026)
45
-
46
- ### Bug fixes
47
-
48
- * Removed unmaintained `hexo-renderer-markdown-it-plus` and `@ckeditor/jsdoc-plugins` dependencies by replacing them with maintained and local implementations, resolving the reported `markdown-it`-related vulnerabilities in Umberto's dependency tree.
49
-
50
50
  ---
51
51
 
52
52
  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": "10.3.0",
3
+ "version": "10.4.1",
4
4
  "description": "CKSource Documentation builder",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -161,6 +161,11 @@ export const buildDocumentation = options => {
161
161
  macrosVariables: mainConfig.variables,
162
162
  quickNavigationProjects: mainConfig.quickNavigationProjects || [
163
163
  { name: 'CKEditor 5', slug: 'ckeditor5' },
164
+ {
165
+ name: 'CKEditor 5 LTS Edition',
166
+ href: 'https://ckeditor.com/docs/ckeditor5/lts-v47/index.html',
167
+ rel: 'nofollow'
168
+ },
164
169
  { name: 'Cloud Services', slug: 'cs' },
165
170
  { name: 'CKBox', slug: 'ckbox' }
166
171
  ],
@@ -50,20 +50,22 @@ mixin dropdown({ id: dropdownId, className, preferredDirection, contentId, updat
50
50
  //- +dropdown-item({ label: 'Active item', active: true })
51
51
  //- +dropdown-item({ label: 'Item with value', value: 'custom-value' })
52
52
  //- +dropdown-item({ label: 'Item with icon', icon: 'github' })
53
- //- +dropdown-item({ label: 'Custom link', href: '/custom-link' })
53
+ //- +dropdown-item({ label: 'Custom link', href: '/custom-link', rel: 'nofollow' })
54
54
  //- +dropdown-item({ label: 'With ID', itemId: 'special-item' })
55
- mixin dropdown-item({ itemId, href, active = false, narrow, label, icon, value } = {})
55
+ mixin dropdown-item({ itemId, href, rel, active = false, narrow, label, icon, value, projectKey } = {})
56
56
  - const tag = href ? 'a' : 'div';
57
57
 
58
58
  li.c-dropdown__list-item
59
59
  #{tag}.b-reset-link.c-dropdown__item(
60
60
  role='menuitem',
61
61
  href=href,
62
+ rel=rel,
62
63
  class={
63
64
  'is-active': active,
64
65
  'c-dropdown__item--narrow': !!narrow
65
66
  },
66
67
  data-item-id=itemId,
68
+ data-project-key=projectKey,
67
69
  data-value=value,
68
70
  tabindex='-1'
69
71
  )
@@ -1,7 +1,21 @@
1
1
  mixin nav-project-select-dropdown()
2
2
  -
3
+ const getProjectKey = pathname => {
4
+ const segments = pathname.split( '/' ).filter( Boolean );
5
+
6
+ if ( segments[ 0 ] === 'docs' ) {
7
+ segments.shift();
8
+ }
9
+
10
+ const [ slug, rawChannel ] = segments;
11
+ const channel = /^\d+\.\d+\.\d+$/.test( rawChannel || '' ) ? 'latest' : ( rawChannel || 'latest' );
12
+
13
+ return slug ? `/${ slug }/${ channel }` : '';
14
+ };
15
+ const getProjectHref = project => project.href || relative_url( page.path, `${ project.slug }/${ project.channel || 'latest' }/index.html` );
16
+ const getItemProjectKey = project => getProjectKey( project.href ? new URL( project.href ).pathname : `/${ project.slug }/${ project.channel || 'latest' }/index.html` );
3
17
  const currentProject = projectLocals && quickNavigationProjects && quickNavigationProjects.find(
4
- project => project.slug === projectLocals.projectSlug
18
+ project => project.slug === projectLocals.projectSlug && !project.channel
5
19
  );
6
20
 
7
21
  +menu-dropdown({
@@ -10,7 +24,56 @@ mixin nav-project-select-dropdown()
10
24
  each project in quickNavigationProjects
11
25
  +dropdown-item({
12
26
  label: project.name,
13
- active: !!currentProject && project.slug === currentProject.slug,
14
- href: relative_url(page.path, `${ project.slug }/latest/index.html`)
27
+ active: !!currentProject &&
28
+ project.slug === currentProject.slug &&
29
+ ( project.channel || 'latest' ) === 'latest',
30
+ href: getProjectHref( project ),
31
+ rel: project.rel,
32
+ projectKey: getItemProjectKey( project )
15
33
  })
16
34
 
35
+ if currentProject
36
+ script.
37
+ (function() {
38
+ const script = document.currentScript;
39
+ const getProjectKey = pathname => {
40
+ const segments = pathname.split( '/' ).filter( Boolean );
41
+
42
+ if ( segments[ 0 ] === 'docs' ) {
43
+ segments.shift();
44
+ }
45
+
46
+ const [ slug, rawChannel ] = segments;
47
+ const channel = /^\d+\.\d+\.\d+$/.test( rawChannel || '' ) ? 'latest' : ( rawChannel || 'latest' );
48
+
49
+ return slug ? `/${ slug }/${ channel }` : '';
50
+ };
51
+
52
+ document.addEventListener( 'DOMContentLoaded', () => {
53
+ const dropdown = script?.previousElementSibling;
54
+ const currentProjectKey = getProjectKey( window.location.pathname );
55
+
56
+ if ( !dropdown || !currentProjectKey ) {
57
+ return;
58
+ }
59
+
60
+ const items = Array.from( dropdown.querySelectorAll( '.c-dropdown__item[data-project-key]' ) );
61
+ const activeItem = items.find( item => item.dataset.projectKey === currentProjectKey );
62
+
63
+ if ( !activeItem ) {
64
+ return;
65
+ }
66
+
67
+ for ( const item of items ) {
68
+ item.classList.toggle( 'is-active', item === activeItem );
69
+ }
70
+
71
+ const labelElement = dropdown.querySelector( '.c-menu-dropdown__button .c-button__label' );
72
+ const labelText = activeItem.querySelector( 'span' )?.textContent?.trim() || activeItem.textContent.trim();
73
+
74
+ if ( labelElement && labelText ) {
75
+ labelElement.textContent = labelText;
76
+ }
77
+ }, { once: true } );
78
+ })();
79
+
@@ -3,6 +3,20 @@ include ./slide-parts
3
3
  mixin mobile-nav()
4
4
  -
5
5
  const currentProject = projectLocals && projectsData.find( project => project.slug === projectLocals.projectSlug );
6
+ const getProjectKey = pathname => {
7
+ const segments = pathname.split( '/' ).filter( Boolean );
8
+
9
+ if ( segments[ 0 ] === 'docs' ) {
10
+ segments.shift();
11
+ }
12
+
13
+ const [ slug, rawChannel ] = segments;
14
+ const channel = /^\d+\.\d+\.\d+$/.test( rawChannel || '' ) ? 'latest' : ( rawChannel || 'latest' );
15
+
16
+ return slug ? `/${ slug }/${ channel }` : '';
17
+ };
18
+ const getProjectHref = project => project.href || relative_url( page.path, `${ project.slug }/${ project.channel || 'latest' }/index.html` );
19
+ const getItemProjectKey = project => getProjectKey( project.href ? new URL( project.href ).pathname : `/${ project.slug }/${ project.channel || 'latest' }/index.html` );
6
20
  let initialSlideId = projectLocals ? `project-${projectLocals.projectSlug}` : 'products';
7
21
 
8
22
  // If the page has a category, set the initial slide to that category's slide
@@ -23,20 +37,60 @@ mixin mobile-nav()
23
37
  if quickNavigationProjects
24
38
  ol.b-reset-list.c-mobile-nav__items
25
39
  each project in quickNavigationProjects
26
- - isCurrentProject = projectLocals && project.slug === projectLocals.projectSlug
27
-
40
+ - isCurrentProject = projectLocals && project.slug === projectLocals.projectSlug && !project.channel
28
41
  if isCurrentProject
29
42
  +mobile-nav-item({
30
43
  label: project.name,
31
44
  active: true,
32
- targetSlideId: `project-${ project.slug }`
45
+ targetSlideId: `project-${ project.slug }`,
46
+ projectKey: getItemProjectKey( project )
33
47
  })
34
48
  else
35
49
  +mobile-nav-item({
36
- href: relative_url(page.path, `${project.slug}/latest/index.html`),
37
- label: project.name
50
+ href: getProjectHref( project ),
51
+ label: project.name,
52
+ rel: project.rel,
53
+ projectKey: getItemProjectKey( project )
38
54
  })
39
55
 
56
+ if currentProject
57
+ script.
58
+ (function() {
59
+ const script = document.currentScript;
60
+ const getProjectKey = pathname => {
61
+ const segments = pathname.split( '/' ).filter( Boolean );
62
+
63
+ if ( segments[ 0 ] === 'docs' ) {
64
+ segments.shift();
65
+ }
66
+
67
+ const [ slug, rawChannel ] = segments;
68
+ const channel = /^\d+\.\d+\.\d+$/.test( rawChannel || '' ) ? 'latest' : ( rawChannel || 'latest' );
69
+
70
+ return slug ? `/${ slug }/${ channel }` : '';
71
+ };
72
+
73
+ document.addEventListener( 'DOMContentLoaded', () => {
74
+ const mobileNav = script?.previousElementSibling;
75
+ const currentProjectKey = getProjectKey( window.location.pathname );
76
+
77
+ if ( !mobileNav || !currentProjectKey ) {
78
+ return;
79
+ }
80
+
81
+ const items = Array.from( mobileNav.querySelectorAll( '.c-mobile-nav__item[data-project-key]' ) );
82
+ const activeItem = items.find( item => item.dataset.projectKey === currentProjectKey );
83
+
84
+ if ( !activeItem ) {
85
+ return;
86
+ }
87
+
88
+ for ( const item of items ) {
89
+ item.classList.toggle( 'is-active', item === activeItem );
90
+ }
91
+ }, { once: true } );
92
+ })();
93
+
40
94
  //- Current project slide
41
95
  if projectLocals
42
96
  +mobile-nav-slide({ slideId: `project-${ projectLocals.projectSlug }` })
@@ -32,7 +32,7 @@ mixin mobile-nav-heading({ href, label })
32
32
  iconPosition: 'right',
33
33
  })
34
34
 
35
- mixin mobile-nav-item({ href, label, targetSlideId, target, active = false })
35
+ mixin mobile-nav-item({ href, label, rel, targetSlideId, target, active = false, projectKey })
36
36
  li
37
37
  if targetSlideId
38
38
  +button({
@@ -48,11 +48,14 @@ mixin mobile-nav-item({ href, label, targetSlideId, target, active = false })
48
48
  'data-navigate-direction'='forward',
49
49
  'aria-controls'=`slide-${targetSlideId}`,
50
50
  'aria-expanded'='false',
51
- 'aria-haspopup'='true'
51
+ 'aria-haspopup'='true',
52
+ 'data-project-key'=projectKey
52
53
  )
53
54
  else
54
55
  a.b-link.c-mobile-nav__item(
55
56
  href=href,
57
+ rel=rel,
58
+ data-project-key=projectKey,
56
59
  class=active ? 'is-active' : '',
57
60
  target=target
58
61
  )= label