8
0

build-content-styles.js 7.2 KB

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