UNPKG

12.3 kBJavaScriptView Raw
1const fs = require('fs');
2const util = require('./util');
3const glob = require('glob');
4
5const {write} = util;
6
7const themeMixins = {};
8const coreMixins = {};
9const themeVar = {};
10const coreVar = {};
11
12/* template for the new components/mixins.scss file*/
13const mixinTemplate = `//
14// Component: Mixin
15// Description: Defines mixins which are used across all components
16//
17// ========================================================================
18
19
20// SVG
21// ========================================================================
22
23/// Replace \`$search\` with \`$replace\` in \`$string\`
24/// @author Hugo Giraudel
25/// @param {String} $string - Initial string
26/// @param {String} $search - Substring to replace
27/// @param {String} $replace ('') - New value
28/// @return {String} - Updated string
29@function str-replace($string, $search, $replace: '') {
30 $index: str-index($string, $search);
31
32 @if $index {
33 @return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace);
34 }
35
36 @return $string;
37}
38
39@mixin svg-fill($src, $color-default, $color-new){
40
41 $replace-src: str-replace($src, $color-default, $color-new) !default;
42 $replace-src: str-replace($replace-src, "#", "%23");
43 background-image: url(quote($replace-src));
44}`;
45
46/* template for the inverse components */
47const inverseTemplate = ` @include hook-inverse-component-base();
48 @include hook-inverse-component-link();
49 @include hook-inverse-component-heading();
50 @include hook-inverse-component-divider();
51 @include hook-inverse-component-list();
52 @include hook-inverse-component-icon();
53 @include hook-inverse-component-form();
54 @include hook-inverse-component-button();
55 @include hook-inverse-component-grid();
56 @include hook-inverse-component-close();
57 @include hook-inverse-component-totop();
58 @include hook-inverse-component-badge();
59 @include hook-inverse-component-label();
60 @include hook-inverse-component-article();
61 @include hook-inverse-component-search();
62 @include hook-inverse-component-nav();
63 @include hook-inverse-component-navbar();
64 @include hook-inverse-component-subnav();
65 @include hook-inverse-component-breadcrumb();
66 @include hook-inverse-component-pagination();
67 @include hook-inverse-component-tab();
68 @include hook-inverse-component-slidenav();
69 @include hook-inverse-component-dotnav();
70 @include hook-inverse-component-accordion();
71 @include hook-inverse-component-iconnav();
72 @include hook-inverse-component-text();
73 @include hook-inverse-component-column();
74 @include hook-inverse-component-utility();`;
75
76/* First Step: Go through all files */
77glob.sync('src/less/**/*.less').forEach(file => {
78
79 const data = fs.readFileSync(file, 'utf8');
80 /* replace all LESS stuff with SCSS */
81 let scssData = data.replace(/\/less\//g, '/scss/') // change less/ dir to scss/ on imports
82 .replace(/\.less/g, '.scss') // change .less extensions to .scss on imports
83 .replace(/@/g, '$') // convert variables
84 .replace(/--uk-[^\s]+: (\$[^\s]+);/g, (exp, name) => exp.replace(name, `#{${name}}`))
85 .replace(/\\\$/g, '\\@') // revert classes using the @ symbol
86 .replace(/ e\(/g, ' unquote(') // convert escape function
87 .replace(/\.([\w-]*)\s*\((.*)\)\s*{/g, '@mixin $1($2){') // hook -> mixins
88 .replace(/(\$[\w-]*)\s*:(.*);/g, '$1: $2 !default;') // make variables optional
89 .replace(/@mixin ([\w-]*)\s*\((.*)\)\s*{\s*}/g, '// @mixin $1($2){}') // comment empty mixins
90 .replace(/\.(hook[a-zA-Z\-\d]+);/g, '@if(mixin-exists($1)) {@include $1();}') // hook calls surrounded by a mixin-exists
91 .replace(/\$(import|supports|media|font-face|page|-ms-viewport|keyframes|-webkit-keyframes|-moz-document)/g, '@$1') // replace valid '@' statements
92 .replace(/tint\((\$[\w-]+),\s([^)]*)\)/g, 'mix(white, $1, $2)') // replace LESS function tint with mix
93 .replace(/fade\((\$[\w-]*), ([0-9]+)%\)/g, (match, p1, p2) => { return `rgba(${p1}, ${p2 / 100})`;}) // replace LESS function fade with rgba
94 .replace(/fadeout\((\$[\w-]*), ([0-9]+)%\)/g, (match, p1, p2) => { return `fade-out(${p1}, ${p2 / 100})`;}) // replace LESS function fadeout with fade-out
95 .replace(/\.svg-fill/g, '@include svg-fill') // include svg-fill mixin
96 .replace(/(.*):extend\((\.[\w-]*) all\) when \((\$[\w-]*) = ([\w]+)\) {}/g, '@if ( $3 == $4 ) { $1 { @extend $2 !optional;} }') // update conditional extend and add !optional to ignore warnings
97 .replace(/(\.[\w-]+)\s*when\s*\((\$[\w-]*)\s*=\s*(\w+)\)\s*{\s*@if\(mixin-exists\(([\w-]*)\)\) {@include\s([\w-]*)\(\);\s*}\s*}/g, '@if ($2 == $3) { $1 { @if (mixin-exists($4)) {@include $4();}}}') // update conditional hook
98 .replace(/(\.[\w-]+)\s*when\s*\((\$[\w-]*)\s*=\s*(\w+)\)\s*({\s*.*?\s*})/gs, '@if ($2 == $3) {\n$1 $4\n}') // replace conditionals
99 .replace(/\${/g, '#{$') // string literals: from: /~"(.*)"/g, to: '#{"$1"}'
100 .replace(/[^(](-\$[\w-]*)/g, ' ($1)') // surround negative variables with brackets
101 .replace(/~('[^']+')/g, 'unquote($1)'); // string literals: for real
102
103 /* File name of the current file */
104 const [filename] = file.split('/').pop().split('.less');
105
106 if (filename !== 'inverse') {
107 scssData = scssData.replace(/hook-inverse(?!-)/g, `hook-inverse-component-${filename}`);
108 } else {
109 const joinedHook = `@mixin hook-inverse(){\n${inverseTemplate}\n}\n`;
110 scssData = scssData.replace(/\*\//, '*/\n' + joinedHook);
111 }
112
113 /* get all the mixins and remove them from the file */
114 scssData = getMixinsFromFile(file, scssData);
115
116 /* get all Variables but not from the mixin.less file */
117 if (filename !== 'mixin') {
118 scssData = getVariablesFromFile(file, scssData);
119 }
120
121 if (filename === 'uikit.theme') {
122 /* remove the theme import first place */
123 scssData = scssData.replace(/\/\/\n\/\/ Theme\n\/\/\n\n@import "theme\/_import.scss";/, '');
124 /* add uikit-mixins and uikit-variables include to the uikit.scss file and change order, to load theme files first */
125 scssData = scssData.replace(/\/\/ Core\n\/\//g, '// Theme\n//\n\n@import "theme/_import.scss";');
126 }
127
128 /* mixin.less needs to be fully replaced by the new mixin file*/
129 if (filename === 'mixin') {
130 scssData = mixinTemplate;
131 }
132
133 return write(file.replace(/less/g, 'scss').replace('.theme.', '-theme.'), scssData);
134
135});
136
137/* Second Step write all new needed files for SASS */
138
139/* write mixins into new file */
140const mixins_theme = Object.keys(themeMixins).map(function (key) { return themeMixins[key]; });
141write('src/scss/mixins-theme.scss', mixins_theme.join('\n'));
142
143const mixins_core = Object.keys(coreMixins).map(function (key) { return coreMixins[key]; });
144write('src/scss/mixins.scss', mixins_core.join('\n'));
145
146/* write core variables */
147const compactCoreVar = new Set();
148Object.keys(coreVar).map(key => getAllDependencies(coreVar, key).forEach(dependency => compactCoreVar.add(dependency)));
149
150write('src/scss/variables.scss', Array.from(compactCoreVar).join('\n'));
151
152/* write theme variables */
153const compactThemeVar = new Set();
154Object.keys(themeVar).map(key => getAllDependencies(themeVar, key).forEach(dependency => compactThemeVar.add(dependency)));
155
156write('src/scss/variables-theme.scss', Array.from(compactThemeVar).join('\n'));
157
158/*
159 * recursive function to get a dependencie Set which is ordered so that no depencies exist to a later on entry
160 * @return Set with all the dependencies.
161 */
162function getAllDependencies(allVariables, currentKey, dependencies = new Set()) {
163
164 if (!allVariables[currentKey].dependencies.length) {
165
166 dependencies.add(`${currentKey}: ${allVariables[currentKey].value}`);
167 return Array.from(dependencies);
168 } else {
169
170 allVariables[currentKey].dependencies.forEach(dependecy => {
171 getAllDependencies(allVariables, dependecy, dependencies).forEach(newDependency => dependencies.add(newDependency));
172 });
173
174 dependencies.add(`${currentKey}: ${allVariables[currentKey].value}`);
175 return Array.from(dependencies);
176 }
177}
178
179/*
180 * function to extract all the mixins from a given file with its data.
181 * @return an updated data where the mixins have been removed.
182 */
183function getMixinsFromFile(file, data) {
184
185 /* Step 1: get all includes and insert them, so that at least empty mixins exist. */
186 let regex = /@include ([a-z0-9-]+)/g;
187 let match = regex.exec(data);
188
189 while (match) {
190 if (!(match[1] in themeMixins)) { themeMixins[match[1]] = `@mixin ${match[1]}(){}`; }
191 if (!(match[1] in coreMixins)) { coreMixins[match[1]] = `@mixin ${match[1]}(){}`; }
192
193 match = regex.exec(data);
194 }
195
196 /* Step 2: get all multiline mixins */
197 regex = /@mixin ([\w-]*)\s*\((.*)\)\s*{\n(\s+[\w\W]+?)(?=\n})\n}/g;
198 match = regex.exec(data);
199
200 while (match) {
201 themeMixins[match[1]] = match[0];
202 if (file.indexOf('theme/') < 0) {
203 coreMixins[match[1]] = match[0];
204 }
205 match = regex.exec(data);
206 }
207
208 /* Step 3: get all singleline mixins */
209 regex = /@mixin ([\w-]*)\s*\((.*)\)\s*{( [^\n]+)}/g;
210 match = regex.exec(data);
211
212 while (match) {
213 themeMixins[match[1]] = match[0];
214 if (file.indexOf('theme/') < 0) {
215 coreMixins[match[1]] = match[0];
216 }
217
218 match = regex.exec(data);
219 }
220
221 /* Step 4: remove the mixins from the file, so that users can overwrite them in their custom code. */
222 return data
223 .replace(/@mixin ([\w-]*)\s*\((.*)\)\s*{\n(\s+[\w\W]+?)(?=\n})\n}/g, '')
224 .replace(/@mixin ([\w-]*)\s*\((.*)\)\s*{( [^\n]+)}/g, '');
225}
226
227/*
228 * function to extract all the variables from a given file with its data.
229 * @return an updated data where the icons have been replaced by the actual SVG data.
230 */
231function getVariablesFromFile(file, data) {
232 const regex = /(\$[\w-]*)\s*:\s*(.*);/g;
233 let match = regex.exec(data);
234
235 while (match) {
236
237 /* check if variable is an background icon, if so replace it directly by the SVG */
238 if (match[0].indexOf('../../images/backgrounds') >= 0) {
239
240 const iconregex = /(\$[\w-]+)\s*:\s*"\.\.\/\.\.\/images\/backgrounds\/([\w./-]+)" !default;/g;
241 const iconmatch = iconregex.exec(match[0]);
242 let svg = fs.readFileSync(`src/images/backgrounds/${iconmatch[2]}`).toString();
243 svg = `"${svg.replace(/\r?\n|\r/g, '%0A')
244 .replace(/"/g, '\'')
245 .replace(/\s/g, '%20')
246 .replace(/</g, '%3C')
247 .replace(/=/g, '%3D')
248 .replace(/'/g, '%22')
249 .replace(/:/g, '%3A')
250 .replace(/\//g, '%2F')
251 .replace(/>/g, '%3E')
252 .replace(/%3Csvg/, 'data:image/svg+xml;charset=UTF-8,%3Csvg')}"`;
253
254 /* add SVG to the coreVar and themeVar only if it is a theme file and make it optional */
255 if (file.indexOf('theme/') < 0) {
256 coreVar[iconmatch[1]] = {value: `${svg} !default;`, dependencies: []};
257 }
258
259 themeVar[iconmatch[1]] = {value: `${svg} !default;`, dependencies: []};
260
261 /* add SVG to the variable within the file itself as well */
262 const inlineSVG = `${iconmatch[1]}: ${svg} !default;`;
263 data = data.replace(match[0], inlineSVG);
264
265 /* when it is not an SVG add the variable and search for its dependencies */
266 } else {
267
268 const variablesRegex = /(\$[\w-]+)/g;
269 let variablesMatch = variablesRegex.exec(match[2]);
270 const dependencies = [];
271
272 while (variablesMatch) {
273 dependencies.push(variablesMatch[1]);
274 variablesMatch = variablesRegex.exec(match[2]);
275 }
276
277 /* add variables only to the core Variables if it is not a theme file */
278 if (file.indexOf('theme/') < 0) {
279 coreVar[match[1]] = {value: `${match[2]};`, dependencies: Array.from(dependencies)};
280 }
281
282 themeVar[match[1]] = {value: `${match[2]};`, dependencies: Array.from(dependencies)};
283 }
284
285 match = regex.exec(data);
286 }
287
288 return data;
289}