8
0

build-content-styles.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. /**
  2. * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /* eslint-env node */
  6. const path = require( 'path' );
  7. const fs = require( 'fs' );
  8. const webpack = require( 'webpack' );
  9. const { styles } = require( '@ckeditor/ckeditor5-dev-utils' );
  10. const DESTINATION_DIRECTORY = path.join( __dirname, '..', '..', 'build', 'content-styles' );
  11. const VARIABLE_DEFINITION_REGEXP = /(--[\w-]+):\s+(.*);/g;
  12. const VARIABLE_USAGE_REGEXP = /var\((--[\w-]+)\)/g;
  13. const contentRules = {
  14. selector: [],
  15. variables: []
  16. };
  17. const webpackConfig = getWebpackConfig();
  18. const parentCwd = path.join( process.cwd(), '..' );
  19. runWebpack( webpackConfig )
  20. .then( () => {
  21. // All variables are placed inside `:root` selector. Let's extract their names and values as a map.
  22. const cssVariables = new Map( contentRules.variables
  23. .map( rule => {
  24. // Let's extract all of them as an array of pairs: [ name, value ]
  25. const allRules = [];
  26. let match;
  27. while ( ( match = VARIABLE_DEFINITION_REGEXP.exec( rule.css ) ) ) {
  28. allRules.push( [ match[ 1 ], match[ 2 ] ] );
  29. }
  30. return allRules;
  31. } )
  32. .reduce( ( previousValue, currentValue ) => {
  33. // And simplify nested arrays as a flattened array.
  34. previousValue.push( ...currentValue );
  35. return previousValue;
  36. }, [] ) );
  37. // CSS variables that are used by the `.ck-content` selector.
  38. const usedVariables = new Set();
  39. const selectorCss = contentRules.selector
  40. .map( rule => {
  41. // Removes all comments from the rule definition.
  42. const cssAsArray = rule.css.replace( /\/\*[^*]+\*\//g, '' ).split( '\n' );
  43. // We want to fix invalid indentations. We need to find a number of how many indentations we want to remove.
  44. // Because the last line ends the block, we can use this value.
  45. const lastLineIndent = cssAsArray[ cssAsArray.length - 1 ].length - 1;
  46. const css = cssAsArray
  47. .filter( line => line.trim().length > 0 )
  48. .map( ( line, index ) => {
  49. // Do not touch the first line. It is always correct.
  50. if ( index === 0 ) {
  51. return line;
  52. }
  53. return line.slice( lastLineIndent );
  54. } )
  55. .join( '\n' );
  56. return `/* ${ rule.file.replace( parentCwd + path.sep, '' ) } */\n${ css }`;
  57. } )
  58. .filter( rule => {
  59. // 1st: path to the css file, 2nd: selector definition - start block, 3rd: end block
  60. // If the rule contains only 3 lines, it means that it does not define any rules.
  61. return rule.split( '\n' ).length > 3;
  62. } )
  63. .join( '\n' );
  64. // Find all CSS variables inside `.ck-content` selector.
  65. let match;
  66. while ( ( match = VARIABLE_USAGE_REGEXP.exec( selectorCss ) ) ) {
  67. usedVariables.add( match[ 1 ] );
  68. }
  69. // We need to also look at whether any of the used variables requires the value of other variables.
  70. let clearRun = false;
  71. // We need to process all variables as long as the entire collection won't be changed.
  72. while ( !clearRun ) {
  73. clearRun = true;
  74. // For the every used variable...
  75. for ( const variable of usedVariables ) {
  76. const value = cssVariables.get( variable );
  77. let match;
  78. // ...find its value and check whether it requires another variable.
  79. while ( ( match = VARIABLE_USAGE_REGEXP.exec( value ) ) ) {
  80. // If so, mark the entire `while()` block as it should be checked once again.
  81. // Also, add the new variable to used variables collection.
  82. if ( !usedVariables.has( match[ 1 ] ) ) {
  83. clearRun = false;
  84. usedVariables.add( match[ 1 ] );
  85. }
  86. }
  87. }
  88. }
  89. // Build the final content of the CSS file.
  90. let data = ':root {\n';
  91. for ( const variable of [ ...usedVariables ].sort() ) {
  92. data += `\t${ variable }: ${ cssVariables.get( variable ) };\n`;
  93. }
  94. data += '}\n\n';
  95. data += selectorCss;
  96. return writeFile( path.join( DESTINATION_DIRECTORY, 'content-styles.css' ), data );
  97. } )
  98. .then( () => {
  99. console.log( `Content styles has saved under the path: ${ path.join( DESTINATION_DIRECTORY, 'content-styles.css' ) }` );
  100. } )
  101. .catch( err => {
  102. console.log( err );
  103. } );
  104. /**
  105. * Prepares configuration for Webpack.
  106. *
  107. * @returns {Object}
  108. */
  109. function getWebpackConfig() {
  110. const postCssConfig = styles.getPostCssConfig( {
  111. themeImporter: {
  112. themePath: require.resolve( '@ckeditor/ckeditor5-theme-lark' )
  113. },
  114. minify: false
  115. } );
  116. const contentStylesPlugin = require( './content-styles/list-content-styles' )( { contentRules } );
  117. postCssConfig.plugins.push( contentStylesPlugin );
  118. return {
  119. mode: 'development',
  120. devtool: 'source-map',
  121. entry: {
  122. ckeditor5: path.join( __dirname, 'content-styles', 'ckeditor.js' )
  123. },
  124. output: {
  125. path: DESTINATION_DIRECTORY,
  126. filename: '[name].js'
  127. },
  128. // Configure the paths so building CKEditor 5 snippets work even if the script
  129. // is triggered from a directory outside ckeditor5 (e.g. multi-project case).
  130. resolve: {
  131. modules: getModuleResolvePaths()
  132. },
  133. resolveLoader: {
  134. modules: getModuleResolvePaths()
  135. },
  136. module: {
  137. rules: [
  138. {
  139. test: /\.svg$/,
  140. use: [ 'raw-loader' ]
  141. },
  142. {
  143. test: /\.css$/,
  144. use: [
  145. 'style-loader',
  146. {
  147. loader: 'postcss-loader',
  148. options: postCssConfig
  149. }
  150. ]
  151. }
  152. ]
  153. }
  154. };
  155. }
  156. /**
  157. * @param {Object} webpackConfig
  158. * @returns {Promise}
  159. */
  160. function runWebpack( webpackConfig ) {
  161. return new Promise( ( resolve, reject ) => {
  162. webpack( webpackConfig, ( err, stats ) => {
  163. if ( err ) {
  164. reject( err );
  165. } else if ( stats.hasErrors() ) {
  166. reject( new Error( stats.toString() ) );
  167. } else {
  168. resolve();
  169. }
  170. } );
  171. } );
  172. }
  173. /**
  174. * @returns {Array.<String>}
  175. */
  176. function getModuleResolvePaths() {
  177. return [
  178. path.resolve( __dirname, '..', '..', 'node_modules' ),
  179. 'node_modules'
  180. ];
  181. }
  182. function writeFile( file, data ) {
  183. return new Promise( ( resolve, reject ) => {
  184. fs.writeFile( file, data, err => {
  185. if ( err ) {
  186. return reject( err );
  187. }
  188. return resolve();
  189. } );
  190. } );
  191. }