Skip to content

Commit ec6c2b1

Browse files
committed
Merge branch 'build'
2 parents f1143cd + 6d03983 commit ec6c2b1

File tree

7 files changed

+5227
-4233
lines changed

7 files changed

+5227
-4233
lines changed

dev/sass/regexr.scss

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
@import "colors"; // colors_dark
1+
// "colors" are imported by the build process to support themes.
2+
23
@import "variables";
34
@import "fonts";
45

dev/src/docs/sidebar_content.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ home.kids = [
8181

8282
{
8383
label:"About",
84-
desc:"RegExr v[build-version] ([build-date])."+
84+
desc:"RegExr v<%= build_version %> (<%= build_date %>)."+
8585
"<p>Created by <a href='http://twitter.com/gskinner/' target='_blank'>Grant Skinner</a> and the <a href='http://gskinner.com/' target='_blank'>gskinner</a> team, using the <a href='http://createjs.com/' target='_blank'>CreateJS</a> & <a href='http://codemirror.net/' target='_blank'>CodeMirror</a> libraries.</p>"+
8686
"<p>You can provide feedback or log bugs on <a href='http://github.com/gskinner/regexr/' target='_blank'>GitHub</a>.</p>"
8787
},

gulpfile.babel.js

Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
// imports
2+
const gulp = require("gulp");
3+
const inject = require("gulp-inject");
4+
const rename = require("gulp-rename");
5+
const template = require("gulp-template");
6+
const sass = require("gulp-sass");
7+
const cleanCSS = require("gulp-clean-css");
8+
const htmlmin = require("gulp-htmlmin");
9+
const svgstore = require("gulp-svgstore");
10+
const svgmin = require("gulp-svgmin");
11+
const autoprefixer = require("gulp-autoprefixer");
12+
const rollup = require("rollup");
13+
const babel = require("rollup-plugin-babel");
14+
const uglify = require("rollup-plugin-uglify").uglify;
15+
const replace = require("rollup-plugin-replace");
16+
const browser = require("browser-sync").create();
17+
const Vinyl = require("vinyl");
18+
const Buffer = require("buffer").Buffer;
19+
const del = require("del");
20+
const Readable = require("stream").Readable;
21+
const createHash = require("crypto").createHash;
22+
const fs = require("fs");
23+
const basename = require("path").basename;
24+
25+
// constants
26+
const isProduction = () => process.env.NODE_ENV === "production";
27+
const pkg = require("./package.json");
28+
const babelPlugin = babel({
29+
presets: [["@babel/env", {modules: false}]],
30+
babelrc: false
31+
});
32+
const replacePlugin = replace({
33+
delimiters: ["<%= ", " %>"],
34+
"build_version": pkg.version,
35+
"build_date": getDateString()
36+
});
37+
const uglifyPlugin = uglify();
38+
let bundleCache;
39+
40+
// tasks
41+
gulp.task("serve", () => {
42+
browser.init({
43+
server: {baseDir: "./"},
44+
options: {ignored: "./dev/**/*"}
45+
});
46+
});
47+
48+
gulp.task("watch", () => {
49+
gulp.watch("./dev/src/**/*.js", gulp.series("js", browser.reload));
50+
gulp.watch("./index.html", gulp.series(browser.reload));
51+
gulp.watch("./dev/icons/*.svg", gulp.series("icons"));
52+
gulp.watch("./dev/inject/*", gulp.series("inject", browser.reload));
53+
// sass watch ignores colors_* files (themes)
54+
gulp.watch(["./dev/sass/**/*.scss", "!**/colors_*.scss"], gulp.series("sass"));
55+
// set up chokidar watcher to re-render themes
56+
gulp.watch("./dev/sass/colors_*.scss").on("change", renderTheme);
57+
});
58+
59+
gulp.task("js", () => {
60+
const plugins = [babelPlugin, replacePlugin];
61+
if (isProduction()) { plugins.push(uglifyPlugin); }
62+
return rollup.rollup({
63+
input: "./dev/src/app.js",
64+
cache: bundleCache,
65+
moduleContext: {
66+
"./dev/lib/codemirror.js": "window",
67+
"./dev/lib/clipboard.js": "window",
68+
"./dev/lib/native.js": "window"
69+
},
70+
plugins,
71+
onwarn: (warning, warn) => {
72+
// ignore circular dependency warnings
73+
if (warning.code === "CIRCULAR_DEPENDENCY") { return; }
74+
warn(warning);
75+
}
76+
}).then(bundle => {
77+
bundleCache = bundle.cache;
78+
return bundle.write({
79+
format: "iife",
80+
file: "./deploy/regexr.js",
81+
name: "regexr",
82+
sourcemap: !isProduction()
83+
})
84+
});
85+
});
86+
87+
gulp.task("sass", () => {
88+
const str = buildSass("default")
89+
.pipe(rename("regexr.css"))
90+
.pipe(gulp.dest("deploy"));
91+
92+
return isProduction()
93+
? str
94+
: str.pipe(browser.stream());
95+
});
96+
97+
// create tasks for all themes
98+
fs.readdirSync("./dev/sass").filter(f => /colors_\w+\.scss/.test(f)).forEach(f => {
99+
const theme = getThemeFromPath(f);
100+
gulp.task(`sass-${theme}`, () => {
101+
return diffTheme(theme).then(() => {
102+
return gulp.src(`./assets/themes/${theme}.css`)
103+
.pipe(browser.stream());
104+
})
105+
});
106+
});
107+
// manually render a theme via task, called from the chokidar listener in the watch task
108+
const renderTheme = filename => {
109+
const theme = getThemeFromPath(basename(filename));
110+
// wrapped in series() so it shows in the console
111+
gulp.series(gulp.task(`sass-${theme}`))();
112+
};
113+
114+
gulp.task("html", () => {
115+
return gulp.src("./index.html")
116+
.pipe(template({
117+
js_version: createFileHash("deploy/regexr.js"),
118+
css_version: createFileHash("deploy/regexr.css")
119+
}))
120+
.pipe(htmlmin({
121+
collapseWhitespace: true,
122+
conservativeCollapse: true,
123+
removeComments: true
124+
}))
125+
.pipe(gulp.dest("build"));
126+
});
127+
128+
gulp.task("icons", () => {
129+
return gulp.src("dev/icons/*.svg")
130+
// strip fill attributes and style tags to facilitate CSS styling:
131+
.pipe(svgmin({
132+
plugins: [
133+
{removeAttrs: {attrs: "fill"}},
134+
{removeStyleElement: true}
135+
]}
136+
))
137+
.pipe(svgstore({inlineSvg: true}))
138+
.pipe(gulp.dest("dev/inject"));
139+
});
140+
141+
gulp.task("inject", () => {
142+
return gulp.src("index.html")
143+
.pipe(inject(gulp.src("dev/inject/*"), {
144+
transform: (path, file) => {
145+
const tag = /\.css$/ig.test(path) ? "style" : "";
146+
return (tag ? `<${tag}>` : "") + file.contents.toString() + (tag ? `</${tag}>` : "");
147+
}
148+
}))
149+
.pipe(gulp.dest("."));
150+
});
151+
152+
gulp.task("clean", () => {
153+
return del([
154+
"build/**",
155+
"!build",
156+
"!build/sitemap.txt",
157+
"!build/{.git*,.git/**}",
158+
"!build/v1/**"
159+
]);
160+
});
161+
162+
gulp.task("copy", () => {
163+
// index.html is copied in by the html task
164+
return gulp.src([
165+
"deploy/**", "assets/**", "index.php", "server/**",
166+
"!deploy/*.map",
167+
"!server/**/composer.*",
168+
"!server/**/*.sql",
169+
"!server/**/*.md",
170+
"!server/gulpfile.js",
171+
"!server/Config*.php",
172+
"!server/**/*package*.json",
173+
"!server/{.git*,.git/**}",
174+
"!server/node_modules/",
175+
"!server/node_modules/**",
176+
], {base: "./"})
177+
.pipe(gulp.dest("./build/"));
178+
});
179+
180+
gulp.task("build", gulp.parallel("js", "sass"));
181+
182+
gulp.task("default",
183+
gulp.series("build",
184+
gulp.parallel("serve", "watch")
185+
)
186+
);
187+
188+
gulp.task("deploy",
189+
gulp.series(
190+
cb => (process.env.NODE_ENV = "production") && cb(),
191+
"clean", "build", "html", "copy"
192+
)
193+
);
194+
195+
// helpers
196+
function createFileHash(filename) {
197+
const hash = createHash("sha256");
198+
const fileContents = fs.readFileSync(filename, "utf-8");
199+
hash.update(fileContents);
200+
return hash.digest("hex").slice(0, 9);
201+
}
202+
203+
function getDateString() {
204+
const now = new Date();
205+
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
206+
return `${months[now.getMonth()]} ${now.getDate()}, ${now.getFullYear()}`;
207+
}
208+
209+
// theme "default", "light", "dark"
210+
function buildSass(theme) {
211+
// read (s)css dependencies for the temp file
212+
const libs = fs.readdirSync("./dev/lib").filter(file => /\.s?css$/.test(file));
213+
const base = "./dev/sass/";
214+
// sass file that is piped into the stream from memory
215+
const tmpSass = `
216+
${libs.map(f => `@import "../lib/${basename(f)}";`).join("\n")}
217+
@import "./colors${theme === "default" ? "" : "_" + theme}.scss";
218+
@import "./regexr.scss";
219+
`;
220+
const tmpFile = new Vinyl({
221+
cwd: "/",
222+
base,
223+
path: `${base + theme}.scss`,
224+
contents: Buffer.from(tmpSass)
225+
});
226+
// open an object stream and read the vinyl file in, piping thru the sass compilation
227+
const src = Readable({ objectMode: true });
228+
src._read = () => {
229+
src.push(tmpFile);
230+
src.push(null); // required for gulp to close properly
231+
};
232+
return src
233+
.pipe(sass().on("error", sass.logError))
234+
.pipe(autoprefixer({remove: false}))
235+
.pipe(cleanCSS());
236+
}
237+
238+
function diffTheme(theme) {
239+
const css = {};
240+
return Promise.all(
241+
// render both the default styles and the theme styles, saving the results
242+
["default", theme].map(type => new Promise((resolve, reject) => {
243+
buildSass(type).on("data", file => {
244+
css[type] = file.contents.toString();
245+
resolve();
246+
});
247+
}))
248+
).then(() => new Promise((resolve, reject) => {
249+
// diff the results, writing the results as the theme to override defaults
250+
const diff = (new CSSDiff()).diff(css.default, css[theme]);
251+
fs.writeFile(`./assets/themes/${theme}.css`, diff, resolve);
252+
}));
253+
}
254+
255+
function getThemeFromPath(filename) {
256+
return filename.match(/_(\w+)\.scss/)[1];
257+
}
258+
259+
// appended here to keep the build process as a single file:
260+
class CSSDiff {
261+
constructor() {}
262+
263+
diff(base, targ, pretty = false) {
264+
let diff = this.compare(this.parse(base), this.parse(targ));
265+
return this._writeDiff(diff, pretty);
266+
}
267+
268+
parse(s, o = {}) {
269+
this._parse(s, /([^\n\r\{\}]+?)\s*\{\s*/g, /\}/g, o);
270+
for (let n in o) {
271+
if (n === " keys") { continue; }
272+
o[n] = this.parseBlock(o[n]);
273+
}
274+
return o;
275+
}
276+
277+
parseBlock(s, o = {}) {
278+
return this._parse(s, /([^\s:]+)\s*:/g, /(?:;|$)/g, o);
279+
}
280+
281+
compare(o0, o1, o = {}) {
282+
let keys = o1[" keys"], l=keys.length, arr=[];
283+
for (let i=0; i<l; i++) {
284+
let n = keys[i];
285+
if (!o0[n]) { o[n] = o1[n]; arr.push(n); continue; }
286+
let diff = this._compareBlock(o0[n], o1[n]);
287+
if (diff) { o[n] = diff; arr.push(n); }
288+
}
289+
o[" keys"] = arr;
290+
return o;
291+
}
292+
293+
_compareBlock(o0, o1) {
294+
let keys = o1[" keys"], l=keys.length, arr=[], o;
295+
for (let i=0; i<l; i++) {
296+
let n = keys[i];
297+
if (o0[n] === o1[n]) { continue; }
298+
if (!o) { o = {}; }
299+
o[n] = o1[n];
300+
arr.push(n);
301+
}
302+
if (o) { o[" keys"] = arr; }
303+
return o;
304+
}
305+
306+
_parse(s, keyRE, closeRE, o) {
307+
let i, match, arr=[];
308+
while (match = keyRE.exec(s)) {
309+
let key = match[1];
310+
i = closeRE.lastIndex = keyRE.lastIndex;
311+
if (!(match = closeRE.exec(s))) { console.log("couldn't find close", key); break; }
312+
o[key] = s.substring(i, closeRE.lastIndex-match[0].length).trim();
313+
i = keyRE.lastIndex = closeRE.lastIndex;
314+
arr.push(key);
315+
}
316+
o[" keys"] = arr;
317+
return o;
318+
}
319+
320+
_writeDiff(o, pretty = false) {
321+
let diff = "", ln="\n", s=" ";
322+
if (!pretty) { ln = s = ""; }
323+
let keys = o[" keys"], l=keys.length;
324+
for (let i=0; i<l; i++) {
325+
let n = keys[i];
326+
if (diff) { diff += ln + ln; }
327+
diff += n + s + "{" + ln;
328+
diff += this._writeBlock(o[n], pretty);
329+
diff += "}";
330+
}
331+
return diff;
332+
}
333+
334+
_writeBlock(o, pretty = false) {
335+
let diff = "", ln="\n", t="\t", s=" ";
336+
if (!pretty) { ln = t = s = ""; }
337+
let keys = o[" keys"], l=keys.length;
338+
for (let i=0; i<l; i++) {
339+
let n = keys[i];
340+
diff += t + n + ":" + s + o[n] + ";" + ln;
341+
}
342+
return diff;
343+
}
344+
}
345+

0 commit comments

Comments
 (0)