|
| 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 | +export default 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