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.
- package/CHANGELOG.md +26 -18
- package/package.json +1 -1
- package/scripts/filter/after-post-render/gloria/append-copy-heading-buttons.js +17 -26
- package/scripts/filter/after-post-render/parseicontag.js +1 -1
- package/scripts/filter/after-render/gloria/inline-svg.js +1 -1
- package/scripts/filter/after-render/gloria/spritesheet-svg.js +1 -1
- package/scripts/filter/before-post-render/gloria/add-breadcrumbs-data-to-page.js +9 -2
- package/scripts/filter/before-post-render/gloria/prerender-xml-pug-components.js +13 -0
- package/scripts/filter/before-post-render/gloria/render-post-render-pug-components.js +14 -6
- package/scripts/helper/u-extract-and-cache-title.js +36 -0
- package/scripts/utils/pug-renderer/create-prerender-pug-template.js +18 -5
- package/scripts/utils/random-id.js +8 -0
- package/src/api-builder/classes/description-parser.js +18 -17
- package/src/hexo/filter/project-locals.js +3 -0
- package/src/tasks/build-api-docs.js +1 -1
- package/src/tasks/build-documentation.js +13 -3
- package/src/tasks/validate-links.js +211 -39
- package/themes/umberto/layout/gloria/_api-docs/_mixin/_api-tree-item.pug +10 -2
- package/themes/umberto/layout/gloria/_api-docs/_toc/_style.scss +3 -1
- package/themes/umberto/layout/gloria/_components/code-block/_style.scss +2 -2
- package/themes/umberto/layout/gloria/_components/code-block/index.pug +10 -2
- package/themes/umberto/layout/gloria/_components/code-switcher/_style.scss +0 -5
- package/themes/umberto/layout/gloria/_components/code-switcher/index.pug +3 -3
- package/themes/umberto/layout/gloria/_components/icon-message/index.pug +5 -5
- package/themes/umberto/layout/gloria/_components/index.pug +1 -0
- package/themes/umberto/layout/gloria/_components/nav-tree/index.pug +1 -1
- package/themes/umberto/layout/gloria/_components/nav-tree/nav-tree-item.pug +11 -7
- package/themes/umberto/layout/gloria/_components/nav-tree/nav-tree-level.pug +3 -3
- package/themes/umberto/layout/gloria/_components/snippet-footer/_style.scss +26 -0
- package/themes/umberto/layout/gloria/_components/snippet-footer/index.pug +10 -0
- package/themes/umberto/layout/gloria/_components/svg/index.pug +1 -1
- package/themes/umberto/layout/gloria/_modules/algolia-search/index.pug +1 -0
- package/themes/umberto/layout/gloria/_modules/header/nav-project-select-dropdown.pug +8 -8
- package/themes/umberto/layout/gloria/_modules/header-nightly-info/index.pug +1 -0
- package/themes/umberto/layout/gloria/_modules/mobile-nav/index.pug +17 -17
- package/themes/umberto/layout/gloria/_modules/toc/_style.scss +1 -1
- package/themes/umberto/layout/gloria/api.pug +0 -1
- package/themes/umberto/source/gloria/assets/_fonts/Lato/Lato-Bold.ttf +0 -0
- package/themes/umberto/source/gloria/assets/_fonts/Lato/Lato-Bold.woff2 +0 -0
- package/themes/umberto/source/gloria/assets/_fonts/Lato/Lato-BoldItalic.ttf +0 -0
- package/themes/umberto/source/gloria/assets/_fonts/Lato/Lato-BoldItalic.woff2 +0 -0
- package/themes/umberto/source/gloria/assets/_fonts/Lato/Lato-Italic.ttf +0 -0
- package/themes/umberto/source/gloria/assets/_fonts/Lato/Lato-Italic.woff2 +0 -0
- package/themes/umberto/source/gloria/assets/_fonts/Lato/Lato-Regular.ttf +0 -0
- package/themes/umberto/source/gloria/assets/_fonts/Lato/Lato-Regular.woff2 +0 -0
- package/themes/umberto/source/gloria/assets/_img/icons/sparkles.svg +1 -0
- package/themes/umberto/src/gloria/css/_fonts.scss +39 -0
- package/themes/umberto/src/gloria/css/base/_inline-code.scss +4 -3
- package/themes/umberto/src/gloria/css/base/_links.scss +2 -1
- package/themes/umberto/src/gloria/css/base/_tables.scss +0 -4
- package/themes/umberto/src/gloria/css/components/_api-collapsing-list.scss +4 -0
- package/themes/umberto/src/gloria/css/components/_index.scss +2 -1
- package/themes/umberto/src/gloria/css/doc/_snippets.scss +1 -0
- package/themes/umberto/src/gloria/css/layout/_base.scss +2 -12
- package/themes/umberto/src/gloria/css/utilities/_spacing.scss +1 -0
- package/themes/umberto/src/gloria/js/components/code-block.js +7 -4
- 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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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 &&
|
|
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
|
-
|
|
61
|
+
if ( !isMaskedID( id ) && !ids.includes( 'icons-' ) ) {
|
|
62
|
+
links.add( upath.resolve( `${ filePath }#${ id }` ) );
|
|
63
|
+
}
|
|
53
64
|
}
|
|
54
65
|
}
|
|
55
66
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
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.
|
|
123
|
+
invalidHrefs.push( { href, text, filePath } );
|
|
106
124
|
}
|
|
107
125
|
}
|
|
108
126
|
|
|
109
|
-
if ( invalidHrefs.
|
|
110
|
-
|
|
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
|
-
*
|
|
125
|
-
*
|
|
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 {
|
|
128
|
-
* @
|
|
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
|
|
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(
|
|
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(
|
|
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:
|
|
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 {
|
|
@@ -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 =
|
|
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 );
|
|
@@ -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 })
|
|
@@ -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' ] ||
|
|
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
|
-
|
|
12
|
-
|
|
11
|
+
aria-current=ariaCurrent,
|
|
12
|
+
title=menuTitle
|
|
13
13
|
)
|
|
14
|
-
span
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
-
|
|
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
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
mixin nav-project-select-dropdown()
|
|
2
2
|
-
|
|
3
|
-
const currentProject = projectLocals &&
|
|
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
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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' })
|