umberto 8.0.0 → 8.0.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +26 -18
  2. package/package.json +1 -1
  3. package/scripts/filter/after-post-render/gloria/append-copy-heading-buttons.js +17 -26
  4. package/scripts/filter/after-post-render/parseicontag.js +1 -1
  5. package/scripts/filter/after-render/gloria/inline-svg.js +1 -1
  6. package/scripts/filter/after-render/gloria/spritesheet-svg.js +1 -1
  7. package/scripts/filter/before-post-render/gloria/add-breadcrumbs-data-to-page.js +9 -2
  8. package/scripts/filter/before-post-render/gloria/prerender-xml-pug-components.js +13 -0
  9. package/scripts/filter/before-post-render/gloria/render-post-render-pug-components.js +14 -6
  10. package/scripts/helper/u-extract-and-cache-title.js +36 -0
  11. package/scripts/utils/pug-renderer/create-prerender-pug-template.js +18 -5
  12. package/scripts/utils/random-id.js +8 -0
  13. package/src/api-builder/classes/description-parser.js +18 -17
  14. package/src/hexo/filter/project-locals.js +3 -0
  15. package/src/tasks/build-api-docs.js +1 -1
  16. package/src/tasks/build-documentation.js +13 -3
  17. package/src/tasks/validate-links.js +211 -39
  18. package/themes/umberto/layout/gloria/_api-docs/_mixin/_api-tree-item.pug +10 -2
  19. package/themes/umberto/layout/gloria/_api-docs/_toc/_style.scss +3 -1
  20. package/themes/umberto/layout/gloria/_components/code-block/_style.scss +2 -2
  21. package/themes/umberto/layout/gloria/_components/code-block/index.pug +10 -2
  22. package/themes/umberto/layout/gloria/_components/code-switcher/_style.scss +0 -5
  23. package/themes/umberto/layout/gloria/_components/code-switcher/index.pug +3 -3
  24. package/themes/umberto/layout/gloria/_components/icon-message/index.pug +5 -5
  25. package/themes/umberto/layout/gloria/_components/index.pug +1 -0
  26. package/themes/umberto/layout/gloria/_components/nav-tree/index.pug +1 -1
  27. package/themes/umberto/layout/gloria/_components/nav-tree/nav-tree-item.pug +11 -7
  28. package/themes/umberto/layout/gloria/_components/nav-tree/nav-tree-level.pug +3 -3
  29. package/themes/umberto/layout/gloria/_components/snippet-footer/_style.scss +26 -0
  30. package/themes/umberto/layout/gloria/_components/snippet-footer/index.pug +10 -0
  31. package/themes/umberto/layout/gloria/_components/svg/index.pug +1 -1
  32. package/themes/umberto/layout/gloria/_modules/algolia-search/index.pug +1 -0
  33. package/themes/umberto/layout/gloria/_modules/header/nav-project-select-dropdown.pug +8 -8
  34. package/themes/umberto/layout/gloria/_modules/header-nightly-info/index.pug +1 -0
  35. package/themes/umberto/layout/gloria/_modules/mobile-nav/index.pug +17 -17
  36. package/themes/umberto/layout/gloria/_modules/toc/_style.scss +1 -1
  37. package/themes/umberto/layout/gloria/api.pug +0 -1
  38. package/themes/umberto/source/gloria/assets/_fonts/Lato/Lato-Bold.ttf +0 -0
  39. package/themes/umberto/source/gloria/assets/_fonts/Lato/Lato-Bold.woff2 +0 -0
  40. package/themes/umberto/source/gloria/assets/_fonts/Lato/Lato-BoldItalic.ttf +0 -0
  41. package/themes/umberto/source/gloria/assets/_fonts/Lato/Lato-BoldItalic.woff2 +0 -0
  42. package/themes/umberto/source/gloria/assets/_fonts/Lato/Lato-Italic.ttf +0 -0
  43. package/themes/umberto/source/gloria/assets/_fonts/Lato/Lato-Italic.woff2 +0 -0
  44. package/themes/umberto/source/gloria/assets/_fonts/Lato/Lato-Regular.ttf +0 -0
  45. package/themes/umberto/source/gloria/assets/_fonts/Lato/Lato-Regular.woff2 +0 -0
  46. package/themes/umberto/source/gloria/assets/_img/icons/sparkles.svg +1 -0
  47. package/themes/umberto/src/gloria/css/_fonts.scss +39 -0
  48. package/themes/umberto/src/gloria/css/base/_inline-code.scss +4 -3
  49. package/themes/umberto/src/gloria/css/base/_links.scss +2 -1
  50. package/themes/umberto/src/gloria/css/base/_tables.scss +0 -4
  51. package/themes/umberto/src/gloria/css/components/_api-collapsing-list.scss +4 -0
  52. package/themes/umberto/src/gloria/css/components/_index.scss +2 -1
  53. package/themes/umberto/src/gloria/css/doc/_snippets.scss +1 -0
  54. package/themes/umberto/src/gloria/css/layout/_base.scss +2 -12
  55. package/themes/umberto/src/gloria/css/utilities/_spacing.scss +1 -0
  56. package/themes/umberto/src/gloria/js/components/code-block.js +7 -4
  57. package/themes/umberto/src/gloria/js/modules/api-filter.js +9 -4
@@ -5,42 +5,51 @@
5
5
 
6
6
  'use strict';
7
7
 
8
+ /**
9
+ * Script for validating links in generated HTML files.
10
+ * Uses multiple threads (worker_threads) for parallel analysis, significantly speeding up the build process,
11
+ * especially for API documentation where the number of links can reach thousands.
12
+ */
13
+
8
14
  const { styleText } = require( 'util' );
9
15
  const upath = require( 'upath' );
10
16
  const fs = require( 'fs-extra' );
17
+ const os = require( 'os' );
18
+ const { Worker, isMainThread, parentPort, workerData } = require( 'worker_threads' );
11
19
  const { globSync } = require( 'glob' );
12
20
  const { parseDocument } = require( 'htmlparser2' );
21
+ const { isMaskedID } = require( '../../scripts/utils/random-id' );
13
22
 
14
- module.exports = ( buildPath, options = {} ) => {
15
- const pattern = upath.join( buildPath, '**', '*' );
16
- let pathsToFiles = globSync( pattern ).map( path => upath.normalize( path ) );
17
-
23
+ /**
24
+ * Collects all links from the provided HTML files.
25
+ * Skips files from the vendors directory and (optionally) api.
26
+ * Adds links to files and their fragments (#id).
27
+ *
28
+ * @param {string[]} pathsToFiles Array of file paths to process.
29
+ * @param {Object} options Options for filtering files.
30
+ * @returns {{links: Set<string>, pathsToFiles: string[]}}
31
+ */
32
+ function collectLinks( pathsToFiles, options ) {
18
33
  const links = new Set();
19
-
20
34
  pathsToFiles = pathsToFiles.filter( p => {
21
35
  if ( p.includes( '/vendors/' ) ) {
22
36
  return false;
23
37
  }
24
38
 
25
- if ( options.skipApi && ( p.includes( '/api/' ) ) ) {
39
+ if ( options.skipApi && p.includes( '/api/' ) ) {
26
40
  return false;
27
41
  }
28
42
 
29
43
  if ( !p.endsWith( '.html' ) ) {
30
- // Add assets like .png, .xlsx, .docx as valid links.
31
44
  const resolvePath = upath.resolve( p );
32
-
33
45
  if ( fs.statSync( resolvePath ).isFile() ) {
34
46
  links.add( upath.resolve( resolvePath ) );
35
47
  }
36
-
37
48
  return false;
38
49
  }
39
-
40
50
  return true;
41
51
  } );
42
52
 
43
- // Obtain every possible hash
44
53
  for ( const filePath of pathsToFiles ) {
45
54
  links.add( upath.resolve( filePath ) );
46
55
  links.add( upath.resolve( filePath + '#' ) );
@@ -49,27 +58,40 @@ module.exports = ( buildPath, options = {} ) => {
49
58
  const ids = ( content.match( /id="[^"]+"/g ) || [] ).map( el => el.replace( /^id="/, '' ).replace( /"$/, '' ) );
50
59
 
51
60
  for ( const id of ids ) {
52
- links.add( upath.resolve( `${ filePath }#${ id }` ) );
61
+ if ( !isMaskedID( id ) && !ids.includes( 'icons-' ) ) {
62
+ links.add( upath.resolve( `${ filePath }#${ id }` ) );
63
+ }
53
64
  }
54
65
  }
55
66
 
56
- // Check if links are in set of possible hashes.
57
- for ( const filePath of pathsToFiles ) {
58
- const invalidHrefs = new Set();
67
+ return {
68
+ links: Array.from( links ),
69
+ pathsToFiles
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Validates links in the given chunk of files.
75
+ * Checks if links point to existing files or fragments.
76
+ * Returns a list of invalid links.
77
+ *
78
+ * @param {Object} params
79
+ * @param {string[]} params.chunk Array of file paths to validate.
80
+ * @param {Set<string>} params.links Set of valid links.
81
+ * @param {Object} params.options Validation options.
82
+ * @returns {Array<Object>}
83
+ */
84
+ function validateChunk( { chunk, links, options } ) {
85
+ const errors = [];
86
+
87
+ for ( const filePath of chunk ) {
88
+ const invalidHrefs = [];
59
89
  const content = fs.readFileSync( filePath, 'utf-8' );
60
90
  const linkElements = [];
61
91
 
62
92
  processNode( parseDocument( content ), linkElements );
63
93
 
64
- for ( let { href, text, skipValidation } of linkElements ) {
65
- if (
66
- href.match( /[a-z:]*\/\// ) ||
67
- href.match( /mailto:/ ) ||
68
- skipValidation
69
- ) {
70
- continue;
71
- }
72
-
94
+ for ( let { href, text } of linkElements ) {
73
95
  if ( href.endsWith( '/' ) && !href.includes( '#' ) ) {
74
96
  href += 'index.html';
75
97
  }
@@ -77,8 +99,6 @@ module.exports = ( buildPath, options = {} ) => {
77
99
  let resolvedPath = getResolvedPath( href, filePath, { publicDir: options.publicDir } );
78
100
 
79
101
  if ( options.skipApi && resolvedPath.includes( '/api/' ) ) {
80
- // Sometimes api url are obtained after resolving entire URL.
81
-
82
102
  continue;
83
103
  }
84
104
 
@@ -87,7 +107,6 @@ module.exports = ( buildPath, options = {} ) => {
87
107
  options.projectsInfo.find( i => resolvedPath.includes( `/${ i.slug }/` ) ) :
88
108
  null;
89
109
  const projectVersion = projectInfo ? projectInfo.version : 'latest';
90
-
91
110
  resolvedPath = resolvedPath.replace( 'latest', projectVersion );
92
111
  }
93
112
 
@@ -100,32 +119,140 @@ module.exports = ( buildPath, options = {} ) => {
100
119
  continue;
101
120
  }
102
121
  }
103
-
104
122
  if ( !links.has( resolvedPath ) ) {
105
- invalidHrefs.add( { href, text } );
123
+ invalidHrefs.push( { href, text, filePath } );
106
124
  }
107
125
  }
108
126
 
109
- if ( invalidHrefs.size ) {
110
- process.exitCode = 1;
127
+ if ( invalidHrefs.length ) {
128
+ errors.push( { filePath, invalidHrefs } );
129
+ }
130
+ }
131
+
132
+ return errors;
133
+ }
134
+
135
+ /**
136
+ * Main function executed in the main thread.
137
+ * Splits files into chunks and runs worker threads for each chunk.
138
+ * Collects results and prints errors.
139
+ *
140
+ * @param {string} buildPath Path to the build directory.
141
+ * @param {Object} [options={}] Validation options.
142
+ */
143
+ if ( isMainThread ) {
144
+ const validateLinks = ( buildPath, options = {} ) => {
145
+ const pattern = upath.join( buildPath, '**', '*' );
146
+ const pathsToFiles = globSync( pattern ).map( path => upath.normalize( path ) );
147
+
148
+ console.info( `Validating links in ${ styleText( 'magenta', buildPath ) }...` );
149
+
150
+ const start = Date.now();
151
+ const { links, pathsToFiles: filteredPaths } = collectLinks( pathsToFiles, options );
152
+
153
+ const cpuCount = Math.max( 1, os.cpus().length );
154
+ const maxWorkers = 8;
155
+ const workerCount = Math.min( cpuCount, maxWorkers );
156
+ const chunks = chunkBasedOnMemory( workerCount, filteredPaths );
157
+
158
+ let finished = 0;
159
+ let allErrors = [];
160
+ let nextChunkIdx = 0;
161
+ const totalChunks = chunks.length;
162
+
163
+ return new Promise( ( resolve, reject ) => {
164
+ const workers = [];
165
+
166
+ function startWorker( workerId ) {
167
+ if ( nextChunkIdx >= totalChunks ) {
168
+ return;
169
+ }
170
+ const chunkIdx = nextChunkIdx++;
171
+ const worker = new Worker( __filename, {
172
+ workerData: {
173
+ chunk: chunks[ chunkIdx ],
174
+ links,
175
+ buildPath,
176
+ options
177
+ }
178
+ } );
179
+
180
+ worker.on( 'message', errors => {
181
+ allErrors = allErrors.concat( errors );
182
+ finished++;
183
+ console.info( `Processed ${ finished } of ${ totalChunks } links chunks.` );
184
+
185
+ if ( nextChunkIdx < totalChunks ) {
186
+ startWorker( workerId ); // Start next chunk on this worker
187
+ } else if ( finished === totalChunks ) {
188
+ printErrors( allErrors, buildPath );
189
+ console.info( `Links validation finished in ${ styleText( 'green', ( Date.now() - start ) + 'ms' ) }.` );
190
+
191
+ if ( allErrors.length ) {
192
+ reject( new Error( `Found ${ allErrors.length } invalid links.` ) );
193
+ } else {
194
+ resolve();
195
+ }
196
+ }
197
+ } );
198
+
199
+ worker.on( 'error', err => {
200
+ finished++;
201
+ console.error( styleText( 'red', `Worker error: ${ err }` ) );
202
+ if ( nextChunkIdx < totalChunks ) {
203
+ startWorker( workerId );
204
+ } else if ( finished === totalChunks ) {
205
+ reject( new Error( `Worker error: ${ err }` ) );
206
+ }
207
+ } );
208
+ workers[ workerId ] = worker;
209
+ }
210
+
211
+ const numWorkers = Math.min( workerCount, totalChunks );
212
+ for ( let i = 0; i < numWorkers; i++ ) {
213
+ startWorker( i );
214
+ }
215
+ } );
216
+ };
217
+
218
+ module.exports = validateLinks;
219
+ } else {
220
+ const { chunk, links, options } = workerData;
221
+ const errors = validateChunk( { chunk, links: new Set( links ), options } );
222
+
223
+ parentPort.postMessage( errors );
224
+ }
225
+
226
+ /**
227
+ * Prints errors found during link validation.
228
+ * Sets exit code to 1 if errors occurred.
229
+ *
230
+ * @param {Array<Object>} allErrors
231
+ * @param {string} buildPath
232
+ */
233
+ function printErrors( allErrors, buildPath ) {
234
+ if ( allErrors.length ) {
235
+ process.exitCode = 1;
236
+ for ( const { filePath, invalidHrefs } of allErrors ) {
111
237
  console.log(
112
238
  styleText( 'red', 'Error: ' ) +
113
239
  `Invalid internal links in ${ styleText( 'magenta', filePath.replace( buildPath, '' ) ) }:`
114
240
  );
115
-
116
241
  for ( const url of invalidHrefs ) {
117
242
  console.log( styleText( 'gray', ` * '${ url.href }' - ${ url.text.replace( /\s+/g, ' ' ) }` ) );
118
243
  }
119
244
  }
120
245
  }
121
- };
246
+ }
122
247
 
123
248
  /**
124
- * @param {String} href A relative or absolute URL to the page.
125
- * @param {String} filePath A relative path (to the options.publicDir) to the checked file.
249
+ * Resolves the target path for the given href relative to the file and public directory.
250
+ *
251
+ * @param {string} href
252
+ * @param {string} filePath
126
253
  * @param {Object} options
127
- * @param {String} options.publicDir An absolute path to the directory where all files are located.
128
- * @return {string}
254
+ * @param {string} options.publicDir
255
+ * @returns {string}
129
256
  */
130
257
  function getResolvedPath( href, filePath, options ) {
131
258
  if ( href.startsWith( '#' ) ) {
@@ -146,14 +273,20 @@ function getResolvedPath( href, filePath, options ) {
146
273
  return upath.resolve( filePath, '..', href );
147
274
  }
148
275
 
276
+ /**
277
+ * Recursively processes the DOM tree, collecting <a> elements with href.
278
+ *
279
+ * @param {Object} node
280
+ * @param {Array<Object>} linkElements
281
+ */
149
282
  function processNode( node, linkElements ) {
150
283
  if ( node.type === 'tag' && node.name === 'a' ) {
151
284
  const text = node.children.find( child => child.type === 'text' )?.data;
152
285
  const href = node.attribs?.href;
153
286
  const skipValidation = node.attribs?.[ 'data-skip-validation' ] !== undefined;
154
287
 
155
- if ( text && href ) {
156
- linkElements.push( { href, text, skipValidation } );
288
+ if ( !skipValidation && text && href && !href.match( /[a-z:]*\/\// ) && !href.match( /mailto:/ ) ) {
289
+ linkElements.push( { href, text } );
157
290
  }
158
291
  }
159
292
 
@@ -165,3 +298,42 @@ function processNode( node, linkElements ) {
165
298
  processNode( childNode, linkElements );
166
299
  }
167
300
  }
301
+
302
+ /**
303
+ * Calculates the optimal chunk count for splitting files based on available system memory.
304
+ * Tries to keep each chunk small enough to avoid OOM, but not too small for efficiency.
305
+ */
306
+ function chunkBasedOnMemory( maxWorkers, array ) {
307
+ const totalMem = os.totalmem();
308
+ const freeMem = os.freemem();
309
+
310
+ // Assume each file may take up to 8MB in memory (parsing, DOM, etc.)
311
+ const perFileEstimate = 8 * 1024 * 1024;
312
+
313
+ // Use only a portion of free memory to be safe (e.g., 60%)
314
+ const usableMem = Math.max( freeMem, totalMem * 0.2 ) * 0.6;
315
+ const maxFilesPerChunk = Math.max( 1, Math.floor( usableMem / perFileEstimate ) );
316
+
317
+ // Try to keep chunks small, but not less than maxWorkers
318
+ const chunkCount = Math.max( maxWorkers, Math.ceil( array.length / maxFilesPerChunk ) );
319
+
320
+ return chunkArray( array, chunkCount );
321
+ }
322
+
323
+ /**
324
+ * Splits an array into chunkCount fragments of similar size.
325
+ *
326
+ * @param {Array} array
327
+ * @param {number} chunkCount
328
+ * @returns {Array<Array>}
329
+ */
330
+ function chunkArray( array, chunkCount ) {
331
+ const chunks = [];
332
+ const chunkSize = Math.ceil( array.length / chunkCount );
333
+
334
+ for ( let i = 0; i < array.length; i += chunkSize ) {
335
+ chunks.push( array.slice( i, i + chunkSize ) );
336
+ }
337
+
338
+ return chunks;
339
+ }
@@ -20,7 +20,11 @@ mixin treeItem( node, level )
20
20
 
21
21
  if !hasChildren
22
22
  div.api-tree__item-wrapper
23
- a.api-tree__link( href=link class=iconClassName )
23
+ a.api-tree__link(
24
+ href=link,
25
+ class=iconClassName,
26
+ data-skip-validation=''
27
+ )
24
28
  div( class = wrapperClass data-ln = longname )
25
29
  span( class = classStr )
26
30
  | #{ node.getProp( 'name' ) }
@@ -28,7 +32,11 @@ mixin treeItem( node, level )
28
32
  div( class = 'api-tree__item-wrapper' data-ln = longname )
29
33
  button.api-tree__button.api-tree__button--chevron.js-sidebar-button( type="button", 'aria-label'='Expand/Collapse '+longname )
30
34
  if ( link )
31
- a( class = [ classStr, 'api-tree__link', iconClassName ] href=link )
35
+ a(
36
+ class = [ classStr, 'api-tree__link', iconClassName ],
37
+ href=link,
38
+ data-skip-validation=''
39
+ )
32
40
  | #{ node.getProp( 'name' ) }
33
41
  else
34
42
  span( class = [ classStr, iconClassName, isFolder ? 'js-sidebar-button' : ''] )
@@ -107,7 +107,7 @@
107
107
 
108
108
  &__heading {
109
109
  margin-bottom: var(--spacing-3);
110
- font-size: 1rem;
110
+ font-size: var(--font-size-base);
111
111
  font-weight: var(--font-weight-bold);
112
112
  color: var(--color-secondary-700);
113
113
 
@@ -128,7 +128,9 @@
128
128
  margin-top: var(--spacing-3);
129
129
 
130
130
  &-icon {
131
+ height: auto;
131
132
  transition: transform var(--duration-normal) ease;
133
+ transform-origin: center;
132
134
  }
133
135
 
134
136
  &--open {
@@ -71,8 +71,8 @@
71
71
  position: absolute;
72
72
  width: 28px;
73
73
  height: 28px;
74
- top: var(--spacing-4);
75
- right: var(--spacing-4);
74
+ top: var(--spacing-3-5);
75
+ right: var(--spacing-3-5);
76
76
  padding: var(--spacing-1);
77
77
  font-size: var(--font-size-lg);
78
78
  opacity: 0.6;
@@ -3,7 +3,7 @@
3
3
  //- Examples:
4
4
  //- +code-block({ language: 'js', code: 'console.log("Hello world!");' })
5
5
  //- +code-block({ language: 'typescript', code: 'const message: string = "Hello world!";\nconsole.log(message);', className: 'my-code-block' })
6
- mixin code-block({ id: codeBlockId, language = 'js', code = '', highlightOnMount = true, className, copyable = true , scrollable = false })
6
+ mixin code-block({ id: codeBlockId, language = 'js', code = '', skeletonLines, highlightOnMount = true, className, copyable = true , scrollable = false })
7
7
  -
8
8
  const id = codeBlockId || uRandomId( 'code-block' );
9
9
  let safeCode = (
@@ -38,7 +38,15 @@ mixin code-block({ id: codeBlockId, language = 'js', code = '', highlightOnMount
38
38
  const widths = [ '70%', '80%', '60%', '75%', '45%', '80%', '65%', '90%', '70%', '50%' ];
39
39
  const approxCodeLineScale = 0.75; // Approximate scale for code lines to match the height of the code block.
40
40
 
41
- let codeLines = safeCode ? Math.max( 1, Math.floor( safeCode.trim().split('\n').length * approxCodeLineScale ) ) : 0;
41
+ let codeLines = skeletonLines || 0;
42
+
43
+ if ( !codeLines ) {
44
+ if ( safeCode ) {
45
+ codeLines = Math.max( 1, Math.floor( safeCode.trim().split('\n').length * approxCodeLineScale ) );
46
+ } else {
47
+ codeLines = 4;
48
+ }
49
+ }
42
50
 
43
51
  if (scrollable) {
44
52
  codeLines = Math.min( codeLines, 5 );
@@ -31,11 +31,6 @@
31
31
  .c-code-block__copy-button {
32
32
  right: var(--spacing-5);
33
33
  }
34
-
35
- .c-code-block__skeleton,
36
- .c-code-block__pre {
37
- padding-bottom: var(--spacing-4);
38
- }
39
34
  }
40
35
  }
41
36
  }
@@ -5,7 +5,7 @@
5
5
  //- +code-tab({ id: 'js', label: 'JavaScript', active: true, code: 'console.log("Hello world!");', language: 'js' })
6
6
  //- +code-tab({ id: 'ts', label: 'TypeScript', code: 'const message: string = "Hello world!";\nconsole.log(message);', language: 'typescript' })
7
7
  //- +code-tab({ id: 'loading', label: 'Loading', skeleton: true })
8
- mixin code-switcher({ id: codeSwitcherId, className, storageSwitcherKey } = {})
8
+ mixin code-switcher({ id: codeSwitcherId, className, skeletonLines, storageSwitcherKey } = {})
9
9
  mixin code-tab({ id, label, tooltip, className, active = false, code, language = 'js', skeleton = false, copyable = false })
10
10
  - const loading = !!skeleton || !code;
11
11
 
@@ -18,9 +18,9 @@ mixin code-switcher({ id: codeSwitcherId, className, storageSwitcherKey } = {})
18
18
  tooltip
19
19
  })
20
20
  if loading
21
- +code-block({ code: '', language, copyable, highlightOnMount: false, scrollable: true })
21
+ +code-block({ code: '', language, copyable, skeletonLines, highlightOnMount: false, scrollable: true })
22
22
  else
23
- +code-block({ code, language, copyable, scrollable: true })
23
+ +code-block({ code, language, copyable, skeletonLines, scrollable: true })
24
24
 
25
25
  .c-code-switcher(
26
26
  id=codeSwitcherId
@@ -3,15 +3,15 @@
3
3
  //- Examples:
4
4
  //- +icon-message({ icon: 'github', iconPosition: 'left' }) Icon message left
5
5
  //- +icon-message({ icon: 'github', iconPosition: 'right' }) Icon message right
6
- mixin icon-message({ icon, iconSize, iconPosition = 'left', className })
6
+ mixin icon-message({ icon, iconSize, iconPosition = 'left', className, iconClassName, contentClassName })
7
7
  span.c-icon-message(class=className)&attributes(attributes)
8
8
  if iconPosition === 'left'
9
- span.c-icon-message__icon
9
+ span.c-icon-message__icon(class=iconClassName)
10
10
  +svg-icon({ icon, size: iconSize, ariaHidden: true })
11
- span.c-icon-message__content
11
+ span.c-icon-message__content(class=contentClassName)
12
12
  block
13
13
  else
14
- span.c-icon-message__content
14
+ span.c-icon-message__content(class=contentClassName)
15
15
  block
16
- span.c-icon-message__icon
16
+ span.c-icon-message__icon(class=iconClassName)
17
17
  +svg-icon({ icon, size: iconSize, ariaHidden: true })
@@ -29,3 +29,4 @@ include ./fake-devtools/index
29
29
  include ./iframe/index
30
30
  include ./tag/index
31
31
  include ./columns/index
32
+ include ./snippet-footer/index
@@ -3,4 +3,4 @@ include ./nav-tree-level
3
3
  mixin nav-tree()
4
4
  - const pageGroup = projectLocals.getPageGroup( page.groupId )
5
5
  ul.b-reset-list.c-nav-tree.js-nav-tree
6
- +navTreeLevel( pageGroup, true, 0 )
6
+ +navTreeLevel({ data: pageGroup })
@@ -2,19 +2,23 @@ mixin navTreeItem( data )
2
2
  -
3
3
  const activeClass = page.path === data.path ? 'c-nav-tree__link--active' : '';
4
4
  const ariaCurrent = page.path === data.path ? 'page' : false;
5
- const menuTitle = data[ 'menu-title' ] || uSplitToTitleAndContent( data.content ).title || data.title;
5
+ const menuTitle = data[ 'menu-title' ] || uExtractAndCacheTitle( data.source, data.content ).title || data.title;
6
6
 
7
7
  li
8
8
  a.b-reset-link.c-nav-tree__link(
9
9
  href=relative_url( page.path, data.path ),
10
10
  class=[activeClass],
11
- title=menuTitle,
12
- aria-current=ariaCurrent
11
+ aria-current=ariaCurrent,
12
+ title=menuTitle
13
13
  )
14
- span( class="c-nav-tree__guide-title" ) #{ menuTitle }
15
- - const shouldDisplayNewIndicator = 'shouldDisplayNewIndicator' in data ? data.shouldDisplayNewIndicator : false;
16
- - const badges = 'badges' in data ? data.badges : false;
17
- - if( shouldDisplayNewIndicator || badges )
14
+ span.c-nav-tree__guide-title
15
+ = menuTitle
16
+
17
+ -
18
+ const shouldDisplayNewIndicator = 'shouldDisplayNewIndicator' in data ? data.shouldDisplayNewIndicator : false;
19
+ const badges = 'badges' in data ? data.badges : false;
20
+
21
+ if shouldDisplayNewIndicator || badges
18
22
  span.c-nav-tree__badges
19
23
  - if( shouldDisplayNewIndicator )
20
24
  +badge({ variant: 'status' })
@@ -1,6 +1,6 @@
1
1
  include nav-tree-item
2
2
 
3
- mixin navTreeLevel( data, isRoot, level )
3
+ mixin navTreeLevel( { data, isRoot = true, level = 0 } )
4
4
  - if ( !data || !site ) return;
5
5
  - const pages = site.pages.filter( getPagesForNavTree( data.id, page.projectName ) );
6
6
  - const categories = data.categories ? markEmptyCategories( { pages: site.pages, categories: data.categories, projectName: page.projectName, group: page.groupId } ) : [];
@@ -18,7 +18,7 @@ mixin navTreeLevel( data, isRoot, level )
18
18
 
19
19
  each item in dataArr
20
20
  if item.name
21
- +navTreeLevel( item, false, level + 1 )
21
+ +navTreeLevel({ data: item, isRoot: false, level: level + 1 })
22
22
  else
23
23
  +navTreeItem( item )
24
24
 
@@ -53,6 +53,6 @@ mixin navTreeLevel( data, isRoot, level )
53
53
  each item in dataArr
54
54
  //- Categories have a property 'name', guides don't have property 'name'.
55
55
  if item.name
56
- +navTreeLevel( item, false, level + 1 )
56
+ +navTreeLevel({ data: item, isRoot: false, level: level + 1 })
57
57
  else
58
58
  +navTreeItem( item )
@@ -0,0 +1,26 @@
1
+ .c-snippet-footer {
2
+ display: flex;
3
+ justify-content: center;
4
+ align-items: center;
5
+ margin-bottom: var(--spacing-4);
6
+
7
+ .doc.live-snippet:has(> .ck-editor) + & {
8
+ margin-top: calc(var(--spacing-3) * -1);
9
+ }
10
+
11
+ &__container {
12
+ .c-icon-message__icon {
13
+ color: var(--color-warning-400);
14
+ }
15
+
16
+ .c-icon-message__content {
17
+ font-size: var(--font-size-sm);
18
+ color: var(--color-secondary-700);
19
+
20
+ .b-paragraph {
21
+ color: inherit;
22
+ font-size: inherit;
23
+ }
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,10 @@
1
+ //- Snippet footer component used to display a message at the bottom of snippet boxes.
2
+ mixin snippet-footer()
3
+ .c-snippet-footer
4
+ +icon-message({
5
+ className: 'c-snippet-footer__container',
6
+ icon: 'sparkles',
7
+ iconSize: 'lg',
8
+ iconPosition: 'left'
9
+ })
10
+ block
@@ -46,7 +46,7 @@ mixin spritesheet-local-svg({ file, className, width, height, title })
46
46
  )
47
47
  if title
48
48
  title(id=titleId)= title
49
- use(href=`#${iconId}`)
49
+ use(href=`#${iconId}`, data-skip-validation='')
50
50
 
51
51
  //- SVG spritesheet preload mixin
52
52
  //-
@@ -5,6 +5,7 @@ mixin algolia-search
5
5
  appId: docSearchConfig.appId,
6
6
  indexName: docSearchConfig.indexName,
7
7
  disableUserPersonalization: true,
8
+ insights: true,
8
9
  searchParameters: {
9
10
  hitsPerPage: 250,
10
11
  attributesToRetrieve: '*'
@@ -1,16 +1,16 @@
1
1
  mixin nav-project-select-dropdown()
2
2
  -
3
- const currentProject = projectLocals && projectsData.find(
3
+ const currentProject = projectLocals && quickNavigationProjects && quickNavigationProjects.find(
4
4
  project => project.slug === projectLocals.projectSlug
5
5
  );
6
6
 
7
7
  +menu-dropdown({
8
8
  label: currentProject ? currentProject.name : 'Choose project'
9
9
  })
10
- each project in projectsData
11
- if ![ 'ckfinder', 'trial', 'ckeditor4' ].includes( project.slug )
12
- +dropdown-item({
13
- label: project.name,
14
- active: !!currentProject && project.slug === currentProject.slug,
15
- href: relative_url(page.path, `${ project.latestBasePath }/index.html`)
16
- })
10
+ each project in quickNavigationProjects
11
+ +dropdown-item({
12
+ label: project.name,
13
+ active: !!currentProject && project.slug === currentProject.slug,
14
+ href: relative_url(page.path, `${ project.slug }/latest/index.html`)
15
+ })
16
+
@@ -4,6 +4,7 @@ mixin header-nightly-info()
4
4
  const stableUrl = `${ page.canonicalUrlBeginning }${ page.path }`
5
5
  .replace('nightly', '')
6
6
  .replace('ckeditor5.github.io/', 'ckeditor.com/')
7
+ .replace(projectLocals.projectVersion, 'latest')
7
8
  .replace(/\/+$/, '');
8
9
 
9
10
  +header-bar({ variant: 'info', ariaLabel: 'Nightly build documentation' })