umberto 9.0.0 → 9.1.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +21 -16
  2. package/package.json +8 -13
  3. package/scripts/filter/after-post-render/fix-code-samples.js +82 -18
  4. package/scripts/filter/after-post-render/gloria.js +27 -0
  5. package/scripts/filter/after-post-render/insert-error-codes.js +34 -26
  6. package/scripts/filter/after-post-render/validate-after-render.js +27 -6
  7. package/scripts/filter/after-render/process-svg.js +21 -0
  8. package/scripts/filter/before-post-render/gloria/render-post-render-pug-components.js +46 -18
  9. package/scripts/helper/u-extract-and-cache-title.js +27 -8
  10. package/scripts/helper/u-split-to-title-and-content.js +32 -8
  11. package/scripts/utils/gloria-after-post-render/append-copy-heading-buttons.js +119 -0
  12. package/scripts/utils/gloria-after-post-render/apply-design-doc-classes.js +157 -0
  13. package/scripts/utils/gloria-after-post-render/wrap-table-into-wrappers.js +25 -0
  14. package/scripts/utils/inline-svg.js +63 -94
  15. package/scripts/utils/spritesheet-svg.js +82 -102
  16. package/scripts/utils/toc.js +85 -31
  17. package/src/api-builder/api-builder.js +53 -40
  18. package/src/api-builder/build-page-worker.js +35 -0
  19. package/src/api-builder/classes/description-parser.js +77 -38
  20. package/src/data-converter/converters/jsduck2umberto.js +43 -15
  21. package/src/hexo/filter/project-locals.js +3 -0
  22. package/src/sdk-builder/get-sdk-sources.js +81 -44
  23. package/src/tasks/build-documentation.js +4 -0
  24. package/src/tasks/minify-html.js +1 -1
  25. package/src/tasks/validate-links-collect-worker.js +34 -0
  26. package/src/tasks/validate-links-worker.js +127 -0
  27. package/src/tasks/validate-links.js +61 -259
  28. package/themes/umberto/layout/gloria/_head/head.pug +3 -0
  29. package/themes/umberto/layout/gloria/_modules/index.pug +1 -0
  30. package/themes/umberto/layout/gloria/_modules/kapa/index.pug +0 -1
  31. package/themes/umberto/layout/gloria/_modules/sentry/index.pug +28 -0
  32. package/scripts/filter/after-post-render/gloria/append-copy-heading-buttons.js +0 -90
  33. package/scripts/filter/after-post-render/gloria/apply-design-doc-classes.js +0 -96
  34. package/scripts/filter/after-post-render/gloria/wrap-table-into-wrappers.js +0 -36
  35. package/scripts/filter/after-render/gloria/inline-svg.js +0 -14
  36. package/scripts/filter/after-render/gloria/spritesheet-svg.js +0 -14
  37. package/scripts/utils/apply-design-doc-classes.js +0 -82
  38. /package/src/tasks/{minify-worker.js → minify-html-worker.js} +0 -0
@@ -0,0 +1,119 @@
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 { parseDocument } = require( 'htmlparser2' );
9
+ const { Element } = require( 'domhandler' );
10
+ const { selectAll, selectOne } = require( 'css-select' );
11
+ const {
12
+ getAttributeValue,
13
+ appendChild,
14
+ removeElement,
15
+ replaceElement,
16
+ textContent
17
+ } = require( 'domutils' );
18
+
19
+ const createPrerenderPugTemplate = require( '../pug-renderer/create-prerender-pug-template' );
20
+
21
+ /**
22
+ * Renders the button using Pug template.
23
+ */
24
+ const renderButton = createPrerenderPugTemplate( {
25
+ requires: [
26
+ '_components/svg/index',
27
+ '_components/icon/index',
28
+ '_components/tooltip-popover/index',
29
+ '_components/heading-link/index'
30
+ ],
31
+ component: 'heading-link',
32
+ componentAttrs: {
33
+ mask: [ 'headingId' ]
34
+ }
35
+ } );
36
+
37
+ module.exports = function appendCopyHeadingButtons( doc ) {
38
+ // Do not process h1 tags as it's processed further by Umberto and may be removed.
39
+ for ( const heading of selectAll( 'h2, h3, h4, h5, h6', doc ) ) {
40
+ if ( hasClassOrParentWithClass( heading, 'no-transform' ) ) {
41
+ continue;
42
+ }
43
+
44
+ removeAllEmptyAnchors( heading );
45
+
46
+ const headingId = getAttributeValue( heading, 'id' );
47
+ const container = new Element( 'div', { class: 'doc b-heading' } );
48
+
49
+ // Create the copy button from the Pug template and parse it into nodes.
50
+ const buttonHtml = renderButton( { headingId } );
51
+ const buttonNodes = parseFragmentElements( buttonHtml );
52
+
53
+ // Put the container where the heading was, then move heading and append the button.
54
+ replaceElement( heading, container );
55
+ appendChild( container, heading );
56
+ for ( const n of buttonNodes ) {
57
+ appendChild( container, n );
58
+ }
59
+ }
60
+
61
+ return doc;
62
+ };
63
+
64
+ /**
65
+ * Removes all empty anchors that were created to hold the heading links buttons.
66
+ * "Empty" = no element children AND text content (after trimming and removing a single '#') is empty.
67
+ */
68
+ function removeAllEmptyAnchors( heading ) {
69
+ for ( const a of selectAll( 'a', heading ) ) {
70
+ const hasElementChildren = ( a.children || [] ).some( c => c && c.type === 'tag' );
71
+ const text = ( textContent ? textContent( a ) : getTextFallback( a ) ).trim().replace( '#', '' );
72
+
73
+ if ( !hasElementChildren && !text ) {
74
+ removeElement( a );
75
+ }
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Checks if the element itself or any of its parents has the specified class.
81
+ */
82
+ function hasClassOrParentWithClass( el, className ) {
83
+ for ( let n = el; n; n = n.parent ) {
84
+ const cls = getAttributeValue( n, 'class' );
85
+ if ( cls && cls.split( /\s+/ ).includes( className ) ) {
86
+ return true;
87
+ }
88
+ }
89
+ return false;
90
+ }
91
+
92
+ /**
93
+ * Parses an HTML fragment into an array of element nodes (skips stray text nodes/whitespace).
94
+ */
95
+ function parseFragmentElements( html ) {
96
+ const frag = parseDocument( html );
97
+ const body = selectOne( 'body', frag );
98
+ const nodes = body ? body.children : frag.children;
99
+
100
+ return ( nodes || [] ).filter( n => n && n.type === 'tag' );
101
+ }
102
+
103
+ /**
104
+ * Small fallback in case domutils.textContent isn't available.
105
+ */
106
+ function getTextFallback( node ) {
107
+ let out = '';
108
+ if ( !node || !node.children ) {
109
+ return out;
110
+ }
111
+ for ( const ch of node.children ) {
112
+ if ( ch.type === 'text' && typeof ch.data === 'string' ) {
113
+ out += ch.data;
114
+ } else if ( ch.children && ch.children.length ) {
115
+ out += getTextFallback( ch );
116
+ }
117
+ }
118
+ return out;
119
+ }
@@ -0,0 +1,157 @@
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 { selectAll } = require( 'css-select' );
9
+ const { getAttributeValue } = require( 'domutils' );
10
+
11
+ /**
12
+ * Global mapping of HTML elements to their corresponding design system classes.
13
+ */
14
+ const ELEMENTS_CLASSES_MAPPINGS = {
15
+ 'h1': [ 'b-h1' ],
16
+ 'h2': [ 'b-h2' ],
17
+ 'h3': [ 'b-h3' ],
18
+ 'h4': [ 'b-h4' ],
19
+ 'h5': [ 'b-h5' ],
20
+ 'h6': [ 'b-h6' ],
21
+ 'a': [ 'b-link' ],
22
+
23
+ // Text formatting elements
24
+ 'p': [ 'b-paragraph' ],
25
+ 'strong': [ 'b-bold' ],
26
+ 'b': [ 'b-bold' ],
27
+ 'em': [ 'b-italic' ],
28
+ 'i': [ 'b-italic' ],
29
+ 'code': [ 'b-inline-code' ],
30
+ 'pre': [ 'b-code-block' ],
31
+ 'blockquote': [ 'b-quote' ],
32
+ 'hr': [ 'b-separator' ],
33
+ 'span': [ 'b-text' ],
34
+ 'small': [ 'b-small-text' ],
35
+ 'sub': [ 'b-subscript' ],
36
+ 'sup': [ 'b-superscript' ],
37
+
38
+ // List elements
39
+ 'ul': [ 'b-list', 'b-list--unordered' ],
40
+ 'ol': [ 'b-list', 'b-list--ordered' ],
41
+ 'li': [ 'b-list__item' ],
42
+ 'dl': [ 'b-description' ],
43
+ 'dt': [ 'b-description__term' ],
44
+ 'dd': [ 'b-description__details' ],
45
+
46
+ // Details
47
+ 'details': [ 'b-details' ],
48
+ 'summary': [ 'b-reset-button', 'b-details__summary' ],
49
+
50
+ // Table elements
51
+ 'table': node => {
52
+ const variant = getAttributeValue( node, 'data-variant' ) || 'striped';
53
+
54
+ return [ 'b-table', `b-table--${ variant }` ];
55
+ },
56
+ 'thead': [ 'b-table__header' ],
57
+ 'tbody': [ 'b-table__body' ],
58
+ 'tr': [ 'b-table__row' ],
59
+ 'td': [ 'b-table__cell' ],
60
+ 'th': [ 'b-table__cell', 'b-table__cell-header' ],
61
+
62
+ // Image and media elements
63
+ 'img': [ 'b-image' ],
64
+ 'figure': [ 'b-figure' ],
65
+ 'figcaption': [ 'b-figure-caption' ],
66
+ 'iframe': [ 'b-iframe' ],
67
+
68
+ // Accessibility elements
69
+ 'kbd': [ 'c-keyboard-shortcut', 'c-keyboard-shortcut--raised' ],
70
+ 'mark': [ 'b-highlight' ]
71
+ };
72
+
73
+ /**
74
+ * Classes that are allowed to be added to elements with existing classes.
75
+ */
76
+ const ELEMENTS_WITH_WHITELIST_CLASSES = [
77
+ 'headerlink'
78
+ ];
79
+
80
+ /**
81
+ * Add design system classes to the parsed document elements. If element has 0 CSS classes
82
+ * it'll be considered as not styled and the design system classes will be applied.
83
+ *
84
+ * @param doc - The parsed document to apply design classes to.
85
+ */
86
+ module.exports = function applyDesignDocClasses( doc ) {
87
+ // Apply classes based on the global mapping.
88
+ for ( const [ selector, classesOrCallback ] of Object.entries( ELEMENTS_CLASSES_MAPPINGS ) ) {
89
+ for ( const element of selectAll( selector, doc ) ) {
90
+ if ( hasClassOrParentWithClass( element, 'no-transform' ) ) {
91
+ continue;
92
+ }
93
+
94
+ // Add the 'doc' class to each element that gets styled.
95
+ addClass( element, 'doc' );
96
+
97
+ const classList = getClasses( element ).filter( className => !ELEMENTS_WITH_WHITELIST_CLASSES.includes( className ) );
98
+
99
+ // Avoid applying classes to elements that already have one non-whitelisted class
100
+ // (besides 'doc' which we just added).
101
+ if ( classList.length > 1 ) {
102
+ continue;
103
+ }
104
+
105
+ const classesToAdd = typeof classesOrCallback === 'function' ?
106
+ classesOrCallback( element ) :
107
+ classesOrCallback;
108
+
109
+ addClass( element, classesToAdd.join( ' ' ) );
110
+ }
111
+ }
112
+
113
+ return doc;
114
+ };
115
+
116
+ /**
117
+ * Checks if the element itself or any of its parents has the specified class.
118
+ *
119
+ * @param element - The DOM element to check.
120
+ * @param className - The class name to look for.
121
+ * @returns True if the element or any parent has the class, false otherwise.
122
+ */
123
+ function hasClassOrParentWithClass( element, className ) {
124
+ for ( let node = element; node; node = node.parent ) {
125
+ if ( node.attribs?.class?.includes( className ) ) {
126
+ return true;
127
+ }
128
+ }
129
+
130
+ return false;
131
+ }
132
+
133
+ /**
134
+ * Adds class to a DOM element.
135
+ *
136
+ * @param element - The DOM element.
137
+ * @param className - Class name to add.
138
+ */
139
+ function addClass( element, className ) {
140
+ const existing = getClasses( element );
141
+
142
+ if ( !existing.includes( className ) ) {
143
+ existing.push( className );
144
+ element.attribs.class = existing.join( ' ' );
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Gets classes of a DOM element.
150
+ *
151
+ * @param element - The DOM element.
152
+ * @returns Array of class names.
153
+ */
154
+ function getClasses( element ) {
155
+ const existing = getAttributeValue( element, 'class' ) || '';
156
+ return existing.split( /\s+/ ).filter( Boolean );
157
+ }
@@ -0,0 +1,25 @@
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 { Element } = require( 'domhandler' );
9
+ const { selectAll } = require( 'css-select' );
10
+ const { replaceElement, appendChild, isTag, getName } = require( 'domutils' );
11
+
12
+ module.exports = function wrapTableIntoWrappers( doc ) {
13
+ for ( const table of selectAll( 'table', doc ) ) {
14
+ const parent = table.parent;
15
+ const parentIsFigure = isTag( parent ) && getName( parent ).toUpperCase() === 'FIGURE';
16
+
17
+ if ( !parentIsFigure ) {
18
+ const figure = new Element( 'figure', { class: 'doc b-table-wrapper' } );
19
+ replaceElement( table, figure );
20
+ appendChild( figure, table );
21
+ }
22
+ }
23
+
24
+ return doc;
25
+ };
@@ -5,148 +5,117 @@
5
5
 
6
6
  'use strict';
7
7
 
8
- const cheerio = require( 'cheerio' );
9
8
  const fs = require( 'fs' );
10
9
  const path = require( 'path' );
11
-
12
- const HexoManager = require( '../../src/hexo-manager' );
13
-
14
- /**
15
- * Cache for parsed SVG elements.
16
- */
17
- const SVG_CACHE = new Map();
18
-
19
- module.exports = function inlineSvg( str, isDocument = true ) {
20
- const $ = cheerio.load( str, null, isDocument );
21
-
22
- // Find all elements with data-inline-svg-image attribute.
23
- $( '[data-inline-svg-image]' ).each( function() {
24
- const element = $( this );
25
- const svgPath = element.attr( 'data-inline-svg-image' );
26
- const fillColor = element.attr( 'data-inline-svg-fill-color' );
27
- const trimDimensions = element.attr( 'data-inline-svg-trim-dimensions' ) !== undefined;
28
- const title = element.attr( 'data-inline-svg-title' );
10
+ const { parseDocument } = require( 'htmlparser2' );
11
+ const { Element, Text } = require( 'domhandler' );
12
+ const { selectAll, selectOne } = require( 'css-select' );
13
+ const { getAttributeValue, hasAttrib, replaceElement, appendChild, prependChild } = require( 'domutils' );
14
+
15
+ module.exports = function inlineSvg( doc, themeDir ) {
16
+ for ( const el of selectAll( '[data-inline-svg-image]', doc ) ) {
17
+ const svgPath = getAttributeValue( el, 'data-inline-svg-image' );
18
+ const fillColor = getAttributeValue( el, 'data-inline-svg-fill-color' );
19
+ const trimDimensions = hasAttrib( el, 'data-inline-svg-trim-dimensions' );
20
+ const title = getAttributeValue( el, 'data-inline-svg-title' );
29
21
 
30
22
  // Construct the absolute path to the SVG file.
31
23
  // Assuming the path is relative to the Hexo root.
32
- const absolutePath = path.join( HexoManager.hexo.theme_dir, svgPath );
24
+ const absolutePath = path.join( themeDir, svgPath );
33
25
 
34
26
  // Read the SVG file content
35
27
  if ( !fs.existsSync( absolutePath ) ) {
36
28
  console.warn( `SVG file not found: ${ absolutePath }` );
37
- return;
29
+ continue;
38
30
  }
39
31
 
40
- // Generate a cache key based on file path, fill color, trim dimensions and title
41
- const cacheKey = JSON.stringify( {
42
- path: absolutePath,
43
- fillColor: fillColor || 'default',
44
- trimDimensions,
45
- title: title || null
46
- } );
32
+ // Parse the SVG.
33
+ const svgContent = fs.readFileSync( absolutePath, 'utf8' );
34
+ const svgDoc = parseDocument( svgContent, { xmlMode: true } );
35
+ const svgRoot = selectOne( 'svg', svgDoc );
47
36
 
48
- let $svg;
49
-
50
- // Try to get the SVG from cache first.
51
- if ( SVG_CACHE.has( cacheKey ) ) {
52
- $svg = SVG_CACHE.get( cacheKey );
53
- } else {
54
- // Parse the SVG.
55
- const svgContent = fs.readFileSync( absolutePath, 'utf8' );
56
-
57
- $svg = cheerio.load( svgContent, { xmlMode: true } )( 'svg' );
58
-
59
- // Apply fill color if specified.
60
- if ( fillColor ) {
61
- applyFillColor( $, $svg, fillColor );
62
- }
63
-
64
- // Remove width and height if trimDimensions is true
65
- if ( trimDimensions ) {
66
- trimSvgDimensions( $, $svg );
67
- }
37
+ // Apply fill color if specified.
38
+ if ( fillColor ) {
39
+ applyFillColor( svgRoot, fillColor );
40
+ }
68
41
 
69
- // Add title if specified
70
- if ( title ) {
71
- addTitle( $, $svg, title );
72
- }
42
+ // Remove width and height if trimDimensions is true
43
+ if ( trimDimensions ) {
44
+ trimSvgDimensions( svgRoot );
45
+ }
73
46
 
74
- // Store in cache.
75
- SVG_CACHE.set( cacheKey, $svg );
47
+ // Add title if specified
48
+ if ( title ) {
49
+ addTitle( svgRoot, title );
76
50
  }
77
51
 
78
52
  // Copy original element's attributes to SVG.
79
- const elementAttributes = element.attr();
80
- let svgCloned = false;
81
-
82
- for ( const attr in elementAttributes ) {
83
- if ( attr.startsWith( 'data-inline-svg-' ) ) {
53
+ for ( const [ name, value ] of Object.entries( el.attribs ) ) {
54
+ if ( name.startsWith( 'data-inline-svg-' ) ) {
84
55
  continue;
85
56
  }
86
57
 
87
- if ( !svgCloned ) {
88
- $svg = $svg.clone();
89
- svgCloned = true;
90
- }
91
-
92
- $svg.attr( attr, elementAttributes[ attr ] );
58
+ svgRoot.attribs[ name ] = value;
93
59
  }
94
60
 
95
- // Replace the original element with the SVG.
96
- element.replaceWith( $svg.toString() );
97
- } );
61
+ // Replace the original element with the prepared <svg>.
62
+ replaceElement( el, svgRoot );
63
+ };
98
64
 
99
- $( '[data-inline-svg-image]' ).removeAttr( 'data-spritesheet-svg' );
65
+ for ( const el of selectAll( '[data-inline-svg-image]', doc ) ) {
66
+ delete el.attribs[ 'data-spritesheet-svg' ];
67
+ }
100
68
 
101
- return $.html();
69
+ return doc;
102
70
  };
103
71
 
104
72
  /**
105
- * Applies fill color to an SVG element.
73
+ * Applies fill color to an SVG element (root and any descendant with [fill]).
74
+ * Uses css-select for querying and direct attrib updates for speed.
106
75
  */
107
- function applyFillColor( $, $svg, fillColor ) {
76
+ function applyFillColor( svg, fillColor ) {
108
77
  // Replace all fill attributes with the specified color.
109
- $svg.find( '[fill]' ).each( function() {
110
- $( this ).attr( 'fill', fillColor );
111
- } );
78
+ for ( const node of selectAll( '[fill]', svg ) ) {
79
+ node.attribs.fill = fillColor;
80
+ }
112
81
 
113
82
  // Also check the root SVG for fill attribute.
114
- if ( $svg.attr( 'fill' ) ) {
115
- $svg.attr( 'fill', fillColor );
83
+ if ( hasAttrib( svg, 'fill' ) ) {
84
+ svg.attribs.fill = fillColor;
116
85
  }
117
-
118
- return $svg;
119
86
  }
120
87
 
121
88
  /**
122
89
  * Removes width and height attributes from SVG elements.
123
90
  */
124
- function trimSvgDimensions( $, $svg ) {
125
- $svg.removeAttr( 'width' );
126
- $svg.removeAttr( 'height' );
127
- return $svg;
91
+ function trimSvgDimensions( svg ) {
92
+ delete svg.attribs.width;
93
+ delete svg.attribs.height;
128
94
  }
129
95
 
130
96
  /**
131
- * Adds or updates a title element in the SVG.
97
+ * Adds or updates a <title> element in the SVG.
98
+ * - If any <title> exists anywhere under the root, update all of them (mirrors Cheerio .find('title').text()).
99
+ * - Otherwise, prepend a new <title> as the first child of the root <svg>.
132
100
  */
133
- function addTitle( $, $svg, title ) {
101
+ function addTitle( svg, title ) {
134
102
  if ( [ '', 'none', 'false', 'null' ].includes( title ) ) {
135
- return $svg;
103
+ return;
136
104
  }
137
105
 
138
106
  // Check if SVG already has a title
139
- const existingTitle = $svg.find( 'title' );
107
+ const titles = selectAll( 'title', svg );
140
108
 
141
- if ( existingTitle.length ) {
109
+ if ( titles.length ) {
110
+ for ( const t of titles ) {
142
111
  // Update existing title
143
- existingTitle.text( title );
112
+ t.children = [];
113
+ appendChild( t, new Text( title ) );
114
+ }
144
115
  } else {
145
116
  // Create new title and add it as the first child
146
- const $title = $( '<title></title>' ).text( title );
147
- $svg.prepend( $title );
117
+ const t = new Element( 'title', {} );
118
+ appendChild( t, new Text( title ) );
119
+ prependChild( svg, t );
148
120
  }
149
-
150
- return $svg;
151
121
  }
152
-