Kaynağa Gözat

Refactor assets bundler and split Javascript files

Frédéric Guillot 7 yıl önce
ebeveyn
işleme
53deb0b8cd
49 değiştirilmiş dosya ile 2821 ekleme ve 1984 silme
  1. 4 4
      Gopkg.lock
  2. 128 50
      generate.go
  3. 2 3
      ui/static/js.go
  4. 3 0
      ui/static/js/.jshintrc
  5. 0 823
      ui/static/js/app.js
  6. 74 0
      ui/static/js/bootstrap.js
  7. 47 0
      ui/static/js/confirm_handler.js
  8. 46 0
      ui/static/js/dom_helper.js
  9. 110 0
      ui/static/js/entry_handler.js
  10. 15 0
      ui/static/js/form_handler.js
  11. 63 0
      ui/static/js/keyboard_handler.js
  12. 27 0
      ui/static/js/menu_handler.js
  13. 31 0
      ui/static/js/modal_handler.js
  14. 11 0
      ui/static/js/mouse_handler.js
  15. 211 0
      ui/static/js/nav_handler.js
  16. 43 0
      ui/static/js/request_builder.js
  17. 94 0
      ui/static/js/touch_handler.js
  18. 33 0
      ui/static/js/unread_counter_handler.js
  19. 6 4
      vendor/github.com/tdewolff/minify/.goreleaser.yml
  20. 6 7
      vendor/github.com/tdewolff/minify/README.md
  21. 30 46
      vendor/github.com/tdewolff/minify/cmd/minify/README.md
  22. 101 99
      vendor/github.com/tdewolff/minify/cmd/minify/main.go
  23. 29 0
      vendor/github.com/tdewolff/minify/cmd/minify/minify_bash_tab_completion
  24. 86 14
      vendor/github.com/tdewolff/minify/cmd/minify/util.go
  25. 152 0
      vendor/github.com/tdewolff/minify/cmd/minify/util_test.go
  26. 1 1
      vendor/github.com/tdewolff/minify/cmd/minify/watch.go
  27. 137 14
      vendor/github.com/tdewolff/minify/common.go
  28. 103 14
      vendor/github.com/tdewolff/minify/common_test.go
  29. 321 199
      vendor/github.com/tdewolff/minify/css/css.go
  30. 39 1
      vendor/github.com/tdewolff/minify/css/css_test.go
  31. 20 8
      vendor/github.com/tdewolff/minify/css/table.go
  32. 14 10
      vendor/github.com/tdewolff/minify/html/html.go
  33. 2 0
      vendor/github.com/tdewolff/minify/html/html_test.go
  34. 0 1
      vendor/github.com/tdewolff/minify/html/table.go
  35. 14 5
      vendor/github.com/tdewolff/minify/js/js.go
  36. 11 0
      vendor/github.com/tdewolff/minify/js/js_test.go
  37. 9 1
      vendor/github.com/tdewolff/minify/svg/pathdata.go
  38. 8 3
      vendor/github.com/tdewolff/minify/svg/pathdata_test.go
  39. 2 33
      vendor/github.com/tdewolff/minify/svg/svg.go
  40. 3 3
      vendor/github.com/tdewolff/minify/svg/svg_test.go
  41. 0 12
      vendor/github.com/tdewolff/minify/svg/table.go
  42. 610 597
      vendor/github.com/tdewolff/parse/css/hash.go
  43. 4 0
      vendor/github.com/tdewolff/parse/css/parse.go
  44. 42 25
      vendor/github.com/tdewolff/parse/js/lex.go
  45. 9 6
      vendor/github.com/tdewolff/parse/js/lex_test.go
  46. 6 1
      vendor/github.com/tdewolff/parse/strconv/int.go
  47. 2 0
      vendor/github.com/tdewolff/parse/strconv/int_test.go
  48. 83 0
      vendor/github.com/tdewolff/parse/strconv/price.go
  49. 29 0
      vendor/github.com/tdewolff/parse/strconv/price_test.go

+ 4 - 4
Gopkg.lock

@@ -54,8 +54,8 @@
     "css",
     "js"
   ]
-  revision = "222672169d634c440a73abc47685074e1a9daa60"
-  version = "v2.3.4"
+  revision = "8d72a4127ae33b755e95bffede9b92e396267ce2"
+  version = "v2.3.5"
 
 [[projects]]
   name = "github.com/tdewolff/parse"
@@ -66,8 +66,8 @@
     "js",
     "strconv"
   ]
-  revision = "639f6272aec6b52094db77b9ec488214b0b4b1a1"
-  version = "v2.3.2"
+  revision = "d739d6fccb0971177e06352fea02d3552625efb1"
+  version = "v2.3.3"
 
 [[projects]]
   branch = "master"

+ 128 - 50
generate.go

@@ -35,82 +35,160 @@ var {{ .Map }}Checksums = map[string]string{
 {{ end }}}
 `
 
-var generatedTpl = template.Must(template.New("").Parse(tpl))
+var bundleTpl = template.Must(template.New("").Parse(tpl))
 
-type GeneratedFile struct {
+type Bundle struct {
 	Package, Map string
 	Files        map[string]string
 	Checksums    map[string]string
 }
 
-func normalizeBasename(filename string) string {
-	filename = strings.TrimSuffix(filename, filepath.Ext(filename))
-	return strings.Replace(filename, " ", "_", -1)
+func (b *Bundle) Write(filename string) {
+	f, err := os.Create(filename)
+	if err != nil {
+		panic(err)
+	}
+	defer f.Close()
+
+	bundleTpl.Execute(f, b)
 }
 
-func generateFile(serializer, pkg, mapName, pattern, output string) {
-	generatedFile := &GeneratedFile{
+func NewBundle(pkg, mapName string) *Bundle {
+	return &Bundle{
 		Package:   pkg,
 		Map:       mapName,
 		Files:     make(map[string]string),
 		Checksums: make(map[string]string),
 	}
+}
+
+func readFile(filename string) []byte {
+	data, err := ioutil.ReadFile(filename)
+	if err != nil {
+		panic(err)
+	}
+	return data
+}
 
+func checksum(data []byte) string {
+	return fmt.Sprintf("%x", sha256.Sum256(data))
+}
+
+func basename(filename string) string {
+	return path.Base(filename)
+}
+
+func stripExtension(filename string) string {
+	filename = strings.TrimSuffix(filename, filepath.Ext(filename))
+	return strings.Replace(filename, " ", "_", -1)
+}
+
+func glob(pattern string) []string {
 	files, _ := filepath.Glob(pattern)
+	return files
+}
+
+func concat(files []string) string {
+	var b strings.Builder
 	for _, file := range files {
-		basename := path.Base(file)
-		content, err := ioutil.ReadFile(file)
+		b.Write(readFile(file))
+	}
+	return b.String()
+}
+
+func generateJSBundle(bundleFile string, srcFiles []string) {
+	var b strings.Builder
+	b.WriteString("(function() {'use strict';")
+	b.WriteString(concat(srcFiles))
+	b.WriteString("})();")
+
+	m := minify.New()
+	m.AddFunc("text/javascript", js.Minify)
+
+	output, err := m.String("text/javascript", b.String())
+	if err != nil {
+		panic(err)
+	}
+
+	bundle := NewBundle("static", "Javascript")
+	bundle.Files["app"] = output
+	bundle.Checksums["app"] = checksum([]byte(output))
+	bundle.Write(bundleFile)
+}
+
+func generateCSSBundle(bundleFile string, srcFiles []string) {
+	bundle := NewBundle("static", "Stylesheets")
+
+	for _, srcFile := range srcFiles {
+		data := readFile(srcFile)
+		filename := stripExtension(basename(srcFile))
+
+		m := minify.New()
+		m.AddFunc("text/css", css.Minify)
+
+		minifiedData, err := m.Bytes("text/css", data)
 		if err != nil {
 			panic(err)
 		}
 
-		switch serializer {
-		case "css":
-			m := minify.New()
-			m.AddFunc("text/css", css.Minify)
-			content, err = m.Bytes("text/css", content)
-			if err != nil {
-				panic(err)
-			}
-
-			basename = normalizeBasename(basename)
-			generatedFile.Files[basename] = string(content)
-		case "js":
-			m := minify.New()
-			m.AddFunc("text/javascript", js.Minify)
-			content, err = m.Bytes("text/javascript", content)
-			if err != nil {
-				panic(err)
-			}
-
-			basename = normalizeBasename(basename)
-			generatedFile.Files[basename] = string(content)
-		case "base64":
-			encodedContent := base64.StdEncoding.EncodeToString(content)
-			generatedFile.Files[basename] = encodedContent
-		default:
-			basename = normalizeBasename(basename)
-			generatedFile.Files[basename] = string(content)
-		}
+		bundle.Files[filename] = string(minifiedData)
+		bundle.Checksums[filename] = checksum(minifiedData)
+	}
+
+	bundle.Write(bundleFile)
+}
 
-		generatedFile.Checksums[basename] = fmt.Sprintf("%x", sha256.Sum256(content))
+func generateBinaryBundle(bundleFile string, srcFiles []string) {
+	bundle := NewBundle("static", "Binaries")
+
+	for _, srcFile := range srcFiles {
+		data := readFile(srcFile)
+		filename := basename(srcFile)
+		encodedData := base64.StdEncoding.EncodeToString(data)
+
+		bundle.Files[filename] = string(encodedData)
+		bundle.Checksums[filename] = checksum(data)
 	}
 
-	f, err := os.Create(output)
-	if err != nil {
-		panic(err)
+	bundle.Write(bundleFile)
+}
+
+func generateBundle(bundleFile, pkg, mapName string, srcFiles []string) {
+	bundle := NewBundle(pkg, mapName)
+
+	for _, srcFile := range srcFiles {
+		data := readFile(srcFile)
+		filename := stripExtension(basename(srcFile))
+
+		bundle.Files[filename] = string(data)
+		bundle.Checksums[filename] = checksum(data)
 	}
-	defer f.Close()
 
-	generatedTpl.Execute(f, generatedFile)
+	bundle.Write(bundleFile)
 }
 
 func main() {
-	generateFile("none", "sql", "SqlMap", "sql/*.sql", "sql/sql.go")
-	generateFile("base64", "static", "Binaries", "ui/static/bin/*", "ui/static/bin.go")
-	generateFile("css", "static", "Stylesheets", "ui/static/css/*.css", "ui/static/css.go")
-	generateFile("js", "static", "Javascript", "ui/static/js/*.js", "ui/static/js.go")
-	generateFile("none", "template", "templateViewsMap", "template/html/*.html", "template/views.go")
-	generateFile("none", "template", "templateCommonMap", "template/html/common/*.html", "template/common.go")
-	generateFile("none", "locale", "translations", "locale/translations/*.json", "locale/translations.go")
+	generateJSBundle("ui/static/js.go", []string{
+		"ui/static/js/dom_helper.js",
+		"ui/static/js/touch_handler.js",
+		"ui/static/js/keyboard_handler.js",
+		"ui/static/js/mouse_handler.js",
+		"ui/static/js/form_handler.js",
+		"ui/static/js/request_builder.js",
+		"ui/static/js/unread_counter_handler.js",
+		"ui/static/js/entry_handler.js",
+		"ui/static/js/confirm_handler.js",
+		"ui/static/js/menu_handler.js",
+		"ui/static/js/modal_handler.js",
+		"ui/static/js/nav_handler.js",
+		"ui/static/js/bootstrap.js",
+	})
+
+	generateCSSBundle("ui/static/css.go", glob("ui/static/css/*.css"))
+	generateBinaryBundle("ui/static/bin.go", glob("ui/static/bin/*"))
+
+	generateBundle("sql/sql.go", "sql", "SqlMap", glob("sql/*.sql"))
+	generateBundle("template/views.go", "template", "templateViewsMap", glob("template/html/*.html"))
+	generateBundle("template/common.go", "template", "templateCommonMap", glob("template/html/common/*.html"))
+	generateBundle("locale/translations.go", "locale", "translations", glob("locale/translations/*.json"))
 }

+ 2 - 3
ui/static/js.go

@@ -34,8 +34,7 @@ if(this.queue.length>=2){this.queue=[];}};}
 isEventIgnored(event){return event.target.tagName==="INPUT"||event.target.tagName==="TEXTAREA";}
 getKey(event){const mapping={'Esc':'Escape','Up':'ArrowUp','Down':'ArrowDown','Left':'ArrowLeft','Right':'ArrowRight'};for(let key in mapping){if(mapping.hasOwnProperty(key)&&key===event.key){return mapping[key];}}
 return event.key;}}
-class FormHandler{static handleSubmitButtons(){let elements=document.querySelectorAll("form");elements.forEach((element)=>{element.onsubmit=()=>{let button=document.querySelector("button");if(button){button.innerHTML=button.dataset.labelLoading;button.disabled=true;}};});}}
-class MouseHandler{onClick(selector,callback){let elements=document.querySelectorAll(selector);elements.forEach((element)=>{element.onclick=(event)=>{event.preventDefault();callback(event);};});}}
+class MouseHandler{onClick(selector,callback){let elements=document.querySelectorAll(selector);elements.forEach((element)=>{element.onclick=(event)=>{event.preventDefault();callback(event);};});}}class FormHandler{static handleSubmitButtons(){let elements=document.querySelectorAll("form");elements.forEach((element)=>{element.onsubmit=()=>{let button=document.querySelector("button");if(button){button.innerHTML=button.dataset.labelLoading;button.disabled=true;}};});}}
 class RequestBuilder{constructor(url){this.callback=null;this.url=url;this.options={method:"POST",cache:"no-cache",credentials:"include",body:null,headers:new Headers({"Content-Type":"application/json","X-Csrf-Token":this.getCsrfToken()})};}
 withBody(body){this.options.body=JSON.stringify(body);return this;}
 withCallback(callback){this.callback=callback;return this;}
@@ -94,5 +93,5 @@ document.addEventListener("DOMContentLoaded",function(){FormHandler.handleSubmit
 }
 
 var JavascriptChecksums = map[string]string{
-	"app": "717f6c6431128b6263dc1f54edf6fd0c6efc3bcbc8c9baf23768c8f23ce53675",
+	"app": "c090bbc7f503aa032b4cfe68b58bc4754133aaed4f77ff768ac63f41528f55c3",
 }

+ 3 - 0
ui/static/js/.jshintrc

@@ -0,0 +1,3 @@
+{
+  "esversion": 6
+}

+ 0 - 823
ui/static/js/app.js

@@ -1,823 +0,0 @@
-/*jshint esversion: 6 */
-(function() {
-'use strict';
-
-class DomHelper {
-    static isVisible(element) {
-        return element.offsetParent !== null;
-    }
-
-    static openNewTab(url) {
-        let win = window.open("");
-        win.opener = null;
-        win.location = url;
-        win.focus();
-    }
-
-    static scrollPageTo(element) {
-        let windowScrollPosition = window.pageYOffset;
-        let windowHeight = document.documentElement.clientHeight;
-        let viewportPosition = windowScrollPosition + windowHeight;
-        let itemBottomPosition = element.offsetTop + element.offsetHeight;
-
-        if (viewportPosition - itemBottomPosition < 0 || viewportPosition - element.offsetTop > windowHeight) {
-            window.scrollTo(0, element.offsetTop - 10);
-        }
-    }
-
-    static getVisibleElements(selector) {
-        let elements = document.querySelectorAll(selector);
-        let result = [];
-
-        for (let i = 0; i < elements.length; i++) {
-            if (this.isVisible(elements[i])) {
-                result.push(elements[i]);
-            }
-        }
-
-        return result;
-    }
-
-    static findParent(element, selector) {
-        for (; element && element !== document; element = element.parentNode) {
-            if (element.classList.contains(selector)) {
-                return element;
-            }
-        }
-
-        return null;
-    }
-}
-
-class TouchHandler {
-    constructor() {
-        this.reset();
-    }
-
-    reset() {
-        this.touch = {
-            start: {x: -1, y: -1},
-            move: {x: -1, y: -1},
-            element: null
-        };
-    }
-
-    calculateDistance() {
-        if (this.touch.start.x >= -1 && this.touch.move.x >= -1) {
-            let horizontalDistance = Math.abs(this.touch.move.x - this.touch.start.x);
-            let verticalDistance = Math.abs(this.touch.move.y - this.touch.start.y);
-
-            if (horizontalDistance > 30 && verticalDistance < 70) {
-                return this.touch.move.x - this.touch.start.x;
-            }
-        }
-
-        return 0;
-    }
-
-    findElement(element) {
-        if (element.classList.contains("touch-item")) {
-            return element;
-        }
-
-        return DomHelper.findParent(element, "touch-item");
-    }
-
-    onTouchStart(event) {
-        if (event.touches === undefined || event.touches.length !== 1) {
-            return;
-        }
-
-        this.reset();
-        this.touch.start.x = event.touches[0].clientX;
-        this.touch.start.y = event.touches[0].clientY;
-        this.touch.element = this.findElement(event.touches[0].target);
-    }
-
-    onTouchMove(event) {
-        if (event.touches === undefined || event.touches.length !== 1 || this.element === null) {
-            return;
-        }
-
-        this.touch.move.x = event.touches[0].clientX;
-        this.touch.move.y = event.touches[0].clientY;
-
-        let distance = this.calculateDistance();
-        let absDistance = Math.abs(distance);
-
-        if (absDistance > 0) {
-            let opacity = 1 - (absDistance > 75 ? 0.9 : absDistance / 75 * 0.9);
-            let tx = distance > 75 ? 75 : (distance < -75 ? -75 : distance);
-
-            this.touch.element.style.opacity = opacity;
-            this.touch.element.style.transform = "translateX(" + tx + "px)";
-        }
-    }
-
-    onTouchEnd(event) {
-        if (event.touches === undefined) {
-            return;
-        }
-
-        if (this.touch.element !== null) {
-            let distance = Math.abs(this.calculateDistance());
-
-            if (distance > 75) {
-                EntryHandler.toggleEntryStatus(this.touch.element);
-            }
-            this.touch.element.style.opacity = 1;
-            this.touch.element.style.transform = "none";
-        }
-
-        this.reset();
-    }
-
-    listen() {
-        let elements = document.querySelectorAll(".touch-item");
-
-        elements.forEach((element) => {
-            element.addEventListener("touchstart", (e) => this.onTouchStart(e), false);
-            element.addEventListener("touchmove", (e) => this.onTouchMove(e), false);
-            element.addEventListener("touchend", (e) => this.onTouchEnd(e), false);
-            element.addEventListener("touchcancel", () => this.reset(), false);
-        });
-    }
-}
-
-class KeyboardHandler {
-    constructor() {
-        this.queue = [];
-        this.shortcuts = {};
-    }
-
-    on(combination, callback) {
-        this.shortcuts[combination] = callback;
-    }
-
-    listen() {
-        document.onkeydown = (event) => {
-            if (this.isEventIgnored(event)) {
-                return;
-            }
-
-            let key = this.getKey(event);
-            this.queue.push(key);
-
-            for (let combination in this.shortcuts) {
-                let keys = combination.split(" ");
-
-                if (keys.every((value, index) => value === this.queue[index])) {
-                    this.queue = [];
-                    this.shortcuts[combination](event);
-                    return;
-                }
-
-                if (keys.length === 1 && key === keys[0]) {
-                    this.queue = [];
-                    this.shortcuts[combination](event);
-                    return;
-                }
-            }
-
-            if (this.queue.length >= 2) {
-                this.queue = [];
-            }
-        };
-    }
-
-    isEventIgnored(event) {
-        return event.target.tagName === "INPUT" || event.target.tagName === "TEXTAREA";
-    }
-
-    getKey(event) {
-        const mapping = {
-            'Esc': 'Escape',
-            'Up': 'ArrowUp',
-            'Down': 'ArrowDown',
-            'Left': 'ArrowLeft',
-            'Right': 'ArrowRight'
-        };
-
-        for (let key in mapping) {
-            if (mapping.hasOwnProperty(key) && key === event.key) {
-                return mapping[key];
-            }
-        }
-
-        return event.key;
-    }
-}
-
-class FormHandler {
-    static handleSubmitButtons() {
-        let elements = document.querySelectorAll("form");
-        elements.forEach((element) => {
-            element.onsubmit = () => {
-                let button = document.querySelector("button");
-
-                if (button) {
-                    button.innerHTML = button.dataset.labelLoading;
-                    button.disabled = true;
-                }
-            };
-        });
-    }
-}
-
-class MouseHandler {
-    onClick(selector, callback) {
-        let elements = document.querySelectorAll(selector);
-        elements.forEach((element) => {
-            element.onclick = (event) => {
-                event.preventDefault();
-                callback(event);
-            };
-        });
-    }
-}
-
-class RequestBuilder {
-    constructor(url) {
-        this.callback = null;
-        this.url = url;
-        this.options = {
-            method: "POST",
-            cache: "no-cache",
-            credentials: "include",
-            body: null,
-            headers: new Headers({
-                "Content-Type": "application/json",
-                "X-Csrf-Token": this.getCsrfToken()
-            })
-        };
-    }
-
-    withBody(body) {
-        this.options.body = JSON.stringify(body);
-        return this;
-    }
-
-    withCallback(callback) {
-        this.callback = callback;
-        return this;
-    }
-
-    getCsrfToken() {
-        let element = document.querySelector("meta[name=X-CSRF-Token]");
-        if (element !== null) {
-            return element.getAttribute("value");
-        }
-
-        return "";
-    }
-
-    execute() {
-        fetch(new Request(this.url, this.options)).then((response) => {
-            if (this.callback) {
-                this.callback(response);
-            }
-        });
-    }
-}
-
-class UnreadCounterHandler {
-    static decrement(n) {
-        this.updateValue((current) => {
-            return current - n;
-        });
-    }
-
-    static increment(n) {
-        this.updateValue((current) => {
-            return current + n;
-        });
-    }
-
-    static updateValue(callback) {
-        let counterElements = document.querySelectorAll("span.unread-counter");
-        counterElements.forEach((element) => {
-            let oldValue = parseInt(element.textContent, 10);
-            element.innerHTML = callback(oldValue);
-        });
-
-        if (window.location.href.endsWith('/unread')) {
-            let oldValue = parseInt(document.title.split('(')[1], 10);
-            let newValue = callback(oldValue);
-
-            document.title = document.title.replace(
-                /(.*?)\(\d+\)(.*?)/,
-                function (match, prefix, suffix, offset, string) {
-                    return prefix + '(' + newValue + ')' + suffix;
-                }
-            );
-        }
-    }
-}
-
-class EntryHandler {
-    static updateEntriesStatus(entryIDs, status, callback) {
-        let url = document.body.dataset.entriesStatusUrl;
-        let request = new RequestBuilder(url);
-        request.withBody({entry_ids: entryIDs, status: status});
-        request.withCallback(callback);
-        request.execute();
-
-        if (status === "read") {
-            UnreadCounterHandler.decrement(1);
-        } else {
-            UnreadCounterHandler.increment(1);
-        }
-    }
-
-    static toggleEntryStatus(element) {
-        let entryID = parseInt(element.dataset.id, 10);
-        let statuses = {read: "unread", unread: "read"};
-
-        for (let currentStatus in statuses) {
-            let newStatus = statuses[currentStatus];
-
-            if (element.classList.contains("item-status-" + currentStatus)) {
-                element.classList.remove("item-status-" + currentStatus);
-                element.classList.add("item-status-" + newStatus);
-
-                this.updateEntriesStatus([entryID], newStatus);
-
-                let link = element.querySelector("a[data-toggle-status]");
-                if (link) {
-                    this.toggleLinkStatus(link);
-                }
-
-                break;
-            }
-        }
-    }
-
-    static toggleLinkStatus(link) {
-        if (link.dataset.value === "read") {
-            link.innerHTML = link.dataset.labelRead;
-            link.dataset.value = "unread";
-        } else {
-            link.innerHTML = link.dataset.labelUnread;
-            link.dataset.value = "read";
-        }
-    }
-
-    static toggleBookmark(element) {
-        element.innerHTML = element.dataset.labelLoading;
-
-        let request = new RequestBuilder(element.dataset.bookmarkUrl);
-        request.withCallback(() => {
-            if (element.dataset.value === "star") {
-                element.innerHTML = element.dataset.labelStar;
-                element.dataset.value = "unstar";
-            } else {
-                element.innerHTML = element.dataset.labelUnstar;
-                element.dataset.value = "star";
-            }
-        });
-        request.execute();
-    }
-
-    static markEntryAsRead(element) {
-        if (element.classList.contains("item-status-unread")) {
-            element.classList.remove("item-status-unread");
-            element.classList.add("item-status-read");
-
-            let entryID = parseInt(element.dataset.id, 10);
-            this.updateEntriesStatus([entryID], "read");
-        }
-    }
-
-    static saveEntry(element) {
-        if (element.dataset.completed) {
-            return;
-        }
-
-        element.innerHTML = element.dataset.labelLoading;
-
-        let request = new RequestBuilder(element.dataset.saveUrl);
-        request.withCallback(() => {
-            element.innerHTML = element.dataset.labelDone;
-            element.dataset.completed = true;
-        });
-        request.execute();
-    }
-
-    static fetchOriginalContent(element) {
-        if (element.dataset.completed) {
-            return;
-        }
-
-        element.innerHTML = element.dataset.labelLoading;
-
-        let request = new RequestBuilder(element.dataset.fetchContentUrl);
-        request.withCallback((response) => {
-            element.innerHTML = element.dataset.labelDone;
-            element.dataset.completed = true;
-
-            response.json().then((data) => {
-                if (data.hasOwnProperty("content")) {
-                    document.querySelector(".entry-content").innerHTML = data.content;
-                }
-            });
-        });
-        request.execute();
-    }
-}
-
-class ConfirmHandler {
-    remove(url) {
-        let request = new RequestBuilder(url);
-        request.withCallback(() => window.location.reload());
-        request.execute();
-    }
-
-    handle(event) {
-        let questionElement = document.createElement("span");
-        let linkElement = event.target;
-        let containerElement = linkElement.parentNode;
-        linkElement.style.display = "none";
-
-        let yesElement = document.createElement("a");
-        yesElement.href = "#";
-        yesElement.appendChild(document.createTextNode(linkElement.dataset.labelYes));
-        yesElement.onclick = (event) => {
-            event.preventDefault();
-
-            let loadingElement = document.createElement("span");
-            loadingElement.className = "loading";
-            loadingElement.appendChild(document.createTextNode(linkElement.dataset.labelLoading));
-
-            questionElement.remove();
-            containerElement.appendChild(loadingElement);
-
-            this.remove(linkElement.dataset.url);
-        };
-
-        let noElement = document.createElement("a");
-        noElement.href = "#";
-        noElement.appendChild(document.createTextNode(linkElement.dataset.labelNo));
-        noElement.onclick = (event) => {
-            event.preventDefault();
-            linkElement.style.display = "inline";
-            questionElement.remove();
-        };
-
-        questionElement.className = "confirm";
-        questionElement.appendChild(document.createTextNode(linkElement.dataset.labelQuestion + " "));
-        questionElement.appendChild(yesElement);
-        questionElement.appendChild(document.createTextNode(", "));
-        questionElement.appendChild(noElement);
-
-        containerElement.appendChild(questionElement);
-    }
-}
-
-class MenuHandler {
-    clickMenuListItem(event) {
-        let element = event.target;
-
-        if (element.tagName === "A") {
-            window.location.href = element.getAttribute("href");
-        } else {
-            window.location.href = element.querySelector("a").getAttribute("href");
-        }
-    }
-
-    toggleMainMenu() {
-        let menu = document.querySelector(".header nav ul");
-        if (DomHelper.isVisible(menu)) {
-            menu.style.display = "none";
-        } else {
-            menu.style.display = "block";
-        }
-
-        let searchElement = document.querySelector(".header .search");
-        if (DomHelper.isVisible(searchElement)) {
-            searchElement.style.display = "none";
-        } else {
-            searchElement.style.display = "block";
-        }
-    }
-}
-
-class ModalHandler {
-    static exists() {
-        return document.getElementById("modal-container") !== null;
-    }
-
-    static open(fragment) {
-        if (ModalHandler.exists()) {
-            return;
-        }
-
-        let container = document.createElement("div");
-        container.id = "modal-container";
-        container.appendChild(document.importNode(fragment, true));
-        document.body.appendChild(container);
-
-        let closeButton = document.querySelector("a.btn-close-modal");
-        if (closeButton !== null) {
-            closeButton.onclick = (event) => {
-                event.preventDefault();
-                ModalHandler.close();
-            };
-        }
-    }
-
-    static close() {
-        let container = document.getElementById("modal-container");
-        if (container !== null) {
-            container.parentNode.removeChild(container);
-        }
-    }
-}
-
-class NavHandler {
-    setFocusToSearchInput(event) {
-        event.preventDefault();
-        event.stopPropagation();
-
-        let toggleSwitchElement = document.querySelector(".search-toggle-switch");
-        if (toggleSwitchElement) {
-            toggleSwitchElement.style.display = "none";
-        }
-
-        let searchFormElement = document.querySelector(".search-form");
-        if (searchFormElement) {
-            searchFormElement.style.display = "block";
-        }
-
-        let searchInputElement = document.getElementById("search-input");
-        if (searchInputElement) {
-            searchInputElement.focus();
-            searchInputElement.value = "";
-        }
-    }
-
-    showKeyboardShortcuts() {
-        let template = document.getElementById("keyboard-shortcuts");
-        if (template !== null) {
-            ModalHandler.open(template.content);
-        }
-    }
-
-    markPageAsRead() {
-        let items = DomHelper.getVisibleElements(".items .item");
-        let entryIDs = [];
-
-        items.forEach((element) => {
-            element.classList.add("item-status-read");
-            entryIDs.push(parseInt(element.dataset.id, 10));
-        });
-
-        if (entryIDs.length > 0) {
-            EntryHandler.updateEntriesStatus(entryIDs, "read", () => {
-                // This callback make sure the Ajax request reach the server before we reload the page.
-                this.goToPage("next", true);
-            });
-        }
-    }
-
-    saveEntry() {
-        if (this.isListView()) {
-            let currentItem = document.querySelector(".current-item");
-            if (currentItem !== null) {
-                let saveLink = currentItem.querySelector("a[data-save-entry]");
-                if (saveLink) {
-                    EntryHandler.saveEntry(saveLink);
-                }
-            }
-        } else {
-            let saveLink = document.querySelector("a[data-save-entry]");
-            if (saveLink) {
-                EntryHandler.saveEntry(saveLink);
-            }
-        }
-    }
-
-    fetchOriginalContent() {
-        if (! this.isListView()){
-            let link = document.querySelector("a[data-fetch-content-entry]");
-            if (link) {
-                EntryHandler.fetchOriginalContent(link);
-            }
-        }
-    }
-
-    toggleEntryStatus() {
-        let currentItem = document.querySelector(".current-item");
-        if (currentItem !== null) {
-            // The order is important here,
-            // On the unread page, the read item will be hidden.
-            this.goToNextListItem();
-            EntryHandler.toggleEntryStatus(currentItem);
-        }
-    }
-
-    toggleBookmark() {
-        if (! this.isListView()) {
-            this.toggleBookmarkLink(document.querySelector(".entry"));
-            return;
-        }
-
-        let currentItem = document.querySelector(".current-item");
-        if (currentItem !== null) {
-            this.toggleBookmarkLink(currentItem);
-        }
-    }
-
-    toggleBookmarkLink(parent) {
-        let bookmarkLink = parent.querySelector("a[data-toggle-bookmark]");
-        if (bookmarkLink) {
-            EntryHandler.toggleBookmark(bookmarkLink);
-        }
-    }
-
-    openOriginalLink() {
-        let entryLink = document.querySelector(".entry h1 a");
-        if (entryLink !== null) {
-            DomHelper.openNewTab(entryLink.getAttribute("href"));
-            return;
-        }
-
-        let currentItemOriginalLink = document.querySelector(".current-item a[data-original-link]");
-        if (currentItemOriginalLink !== null) {
-            DomHelper.openNewTab(currentItemOriginalLink.getAttribute("href"));
-
-            // Move to the next item and if we are on the unread page mark this item as read.
-            let currentItem = document.querySelector(".current-item");
-            this.goToNextListItem();
-            EntryHandler.markEntryAsRead(currentItem);
-        }
-    }
-
-    openSelectedItem() {
-        let currentItemLink = document.querySelector(".current-item .item-title a");
-        if (currentItemLink !== null) {
-            window.location.href = currentItemLink.getAttribute("href");
-        }
-    }
-
-    /**
-     * @param {string} page Page to redirect to.
-     * @param {boolean} fallbackSelf Refresh actual page if the page is not found.
-     */
-    goToPage(page, fallbackSelf) {
-        let element = document.querySelector("a[data-page=" + page + "]");
-
-        if (element) {
-            document.location.href = element.href;
-        } else if (fallbackSelf) {
-            window.location.reload();
-        }
-    }
-
-    goToPrevious() {
-        if (this.isListView()) {
-            this.goToPreviousListItem();
-        } else {
-            this.goToPage("previous");
-        }
-    }
-
-    goToNext() {
-        if (this.isListView()) {
-            this.goToNextListItem();
-        } else {
-            this.goToPage("next");
-        }
-    }
-
-    goToPreviousListItem() {
-        let items = DomHelper.getVisibleElements(".items .item");
-        if (items.length === 0) {
-            return;
-        }
-
-        if (document.querySelector(".current-item") === null) {
-            items[0].classList.add("current-item");
-            return;
-        }
-
-        for (let i = 0; i < items.length; i++) {
-            if (items[i].classList.contains("current-item")) {
-                items[i].classList.remove("current-item");
-
-                if (i - 1 >= 0) {
-                    items[i - 1].classList.add("current-item");
-                    DomHelper.scrollPageTo(items[i - 1]);
-                }
-
-                break;
-            }
-        }
-    }
-
-    goToNextListItem() {
-        let currentItem = document.querySelector(".current-item");
-        let items = DomHelper.getVisibleElements(".items .item");
-        if (items.length === 0) {
-            return;
-        }
-
-        if (currentItem === null) {
-            items[0].classList.add("current-item");
-            return;
-        }
-
-        for (let i = 0; i < items.length; i++) {
-            if (items[i].classList.contains("current-item")) {
-                items[i].classList.remove("current-item");
-
-                if (i + 1 < items.length) {
-                    items[i + 1].classList.add("current-item");
-                    DomHelper.scrollPageTo(items[i + 1]);
-                }
-
-                break;
-            }
-        }
-    }
-
-    isListView() {
-        return document.querySelector(".items") !== null;
-    }
-}
-
-document.addEventListener("DOMContentLoaded", function() {
-    FormHandler.handleSubmitButtons();
-
-    let touchHandler = new TouchHandler();
-    touchHandler.listen();
-
-    let navHandler = new NavHandler();
-    let keyboardHandler = new KeyboardHandler();
-    keyboardHandler.on("g u", () => navHandler.goToPage("unread"));
-    keyboardHandler.on("g b", () => navHandler.goToPage("starred"));
-    keyboardHandler.on("g h", () => navHandler.goToPage("history"));
-    keyboardHandler.on("g f", () => navHandler.goToPage("feeds"));
-    keyboardHandler.on("g c", () => navHandler.goToPage("categories"));
-    keyboardHandler.on("g s", () => navHandler.goToPage("settings"));
-    keyboardHandler.on("ArrowLeft", () => navHandler.goToPrevious());
-    keyboardHandler.on("ArrowRight", () => navHandler.goToNext());
-    keyboardHandler.on("j", () => navHandler.goToPrevious());
-    keyboardHandler.on("p", () => navHandler.goToPrevious());
-    keyboardHandler.on("k", () => navHandler.goToNext());
-    keyboardHandler.on("n", () => navHandler.goToNext());
-    keyboardHandler.on("h", () => navHandler.goToPage("previous"));
-    keyboardHandler.on("l", () => navHandler.goToPage("next"));
-    keyboardHandler.on("o", () => navHandler.openSelectedItem());
-    keyboardHandler.on("v", () => navHandler.openOriginalLink());
-    keyboardHandler.on("m", () => navHandler.toggleEntryStatus());
-    keyboardHandler.on("A", () => navHandler.markPageAsRead());
-    keyboardHandler.on("s", () => navHandler.saveEntry());
-    keyboardHandler.on("d", () => navHandler.fetchOriginalContent());
-    keyboardHandler.on("f", () => navHandler.toggleBookmark());
-    keyboardHandler.on("?", () => navHandler.showKeyboardShortcuts());
-    keyboardHandler.on("/", (e) => navHandler.setFocusToSearchInput(e));
-    keyboardHandler.on("Escape", () => ModalHandler.close());
-    keyboardHandler.listen();
-
-    let mouseHandler = new MouseHandler();
-    mouseHandler.onClick("a[data-save-entry]", (event) => {
-        event.preventDefault();
-        EntryHandler.saveEntry(event.target);
-    });
-
-    mouseHandler.onClick("a[data-toggle-bookmark]", (event) => {
-        event.preventDefault();
-        EntryHandler.toggleBookmark(event.target);
-    });
-
-    mouseHandler.onClick("a[data-toggle-status]", (event) => {
-        event.preventDefault();
-
-        let currentItem = DomHelper.findParent(event.target, "item");
-        if (currentItem) {
-            EntryHandler.toggleEntryStatus(currentItem);
-        }
-    });
-
-    mouseHandler.onClick("a[data-fetch-content-entry]", (event) => {
-        event.preventDefault();
-        EntryHandler.fetchOriginalContent(event.target);
-    });
-
-    mouseHandler.onClick("a[data-on-click=markPageAsRead]", () => navHandler.markPageAsRead());
-    mouseHandler.onClick("a[data-confirm]", (event) => {
-        (new ConfirmHandler()).handle(event);
-    });
-
-    mouseHandler.onClick("a[data-action=search]", (event) => {
-        navHandler.setFocusToSearchInput(event);
-    });
-
-    if (document.documentElement.clientWidth < 600) {
-        let menuHandler = new MenuHandler();
-        mouseHandler.onClick(".logo", () => menuHandler.toggleMainMenu());
-        mouseHandler.onClick(".header nav li", (event) => menuHandler.clickMenuListItem(event));
-    }
-});
-
-})();

+ 74 - 0
ui/static/js/bootstrap.js

@@ -0,0 +1,74 @@
+document.addEventListener("DOMContentLoaded", function() {
+    FormHandler.handleSubmitButtons();
+
+    let touchHandler = new TouchHandler();
+    touchHandler.listen();
+
+    let navHandler = new NavHandler();
+    let keyboardHandler = new KeyboardHandler();
+    keyboardHandler.on("g u", () => navHandler.goToPage("unread"));
+    keyboardHandler.on("g b", () => navHandler.goToPage("starred"));
+    keyboardHandler.on("g h", () => navHandler.goToPage("history"));
+    keyboardHandler.on("g f", () => navHandler.goToPage("feeds"));
+    keyboardHandler.on("g c", () => navHandler.goToPage("categories"));
+    keyboardHandler.on("g s", () => navHandler.goToPage("settings"));
+    keyboardHandler.on("ArrowLeft", () => navHandler.goToPrevious());
+    keyboardHandler.on("ArrowRight", () => navHandler.goToNext());
+    keyboardHandler.on("j", () => navHandler.goToPrevious());
+    keyboardHandler.on("p", () => navHandler.goToPrevious());
+    keyboardHandler.on("k", () => navHandler.goToNext());
+    keyboardHandler.on("n", () => navHandler.goToNext());
+    keyboardHandler.on("h", () => navHandler.goToPage("previous"));
+    keyboardHandler.on("l", () => navHandler.goToPage("next"));
+    keyboardHandler.on("o", () => navHandler.openSelectedItem());
+    keyboardHandler.on("v", () => navHandler.openOriginalLink());
+    keyboardHandler.on("m", () => navHandler.toggleEntryStatus());
+    keyboardHandler.on("A", () => navHandler.markPageAsRead());
+    keyboardHandler.on("s", () => navHandler.saveEntry());
+    keyboardHandler.on("d", () => navHandler.fetchOriginalContent());
+    keyboardHandler.on("f", () => navHandler.toggleBookmark());
+    keyboardHandler.on("?", () => navHandler.showKeyboardShortcuts());
+    keyboardHandler.on("/", (e) => navHandler.setFocusToSearchInput(e));
+    keyboardHandler.on("Escape", () => ModalHandler.close());
+    keyboardHandler.listen();
+
+    let mouseHandler = new MouseHandler();
+    mouseHandler.onClick("a[data-save-entry]", (event) => {
+        event.preventDefault();
+        EntryHandler.saveEntry(event.target);
+    });
+
+    mouseHandler.onClick("a[data-toggle-bookmark]", (event) => {
+        event.preventDefault();
+        EntryHandler.toggleBookmark(event.target);
+    });
+
+    mouseHandler.onClick("a[data-toggle-status]", (event) => {
+        event.preventDefault();
+
+        let currentItem = DomHelper.findParent(event.target, "item");
+        if (currentItem) {
+            EntryHandler.toggleEntryStatus(currentItem);
+        }
+    });
+
+    mouseHandler.onClick("a[data-fetch-content-entry]", (event) => {
+        event.preventDefault();
+        EntryHandler.fetchOriginalContent(event.target);
+    });
+
+    mouseHandler.onClick("a[data-on-click=markPageAsRead]", () => navHandler.markPageAsRead());
+    mouseHandler.onClick("a[data-confirm]", (event) => {
+        (new ConfirmHandler()).handle(event);
+    });
+
+    mouseHandler.onClick("a[data-action=search]", (event) => {
+        navHandler.setFocusToSearchInput(event);
+    });
+
+    if (document.documentElement.clientWidth < 600) {
+        let menuHandler = new MenuHandler();
+        mouseHandler.onClick(".logo", () => menuHandler.toggleMainMenu());
+        mouseHandler.onClick(".header nav li", (event) => menuHandler.clickMenuListItem(event));
+    }
+});

+ 47 - 0
ui/static/js/confirm_handler.js

@@ -0,0 +1,47 @@
+class ConfirmHandler {
+    remove(url) {
+        let request = new RequestBuilder(url);
+        request.withCallback(() => window.location.reload());
+        request.execute();
+    }
+
+    handle(event) {
+        let questionElement = document.createElement("span");
+        let linkElement = event.target;
+        let containerElement = linkElement.parentNode;
+        linkElement.style.display = "none";
+
+        let yesElement = document.createElement("a");
+        yesElement.href = "#";
+        yesElement.appendChild(document.createTextNode(linkElement.dataset.labelYes));
+        yesElement.onclick = (event) => {
+            event.preventDefault();
+
+            let loadingElement = document.createElement("span");
+            loadingElement.className = "loading";
+            loadingElement.appendChild(document.createTextNode(linkElement.dataset.labelLoading));
+
+            questionElement.remove();
+            containerElement.appendChild(loadingElement);
+
+            this.remove(linkElement.dataset.url);
+        };
+
+        let noElement = document.createElement("a");
+        noElement.href = "#";
+        noElement.appendChild(document.createTextNode(linkElement.dataset.labelNo));
+        noElement.onclick = (event) => {
+            event.preventDefault();
+            linkElement.style.display = "inline";
+            questionElement.remove();
+        };
+
+        questionElement.className = "confirm";
+        questionElement.appendChild(document.createTextNode(linkElement.dataset.labelQuestion + " "));
+        questionElement.appendChild(yesElement);
+        questionElement.appendChild(document.createTextNode(", "));
+        questionElement.appendChild(noElement);
+
+        containerElement.appendChild(questionElement);
+    }
+}

+ 46 - 0
ui/static/js/dom_helper.js

@@ -0,0 +1,46 @@
+class DomHelper {
+    static isVisible(element) {
+        return element.offsetParent !== null;
+    }
+
+    static openNewTab(url) {
+        let win = window.open("");
+        win.opener = null;
+        win.location = url;
+        win.focus();
+    }
+
+    static scrollPageTo(element) {
+        let windowScrollPosition = window.pageYOffset;
+        let windowHeight = document.documentElement.clientHeight;
+        let viewportPosition = windowScrollPosition + windowHeight;
+        let itemBottomPosition = element.offsetTop + element.offsetHeight;
+
+        if (viewportPosition - itemBottomPosition < 0 || viewportPosition - element.offsetTop > windowHeight) {
+            window.scrollTo(0, element.offsetTop - 10);
+        }
+    }
+
+    static getVisibleElements(selector) {
+        let elements = document.querySelectorAll(selector);
+        let result = [];
+
+        for (let i = 0; i < elements.length; i++) {
+            if (this.isVisible(elements[i])) {
+                result.push(elements[i]);
+            }
+        }
+
+        return result;
+    }
+
+    static findParent(element, selector) {
+        for (; element && element !== document; element = element.parentNode) {
+            if (element.classList.contains(selector)) {
+                return element;
+            }
+        }
+
+        return null;
+    }
+}

+ 110 - 0
ui/static/js/entry_handler.js

@@ -0,0 +1,110 @@
+class EntryHandler {
+    static updateEntriesStatus(entryIDs, status, callback) {
+        let url = document.body.dataset.entriesStatusUrl;
+        let request = new RequestBuilder(url);
+        request.withBody({entry_ids: entryIDs, status: status});
+        request.withCallback(callback);
+        request.execute();
+
+        if (status === "read") {
+            UnreadCounterHandler.decrement(1);
+        } else {
+            UnreadCounterHandler.increment(1);
+        }
+    }
+
+    static toggleEntryStatus(element) {
+        let entryID = parseInt(element.dataset.id, 10);
+        let statuses = {read: "unread", unread: "read"};
+
+        for (let currentStatus in statuses) {
+            let newStatus = statuses[currentStatus];
+
+            if (element.classList.contains("item-status-" + currentStatus)) {
+                element.classList.remove("item-status-" + currentStatus);
+                element.classList.add("item-status-" + newStatus);
+
+                this.updateEntriesStatus([entryID], newStatus);
+
+                let link = element.querySelector("a[data-toggle-status]");
+                if (link) {
+                    this.toggleLinkStatus(link);
+                }
+
+                break;
+            }
+        }
+    }
+
+    static toggleLinkStatus(link) {
+        if (link.dataset.value === "read") {
+            link.innerHTML = link.dataset.labelRead;
+            link.dataset.value = "unread";
+        } else {
+            link.innerHTML = link.dataset.labelUnread;
+            link.dataset.value = "read";
+        }
+    }
+
+    static toggleBookmark(element) {
+        element.innerHTML = element.dataset.labelLoading;
+
+        let request = new RequestBuilder(element.dataset.bookmarkUrl);
+        request.withCallback(() => {
+            if (element.dataset.value === "star") {
+                element.innerHTML = element.dataset.labelStar;
+                element.dataset.value = "unstar";
+            } else {
+                element.innerHTML = element.dataset.labelUnstar;
+                element.dataset.value = "star";
+            }
+        });
+        request.execute();
+    }
+
+    static markEntryAsRead(element) {
+        if (element.classList.contains("item-status-unread")) {
+            element.classList.remove("item-status-unread");
+            element.classList.add("item-status-read");
+
+            let entryID = parseInt(element.dataset.id, 10);
+            this.updateEntriesStatus([entryID], "read");
+        }
+    }
+
+    static saveEntry(element) {
+        if (element.dataset.completed) {
+            return;
+        }
+
+        element.innerHTML = element.dataset.labelLoading;
+
+        let request = new RequestBuilder(element.dataset.saveUrl);
+        request.withCallback(() => {
+            element.innerHTML = element.dataset.labelDone;
+            element.dataset.completed = true;
+        });
+        request.execute();
+    }
+
+    static fetchOriginalContent(element) {
+        if (element.dataset.completed) {
+            return;
+        }
+
+        element.innerHTML = element.dataset.labelLoading;
+
+        let request = new RequestBuilder(element.dataset.fetchContentUrl);
+        request.withCallback((response) => {
+            element.innerHTML = element.dataset.labelDone;
+            element.dataset.completed = true;
+
+            response.json().then((data) => {
+                if (data.hasOwnProperty("content")) {
+                    document.querySelector(".entry-content").innerHTML = data.content;
+                }
+            });
+        });
+        request.execute();
+    }
+}

+ 15 - 0
ui/static/js/form_handler.js

@@ -0,0 +1,15 @@
+class FormHandler {
+    static handleSubmitButtons() {
+        let elements = document.querySelectorAll("form");
+        elements.forEach((element) => {
+            element.onsubmit = () => {
+                let button = document.querySelector("button");
+
+                if (button) {
+                    button.innerHTML = button.dataset.labelLoading;
+                    button.disabled = true;
+                }
+            };
+        });
+    }
+}

+ 63 - 0
ui/static/js/keyboard_handler.js

@@ -0,0 +1,63 @@
+class KeyboardHandler {
+    constructor() {
+        this.queue = [];
+        this.shortcuts = {};
+    }
+
+    on(combination, callback) {
+        this.shortcuts[combination] = callback;
+    }
+
+    listen() {
+        document.onkeydown = (event) => {
+            if (this.isEventIgnored(event)) {
+                return;
+            }
+
+            let key = this.getKey(event);
+            this.queue.push(key);
+
+            for (let combination in this.shortcuts) {
+                let keys = combination.split(" ");
+
+                if (keys.every((value, index) => value === this.queue[index])) {
+                    this.queue = [];
+                    this.shortcuts[combination](event);
+                    return;
+                }
+
+                if (keys.length === 1 && key === keys[0]) {
+                    this.queue = [];
+                    this.shortcuts[combination](event);
+                    return;
+                }
+            }
+
+            if (this.queue.length >= 2) {
+                this.queue = [];
+            }
+        };
+    }
+
+    isEventIgnored(event) {
+        return event.target.tagName === "INPUT" || event.target.tagName === "TEXTAREA";
+    }
+
+    getKey(event) {
+        const mapping = {
+            'Esc': 'Escape',
+            'Up': 'ArrowUp',
+            'Down': 'ArrowDown',
+            'Left': 'ArrowLeft',
+            'Right': 'ArrowRight'
+        };
+
+        for (let key in mapping) {
+            if (mapping.hasOwnProperty(key) && key === event.key) {
+                return mapping[key];
+            }
+        }
+
+        return event.key;
+    }
+}

+ 27 - 0
ui/static/js/menu_handler.js

@@ -0,0 +1,27 @@
+class MenuHandler {
+    clickMenuListItem(event) {
+        let element = event.target;
+
+        if (element.tagName === "A") {
+            window.location.href = element.getAttribute("href");
+        } else {
+            window.location.href = element.querySelector("a").getAttribute("href");
+        }
+    }
+
+    toggleMainMenu() {
+        let menu = document.querySelector(".header nav ul");
+        if (DomHelper.isVisible(menu)) {
+            menu.style.display = "none";
+        } else {
+            menu.style.display = "block";
+        }
+
+        let searchElement = document.querySelector(".header .search");
+        if (DomHelper.isVisible(searchElement)) {
+            searchElement.style.display = "none";
+        } else {
+            searchElement.style.display = "block";
+        }
+    }
+}

+ 31 - 0
ui/static/js/modal_handler.js

@@ -0,0 +1,31 @@
+class ModalHandler {
+    static exists() {
+        return document.getElementById("modal-container") !== null;
+    }
+
+    static open(fragment) {
+        if (ModalHandler.exists()) {
+            return;
+        }
+
+        let container = document.createElement("div");
+        container.id = "modal-container";
+        container.appendChild(document.importNode(fragment, true));
+        document.body.appendChild(container);
+
+        let closeButton = document.querySelector("a.btn-close-modal");
+        if (closeButton !== null) {
+            closeButton.onclick = (event) => {
+                event.preventDefault();
+                ModalHandler.close();
+            };
+        }
+    }
+
+    static close() {
+        let container = document.getElementById("modal-container");
+        if (container !== null) {
+            container.parentNode.removeChild(container);
+        }
+    }
+}

+ 11 - 0
ui/static/js/mouse_handler.js

@@ -0,0 +1,11 @@
+class MouseHandler {
+    onClick(selector, callback) {
+        let elements = document.querySelectorAll(selector);
+        elements.forEach((element) => {
+            element.onclick = (event) => {
+                event.preventDefault();
+                callback(event);
+            };
+        });
+    }
+}

+ 211 - 0
ui/static/js/nav_handler.js

@@ -0,0 +1,211 @@
+class NavHandler {
+    setFocusToSearchInput(event) {
+        event.preventDefault();
+        event.stopPropagation();
+
+        let toggleSwitchElement = document.querySelector(".search-toggle-switch");
+        if (toggleSwitchElement) {
+            toggleSwitchElement.style.display = "none";
+        }
+
+        let searchFormElement = document.querySelector(".search-form");
+        if (searchFormElement) {
+            searchFormElement.style.display = "block";
+        }
+
+        let searchInputElement = document.getElementById("search-input");
+        if (searchInputElement) {
+            searchInputElement.focus();
+            searchInputElement.value = "";
+        }
+    }
+
+    showKeyboardShortcuts() {
+        let template = document.getElementById("keyboard-shortcuts");
+        if (template !== null) {
+            ModalHandler.open(template.content);
+        }
+    }
+
+    markPageAsRead() {
+        let items = DomHelper.getVisibleElements(".items .item");
+        let entryIDs = [];
+
+        items.forEach((element) => {
+            element.classList.add("item-status-read");
+            entryIDs.push(parseInt(element.dataset.id, 10));
+        });
+
+        if (entryIDs.length > 0) {
+            EntryHandler.updateEntriesStatus(entryIDs, "read", () => {
+                // This callback make sure the Ajax request reach the server before we reload the page.
+                this.goToPage("next", true);
+            });
+        }
+    }
+
+    saveEntry() {
+        if (this.isListView()) {
+            let currentItem = document.querySelector(".current-item");
+            if (currentItem !== null) {
+                let saveLink = currentItem.querySelector("a[data-save-entry]");
+                if (saveLink) {
+                    EntryHandler.saveEntry(saveLink);
+                }
+            }
+        } else {
+            let saveLink = document.querySelector("a[data-save-entry]");
+            if (saveLink) {
+                EntryHandler.saveEntry(saveLink);
+            }
+        }
+    }
+
+    fetchOriginalContent() {
+        if (! this.isListView()){
+            let link = document.querySelector("a[data-fetch-content-entry]");
+            if (link) {
+                EntryHandler.fetchOriginalContent(link);
+            }
+        }
+    }
+
+    toggleEntryStatus() {
+        let currentItem = document.querySelector(".current-item");
+        if (currentItem !== null) {
+            // The order is important here,
+            // On the unread page, the read item will be hidden.
+            this.goToNextListItem();
+            EntryHandler.toggleEntryStatus(currentItem);
+        }
+    }
+
+    toggleBookmark() {
+        if (! this.isListView()) {
+            this.toggleBookmarkLink(document.querySelector(".entry"));
+            return;
+        }
+
+        let currentItem = document.querySelector(".current-item");
+        if (currentItem !== null) {
+            this.toggleBookmarkLink(currentItem);
+        }
+    }
+
+    toggleBookmarkLink(parent) {
+        let bookmarkLink = parent.querySelector("a[data-toggle-bookmark]");
+        if (bookmarkLink) {
+            EntryHandler.toggleBookmark(bookmarkLink);
+        }
+    }
+
+    openOriginalLink() {
+        let entryLink = document.querySelector(".entry h1 a");
+        if (entryLink !== null) {
+            DomHelper.openNewTab(entryLink.getAttribute("href"));
+            return;
+        }
+
+        let currentItemOriginalLink = document.querySelector(".current-item a[data-original-link]");
+        if (currentItemOriginalLink !== null) {
+            DomHelper.openNewTab(currentItemOriginalLink.getAttribute("href"));
+
+            // Move to the next item and if we are on the unread page mark this item as read.
+            let currentItem = document.querySelector(".current-item");
+            this.goToNextListItem();
+            EntryHandler.markEntryAsRead(currentItem);
+        }
+    }
+
+    openSelectedItem() {
+        let currentItemLink = document.querySelector(".current-item .item-title a");
+        if (currentItemLink !== null) {
+            window.location.href = currentItemLink.getAttribute("href");
+        }
+    }
+
+    /**
+     * @param {string} page Page to redirect to.
+     * @param {boolean} fallbackSelf Refresh actual page if the page is not found.
+     */
+    goToPage(page, fallbackSelf) {
+        let element = document.querySelector("a[data-page=" + page + "]");
+
+        if (element) {
+            document.location.href = element.href;
+        } else if (fallbackSelf) {
+            window.location.reload();
+        }
+    }
+
+    goToPrevious() {
+        if (this.isListView()) {
+            this.goToPreviousListItem();
+        } else {
+            this.goToPage("previous");
+        }
+    }
+
+    goToNext() {
+        if (this.isListView()) {
+            this.goToNextListItem();
+        } else {
+            this.goToPage("next");
+        }
+    }
+
+    goToPreviousListItem() {
+        let items = DomHelper.getVisibleElements(".items .item");
+        if (items.length === 0) {
+            return;
+        }
+
+        if (document.querySelector(".current-item") === null) {
+            items[0].classList.add("current-item");
+            return;
+        }
+
+        for (let i = 0; i < items.length; i++) {
+            if (items[i].classList.contains("current-item")) {
+                items[i].classList.remove("current-item");
+
+                if (i - 1 >= 0) {
+                    items[i - 1].classList.add("current-item");
+                    DomHelper.scrollPageTo(items[i - 1]);
+                }
+
+                break;
+            }
+        }
+    }
+
+    goToNextListItem() {
+        let currentItem = document.querySelector(".current-item");
+        let items = DomHelper.getVisibleElements(".items .item");
+        if (items.length === 0) {
+            return;
+        }
+
+        if (currentItem === null) {
+            items[0].classList.add("current-item");
+            return;
+        }
+
+        for (let i = 0; i < items.length; i++) {
+            if (items[i].classList.contains("current-item")) {
+                items[i].classList.remove("current-item");
+
+                if (i + 1 < items.length) {
+                    items[i + 1].classList.add("current-item");
+                    DomHelper.scrollPageTo(items[i + 1]);
+                }
+
+                break;
+            }
+        }
+    }
+
+    isListView() {
+        return document.querySelector(".items") !== null;
+    }
+}

+ 43 - 0
ui/static/js/request_builder.js

@@ -0,0 +1,43 @@
+class RequestBuilder {
+    constructor(url) {
+        this.callback = null;
+        this.url = url;
+        this.options = {
+            method: "POST",
+            cache: "no-cache",
+            credentials: "include",
+            body: null,
+            headers: new Headers({
+                "Content-Type": "application/json",
+                "X-Csrf-Token": this.getCsrfToken()
+            })
+        };
+    }
+
+    withBody(body) {
+        this.options.body = JSON.stringify(body);
+        return this;
+    }
+
+    withCallback(callback) {
+        this.callback = callback;
+        return this;
+    }
+
+    getCsrfToken() {
+        let element = document.querySelector("meta[name=X-CSRF-Token]");
+        if (element !== null) {
+            return element.getAttribute("value");
+        }
+
+        return "";
+    }
+
+    execute() {
+        fetch(new Request(this.url, this.options)).then((response) => {
+            if (this.callback) {
+                this.callback(response);
+            }
+        });
+    }
+}

+ 94 - 0
ui/static/js/touch_handler.js

@@ -0,0 +1,94 @@
+class TouchHandler {
+    constructor() {
+        this.reset();
+    }
+
+    reset() {
+        this.touch = {
+            start: {x: -1, y: -1},
+            move: {x: -1, y: -1},
+            element: null
+        };
+    }
+
+    calculateDistance() {
+        if (this.touch.start.x >= -1 && this.touch.move.x >= -1) {
+            let horizontalDistance = Math.abs(this.touch.move.x - this.touch.start.x);
+            let verticalDistance = Math.abs(this.touch.move.y - this.touch.start.y);
+
+            if (horizontalDistance > 30 && verticalDistance < 70) {
+                return this.touch.move.x - this.touch.start.x;
+            }
+        }
+
+        return 0;
+    }
+
+    findElement(element) {
+        if (element.classList.contains("touch-item")) {
+            return element;
+        }
+
+        return DomHelper.findParent(element, "touch-item");
+    }
+
+    onTouchStart(event) {
+        if (event.touches === undefined || event.touches.length !== 1) {
+            return;
+        }
+
+        this.reset();
+        this.touch.start.x = event.touches[0].clientX;
+        this.touch.start.y = event.touches[0].clientY;
+        this.touch.element = this.findElement(event.touches[0].target);
+    }
+
+    onTouchMove(event) {
+        if (event.touches === undefined || event.touches.length !== 1 || this.element === null) {
+            return;
+        }
+
+        this.touch.move.x = event.touches[0].clientX;
+        this.touch.move.y = event.touches[0].clientY;
+
+        let distance = this.calculateDistance();
+        let absDistance = Math.abs(distance);
+
+        if (absDistance > 0) {
+            let opacity = 1 - (absDistance > 75 ? 0.9 : absDistance / 75 * 0.9);
+            let tx = distance > 75 ? 75 : (distance < -75 ? -75 : distance);
+
+            this.touch.element.style.opacity = opacity;
+            this.touch.element.style.transform = "translateX(" + tx + "px)";
+        }
+    }
+
+    onTouchEnd(event) {
+        if (event.touches === undefined) {
+            return;
+        }
+
+        if (this.touch.element !== null) {
+            let distance = Math.abs(this.calculateDistance());
+
+            if (distance > 75) {
+                EntryHandler.toggleEntryStatus(this.touch.element);
+            }
+            this.touch.element.style.opacity = 1;
+            this.touch.element.style.transform = "none";
+        }
+
+        this.reset();
+    }
+
+    listen() {
+        let elements = document.querySelectorAll(".touch-item");
+
+        elements.forEach((element) => {
+            element.addEventListener("touchstart", (e) => this.onTouchStart(e), false);
+            element.addEventListener("touchmove", (e) => this.onTouchMove(e), false);
+            element.addEventListener("touchend", (e) => this.onTouchEnd(e), false);
+            element.addEventListener("touchcancel", () => this.reset(), false);
+        });
+    }
+}

+ 33 - 0
ui/static/js/unread_counter_handler.js

@@ -0,0 +1,33 @@
+class UnreadCounterHandler {
+    static decrement(n) {
+        this.updateValue((current) => {
+            return current - n;
+        });
+    }
+
+    static increment(n) {
+        this.updateValue((current) => {
+            return current + n;
+        });
+    }
+
+    static updateValue(callback) {
+        let counterElements = document.querySelectorAll("span.unread-counter");
+        counterElements.forEach((element) => {
+            let oldValue = parseInt(element.textContent, 10);
+            element.innerHTML = callback(oldValue);
+        });
+
+        if (window.location.href.endsWith('/unread')) {
+            let oldValue = parseInt(document.title.split('(')[1], 10);
+            let newValue = callback(oldValue);
+
+            document.title = document.title.replace(
+                /(.*?)\(\d+\)(.*?)/,
+                function (match, prefix, suffix, offset, string) {
+                    return prefix + '(' + newValue + ')' + suffix;
+                }
+            );
+        }
+    }
+}

+ 6 - 4
vendor/github.com/tdewolff/minify/.goreleaser.yml

@@ -2,15 +2,17 @@ builds:
     - binary: minify
       main: ./cmd/minify/
       ldflags: -s -w -X main.Version={{.Version}} -X main.Commit={{.Commit}} -X main.Date={{.Date}}
+      env:
+          - CGO_ENABLED=0
       goos:
-          - windows
           - linux
+          - windows
           - darwin
+          - freebsd
+          - netbsd
+          - openbsd
       goarch:
           - amd64
-          - 386
-          - arm
-          - arm64
 archive:
     format: tar.gz
     format_overrides:

+ 6 - 7
vendor/github.com/tdewolff/minify/README.md

@@ -58,16 +58,16 @@ The core functionality associates mimetypes with minification functions, allowin
 - [ ] General speed-up of all minifiers (use ASM for whitespace funcs)
 - [ ] Improve JS minifiers by shortening variables and proper semicolon omission
 - [ ] Speed-up SVG minifier, it is very slow
-- [ ] Proper parser error reporting and line number + column information
+- [x] Proper parser error reporting and line number + column information
 - [ ] Generation of source maps (uncertain, might slow down parsers too much if it cannot run separately nicely)
-- [ ] Look into compression of images, fonts and other web resources (into package `compress`?)
+- [ ] Look into compression of images, fonts and other web resources (into package `compress`)?
 - [ ] Create a cmd to pack webfiles (much like webpack), ie. merging CSS and JS files, inlining small external files, minification and gzipping. This would work on HTML files.
-- [ ] Create a package to format files, much like `gofmt` for Go files
+- [ ] Create a package to format files, much like `gofmt` for Go files?
 
 ## Prologue
-Minifiers or bindings to minifiers exist in almost all programming languages. Some implementations are merely using several regular-expressions to trim whitespace and comments (even though regex for parsing HTML/XML is ill-advised, for a good read see [Regular Expressions: Now You Have Two Problems](http://blog.codinghorror.com/regular-expressions-now-you-have-two-problems/)). Some implementations are much more profound, such as the [YUI Compressor](http://yui.github.io/yuicompressor/) and [Google Closure Compiler](https://github.com/google/closure-compiler) for JS. As most existing implementations either use Java or JavaScript and don't focus on performance, they are pretty slow. Additionally, loading the whole file into memory at once is bad for really large files (or impossible for streams).
+Minifiers or bindings to minifiers exist in almost all programming languages. Some implementations are merely using several regular-expressions to trim whitespace and comments (even though regex for parsing HTML/XML is ill-advised, for a good read see [Regular Expressions: Now You Have Two Problems](http://blog.codinghorror.com/regular-expressions-now-you-have-two-problems/)). Some implementations are much more profound, such as the [YUI Compressor](http://yui.github.io/yuicompressor/) and [Google Closure Compiler](https://github.com/google/closure-compiler) for JS. As most existing implementations either use JavaScript, use regexes, and don't focus on performance, they are pretty slow.
 
-This minifier proves to be that fast and extensive minifier that can handle HTML and any other filetype it may contain (CSS, JS, ...). It streams the input and output and can minify files concurrently.
+This minifier proves to be that fast and extensive minifier that can handle HTML and any other filetype it may contain (CSS, JS, ...). It is usually orders of magnitude faster than existing minifiers.
 
 ## Installation
 Run the following command
@@ -225,7 +225,7 @@ Options:
 
 The JS minifier is pretty basic. It removes comments, whitespace and line breaks whenever it can. It employs all the rules that [JSMin](http://www.crockford.com/javascript/jsmin.html) does too, but has additional improvements. For example the prefix-postfix bug is fixed.
 
-Common speeds of PHP and JS implementations are about 100-300kB/s (see [Uglify2](http://lisperator.net/uglifyjs/), [Adventures in PHP web asset minimization](https://www.happyassassin.net/2014/12/29/adventures-in-php-web-asset-minimization/)). This implementation or orders of magnitude faster, around ~50MB/s.
+Common speeds of PHP and JS implementations are about 100-300kB/s (see [Uglify2](http://lisperator.net/uglifyjs/), [Adventures in PHP web asset minimization](https://www.happyassassin.net/2014/12/29/adventures-in-php-web-asset-minimization/)). This implementation or orders of magnitude faster, around ~80MB/s.
 
 TODO:
 - shorten local variables / function parameters names
@@ -246,7 +246,6 @@ The SVG minifier uses these minifications:
 - strip SVG version
 - strip CDATA sections wherever possible
 - collapse tags with no content to a void tag
-- collapse empty container tags (`g`, `svg`, ...)
 - minify style tag and attributes with the CSS minifier
 - minify colors
 - shorten lengths and numbers and remove default `px` unit

+ 30 - 46
vendor/github.com/tdewolff/minify/cmd/minify/README.md

@@ -13,53 +13,37 @@ Run the following command
 
 and the `minify` command will be in your `$GOPATH/bin`.
 
-## Usage
+You can enable bash tab completion by using
+
+    source minify_bash_tab_completion
 
-	Usage: minify [options] [input]
-
-	Options:
-	  -a, --all
-	        Minify all files, including hidden files and files in hidden directories
-	  -l, --list
-	        List all accepted filetypes
-	  --match string
-	        Filename pattern matching using regular expressions, see https://github.com/google/re2/wiki/Syntax
-	  --mime string
-	        Mimetype (text/css, application/javascript, ...), optional for input filenames, has precedence over -type
-	  -o, --output string
-	        Output file or directory (must have trailing slash), leave blank to use stdout
-	  -r, --recursive
-	        Recursively minify directories
-	  --type string
-	        Filetype (css, html, js, ...), optional for input filenames
-	  -u, --update
-	        Update binary
-	  --url string
-	        URL of file to enable URL minification
-	  -v, --verbose
-	        Verbose
-	  -w, --watch
-	        Watch files and minify upon changes
-
-	  --css-decimals
-	        Number of decimals to preserve in numbers, -1 is all
-	  --html-keep-conditional-comments
-	  		Preserve all IE conditional comments
-	  --html-keep-default-attrvals
-	        Preserve default attribute values
-	  --html-keep-document-tags
-	        Preserve html, head and body tags
-	  --html-keep-end-tags
-	        Preserve all end tags
-	  --html-keep-whitespace
-	        Preserve whitespace characters but still collapse multiple into one
-	  --svg-decimals
-	        Number of decimals to preserve in numbers, -1 is all
-	  --xml-keep-whitespace
-	        Preserve whitespace characters but still collapse multiple into one
-
-	Input:
-	  Files or directories, leave blank to use stdin
+## Usage
+    Usage: minify [options] [input]
+
+    Options:
+      -a, --all                              Minify all files, including hidden files and files in hidden directories
+          --css-decimals int                 Number of decimals to preserve in numbers, -1 is all (default -1)
+      -h, --help                             Show usage
+          --html-keep-conditional-comments   Preserve all IE conditional comments
+          --html-keep-default-attrvals       Preserve default attribute values
+          --html-keep-document-tags          Preserve html, head and body tags
+          --html-keep-end-tags               Preserve all end tags
+          --html-keep-whitespace             Preserve whitespace characters but still collapse multiple into one
+      -l, --list                             List all accepted filetypes
+          --match string                     Filename pattern matching using regular expressions
+          --mime string                      Mimetype (eg. text/css), optional for input filenames, has precedence over -type
+      -o, --output string                    Output file or directory (must have trailing slash), leave blank to use stdout
+      -r, --recursive                        Recursively minify directories
+          --svg-decimals int                 Number of decimals to preserve in numbers, -1 is all (default -1)
+          --type string                      Filetype (eg. css), optional for input filenames
+          --url string                       URL of file to enable URL minification
+      -v, --verbose                          Verbose
+          --version                          Version
+      -w, --watch                            Watch files and minify upon changes
+          --xml-keep-whitespace              Preserve whitespace characters but still collapse multiple into one
+
+    Input:
+      Files or directories, leave blank to use stdin
 
 ### Types
 

+ 101 - 99
vendor/github.com/tdewolff/minify/cmd/minify/main.go

@@ -15,7 +15,6 @@ import (
 	"runtime"
 	"sort"
 	"strings"
-	"sync/atomic"
 	"time"
 
 	humanize "github.com/dustin/go-humanize"
@@ -45,6 +44,7 @@ var filetypeMime = map[string]string{
 }
 
 var (
+	help      bool
 	hidden    bool
 	list      bool
 	m         *min.M
@@ -55,7 +55,7 @@ var (
 	watch     bool
 )
 
-type task struct {
+type Task struct {
 	srcs   []string
 	srcDir string
 	dst    string
@@ -80,15 +80,18 @@ func main() {
 	svgMinifier := &svg.Minifier{}
 	xmlMinifier := &xml.Minifier{}
 
+	flag := flag.NewFlagSet("minify", flag.ContinueOnError)
 	flag.Usage = func() {
 		fmt.Fprintf(os.Stderr, "Usage: %s [options] [input]\n\nOptions:\n", os.Args[0])
 		flag.PrintDefaults()
 		fmt.Fprintf(os.Stderr, "\nInput:\n  Files or directories, leave blank to use stdin\n")
 	}
+
+	flag.BoolVarP(&help, "help", "h", false, "Show usage")
 	flag.StringVarP(&output, "output", "o", "", "Output file or directory (must have trailing slash), leave blank to use stdout")
-	flag.StringVar(&mimetype, "mime", "", "Mimetype (text/css, application/javascript, ...), optional for input filenames, has precedence over -type")
-	flag.StringVar(&filetype, "type", "", "Filetype (css, html, js, ...), optional for input filenames")
-	flag.StringVar(&match, "match", "", "Filename pattern matching using regular expressions, see https://github.com/google/re2/wiki/Syntax")
+	flag.StringVar(&mimetype, "mime", "", "Mimetype (eg. text/css), optional for input filenames, has precedence over -type")
+	flag.StringVar(&filetype, "type", "", "Filetype (eg. css), optional for input filenames")
+	flag.StringVar(&match, "match", "", "Filename pattern matching using regular expressions")
 	flag.BoolVarP(&recursive, "recursive", "r", false, "Recursively minify directories")
 	flag.BoolVarP(&hidden, "all", "a", false, "Minify all files, including hidden files and files in hidden directories")
 	flag.BoolVarP(&list, "list", "l", false, "List all accepted filetypes")
@@ -105,7 +108,11 @@ func main() {
 	flag.BoolVar(&htmlMinifier.KeepWhitespace, "html-keep-whitespace", false, "Preserve whitespace characters but still collapse multiple into one")
 	flag.IntVar(&svgMinifier.Decimals, "svg-decimals", -1, "Number of decimals to preserve in numbers, -1 is all")
 	flag.BoolVar(&xmlMinifier.KeepWhitespace, "xml-keep-whitespace", false, "Preserve whitespace characters but still collapse multiple into one")
-	flag.Parse()
+	if err := flag.Parse(os.Args[1:]); err != nil {
+		fmt.Printf("Error: %v\n\n", err)
+		flag.Usage()
+		os.Exit(2)
+	}
 	rawInputs := flag.Args()
 
 	Error = log.New(os.Stderr, "ERROR: ", 0)
@@ -115,13 +122,18 @@ func main() {
 		Info = log.New(ioutil.Discard, "INFO: ", 0)
 	}
 
+	if help {
+		flag.Usage()
+		os.Exit(0)
+	}
+
 	if version {
 		if Version == "devel" {
 			fmt.Printf("minify version devel+%.7s %s\n", Commit, Date)
 		} else {
 			fmt.Printf("minify version %s\n", Version)
 		}
-		return
+		os.Exit(0)
 	}
 
 	if list {
@@ -133,7 +145,7 @@ func main() {
 		for _, k := range keys {
 			fmt.Println(k + "\t" + filetypeMime[k])
 		}
-		return
+		os.Exit(0)
 	}
 
 	useStdin := len(rawInputs) == 0
@@ -148,7 +160,11 @@ func main() {
 	}
 
 	if watch && (useStdin || output == "") {
-		Error.Fatalln("watch doesn't work with stdin or stdout")
+		Error.Fatalln("watch doesn't work on stdin and stdout, specify input and output")
+	}
+
+	if recursive && (useStdin || output == "") {
+		Error.Fatalln("recursive minification doesn't work on stdin and stdout, specify input and output")
 	}
 
 	////////////////
@@ -174,7 +190,7 @@ func main() {
 	}
 
 	if len(tasks) == 0 {
-		tasks = append(tasks, task{[]string{""}, "", output}) // stdin
+		tasks = append(tasks, Task{[]string{""}, "", output}) // stdin
 	}
 
 	m = min.New()
@@ -191,47 +207,33 @@ func main() {
 
 	start := time.Now()
 
-	var fails int32
-	if verbose || len(tasks) == 1 {
-		for _, t := range tasks {
-			if ok := minify(mimetype, t); !ok {
-				fails++
-			}
-		}
-	} else {
-		numWorkers := 4
+	chanTasks := make(chan Task, 100)
+	chanFails := make(chan int, 100)
+
+	numWorkers := 1
+	if !verbose && len(tasks) > 1 {
+		numWorkers = 4
 		if n := runtime.NumCPU(); n > numWorkers {
 			numWorkers = n
 		}
+	}
 
-		sem := make(chan struct{}, numWorkers)
-		for _, t := range tasks {
-			sem <- struct{}{}
-			go func(t task) {
-				defer func() {
-					<-sem
-				}()
-				if ok := minify(mimetype, t); !ok {
-					atomic.AddInt32(&fails, 1)
-				}
-			}(t)
-		}
+	for n := 0; n < numWorkers; n++ {
+		go minifyWorker(mimetype, chanTasks, chanFails)
+	}
 
-		// wait for all jobs to be done
-		for i := 0; i < cap(sem); i++ {
-			sem <- struct{}{}
-		}
+	for _, task := range tasks {
+		chanTasks <- task
 	}
 
 	if watch {
-		var watcher *RecursiveWatcher
-		watcher, err = NewRecursiveWatcher(recursive)
+		watcher, err := NewRecursiveWatcher(recursive)
 		if err != nil {
 			Error.Fatalln(err)
 		}
 		defer watcher.Close()
 
-		var watcherTasks = make(map[string]task, len(rawInputs))
+		watcherTasks := make(map[string]Task, len(rawInputs))
 		for _, task := range tasks {
 			for _, src := range task.srcs {
 				watcherTasks[src] = task
@@ -248,6 +250,7 @@ func main() {
 			select {
 			case <-c:
 				watcher.Close()
+				fmt.Printf("\n")
 			case file, ok := <-changes:
 				if !ok {
 					changes = nil
@@ -260,10 +263,10 @@ func main() {
 					continue
 				}
 
-				var t task
+				var t Task
 				if t, ok = watcherTasks[file]; ok {
 					if !verbose {
-						fmt.Fprintln(os.Stderr, file, "changed")
+						Info.Println(file, "changed")
 					}
 					for _, src := range t.srcs {
 						if src == t.dst {
@@ -271,21 +274,35 @@ func main() {
 							break
 						}
 					}
-					if ok := minify(mimetype, t); !ok {
-						fails++
-					}
+					chanTasks <- t
 				}
 			}
 		}
 	}
 
+	fails := 0
+	close(chanTasks)
+	for n := 0; n < numWorkers; n++ {
+		fails += <-chanFails
+	}
+
 	if verbose {
 		Info.Println(time.Since(start), "total")
 	}
-
 	if fails > 0 {
 		os.Exit(1)
 	}
+	os.Exit(0)
+}
+
+func minifyWorker(mimetype string, chanTasks <-chan Task, chanFails chan<- int) {
+	fails := 0
+	for task := range chanTasks {
+		if ok := minify(mimetype, task); !ok {
+			fails++
+		}
+	}
+	chanFails <- fails
 }
 
 func getMimetype(mimetype, filetype string, useStdin bool) string {
@@ -344,9 +361,9 @@ func validDir(info os.FileInfo) bool {
 	return info.Mode().IsDir() && len(info.Name()) > 0 && (hidden || info.Name()[0] != '.')
 }
 
-func expandInputs(inputs []string, dirDst bool) ([]task, bool) {
+func expandInputs(inputs []string, dirDst bool) ([]Task, bool) {
 	ok := true
-	tasks := []task{}
+	tasks := []Task{}
 	for _, input := range inputs {
 		input = sanitizePath(input)
 		info, err := os.Stat(input)
@@ -357,7 +374,7 @@ func expandInputs(inputs []string, dirDst bool) ([]task, bool) {
 		}
 
 		if info.Mode().IsRegular() {
-			tasks = append(tasks, task{[]string{filepath.ToSlash(input)}, "", ""})
+			tasks = append(tasks, Task{[]string{filepath.ToSlash(input)}, "", ""})
 		} else if info.Mode().IsDir() {
 			expandDir(input, &tasks, &ok)
 		} else {
@@ -391,7 +408,7 @@ func expandInputs(inputs []string, dirDst bool) ([]task, bool) {
 	return tasks, ok
 }
 
-func expandDir(input string, tasks *[]task, ok *bool) {
+func expandDir(input string, tasks *[]Task, ok *bool) {
 	if !recursive {
 		if verbose {
 			Info.Println("expanding directory", input)
@@ -404,7 +421,7 @@ func expandDir(input string, tasks *[]task, ok *bool) {
 		}
 		for _, info := range infos {
 			if validFile(info) {
-				*tasks = append(*tasks, task{[]string{path.Join(input, info.Name())}, input, ""})
+				*tasks = append(*tasks, Task{[]string{path.Join(input, info.Name())}, input, ""})
 			}
 		}
 	} else {
@@ -417,7 +434,7 @@ func expandDir(input string, tasks *[]task, ok *bool) {
 				return err
 			}
 			if validFile(info) {
-				*tasks = append(*tasks, task{[]string{filepath.ToSlash(path)}, input, ""})
+				*tasks = append(*tasks, Task{[]string{filepath.ToSlash(path)}, input, ""})
 			} else if info.Mode().IsDir() && !validDir(info) && info.Name() != "." && info.Name() != ".." { // check for IsDir, so we don't skip the rest of the directory when we have an invalid file
 				return filepath.SkipDir
 			}
@@ -430,7 +447,7 @@ func expandDir(input string, tasks *[]task, ok *bool) {
 	}
 }
 
-func expandOutputs(output string, tasks *[]task) bool {
+func expandOutputs(output string, tasks *[]Task) bool {
 	if verbose {
 		if output == "" {
 			Info.Println("minify to stdout")
@@ -459,7 +476,7 @@ func expandOutputs(output string, tasks *[]task) bool {
 	return ok
 }
 
-func getOutputFilename(output string, t task) (string, error) {
+func getOutputFilename(output string, t Task) (string, error) {
 	if len(output) > 0 && output[len(output)-1] == '/' {
 		rel, err := filepath.Rel(t.srcDir, t.srcs[0])
 		if err != nil {
@@ -470,47 +487,44 @@ func getOutputFilename(output string, t task) (string, error) {
 	return output, nil
 }
 
-func openInputFile(input string) (*os.File, bool) {
+func openInputFile(input string) (io.ReadCloser, error) {
 	var r *os.File
 	if input == "" {
 		r = os.Stdin
 	} else {
 		err := try.Do(func(attempt int) (bool, error) {
-			var err error
-			r, err = os.Open(input)
-			return attempt < 5, err
+			var ferr error
+			r, ferr = os.Open(input)
+			return attempt < 5, ferr
 		})
 		if err != nil {
-			Error.Println(err)
-			return nil, false
+			return nil, err
 		}
 	}
-	return r, true
+	return r, nil
 }
 
-func openOutputFile(output string) (*os.File, bool) {
+func openOutputFile(output string) (*os.File, error) {
 	var w *os.File
 	if output == "" {
 		w = os.Stdout
 	} else {
 		if err := os.MkdirAll(path.Dir(output), 0777); err != nil {
-			Error.Println(err)
-			return nil, false
+			return nil, err
 		}
 		err := try.Do(func(attempt int) (bool, error) {
-			var err error
-			w, err = os.OpenFile(output, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666)
-			return attempt < 5, err
+			var ferr error
+			w, ferr = os.OpenFile(output, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666)
+			return attempt < 5, ferr
 		})
 		if err != nil {
-			Error.Println(err)
-			return nil, false
+			return nil, err
 		}
 	}
-	return w, true
+	return w, nil
 }
 
-func minify(mimetype string, t task) bool {
+func minify(mimetype string, t Task) bool {
 	if mimetype == "" {
 		for _, src := range t.srcs {
 			if len(path.Ext(src)) > 0 {
@@ -545,8 +559,8 @@ func minify(mimetype string, t task) bool {
 			if t.srcs[i] == t.dst {
 				t.srcs[i] += ".bak"
 				err := try.Do(func(attempt int) (bool, error) {
-					err := os.Rename(t.dst, t.srcs[i])
-					return attempt < 5, err
+					ferr := os.Rename(t.dst, t.srcs[i])
+					return attempt < 5, ferr
 				})
 				if err != nil {
 					Error.Println(err)
@@ -557,42 +571,32 @@ func minify(mimetype string, t task) bool {
 		}
 	}
 
-	frs := make([]io.Reader, len(t.srcs))
-	for i, src := range t.srcs {
-		fr, ok := openInputFile(src)
-		if !ok {
-			for _, fr := range frs {
-				fr.(io.ReadCloser).Close()
-			}
-			return false
-		}
-		if i > 0 && mimetype == filetypeMime["js"] {
-			// prepend newline when concatenating JS files
-			frs[i] = NewPrependReader(fr, []byte("\n"))
-		} else {
-			frs[i] = fr
-		}
+	fr, err := NewConcatFileReader(t.srcs, openInputFile)
+	if err != nil {
+		Error.Println(err)
+		return false
 	}
-	r := &countingReader{io.MultiReader(frs...), 0}
+	if mimetype == filetypeMime["js"] {
+		fr.SetSeparator([]byte("\n"))
+	}
+	r := NewCountingReader(fr)
 
-	fw, ok := openOutputFile(t.dst)
-	if !ok {
-		for _, fr := range frs {
-			fr.(io.ReadCloser).Close()
-		}
+	fw, err := openOutputFile(t.dst)
+	if err != nil {
+		Error.Println(err)
+		fr.Close()
 		return false
 	}
 	var w *countingWriter
 	if fw == os.Stdout {
-		w = &countingWriter{fw, 0}
+		w = NewCountingWriter(fw)
 	} else {
-		w = &countingWriter{bufio.NewWriter(fw), 0}
+		w = NewCountingWriter(bufio.NewWriter(fw))
 	}
 
 	success := true
 	startTime := time.Now()
-	err := m.Minify(mimetype, w, r)
-	if err != nil {
+	if err = m.Minify(mimetype, w, r); err != nil {
 		Error.Println("cannot minify "+srcName+":", err)
 		success = false
 	}
@@ -615,9 +619,7 @@ func minify(mimetype string, t task) bool {
 		}
 	}
 
-	for _, fr := range frs {
-		fr.(io.ReadCloser).Close()
-	}
+	fr.Close()
 	if bw, ok := w.Writer.(*bufio.Writer); ok {
 		bw.Flush()
 	}

+ 29 - 0
vendor/github.com/tdewolff/minify/cmd/minify/minify_bash_tab_completion

@@ -0,0 +1,29 @@
+#!/bin/bash
+
+_minify_complete()
+{
+    local cur_word prev_word flags mimes types
+
+    cur_word="${COMP_WORDS[COMP_CWORD]}"
+    prev_word="${COMP_WORDS[COMP_CWORD-1]}"
+    flags="-a --all -l --list --match --mime -o --output -r --recursive --type --url -v --verbose --version -w --watch --css-decimals --html-keep-conditional-comments --html-keep-default-attrvals --html-keep-document-tags --html-keep-end-tags --html-keep-whitespace --svg-decimals --xml-keep-whitespace"
+    mimes="text/css text/html text/javascript application/json image/svg+xml text/xml"
+    types="css html js json svg xml"
+
+    if [[ ${cur_word} == -* ]] ; then
+        COMPREPLY=( $(compgen -W "${flags}" -- ${cur_word}) )
+    elif [[ ${prev_word} =~ ^--mime$ ]] ; then
+        COMPREPLY=( $(compgen -W "${mimes}" -- ${cur_word}) )
+    elif [[ ${prev_word} =~ ^--type$ ]] ; then
+        COMPREPLY=( $(compgen -W "${types}" -- ${cur_word}) )
+    elif [[ ${prev_word} =~ ^--(match|url|css-decimals|svg-decimals)$ ]] ; then
+        compopt +o default
+        COMPREPLY=()
+    else
+        compopt -o default
+        COMPREPLY=()
+    fi
+    return 0
+}
+
+complete -F _minify_complete minify

+ 86 - 14
vendor/github.com/tdewolff/minify/cmd/minify/util.go

@@ -1,12 +1,18 @@
 package main
 
-import "io"
+import (
+	"io"
+)
 
 type countingReader struct {
 	io.Reader
 	N int
 }
 
+func NewCountingReader(r io.Reader) *countingReader {
+	return &countingReader{r, 0}
+}
+
 func (r *countingReader) Read(p []byte) (int, error) {
 	n, err := r.Reader.Read(p)
 	r.N += n
@@ -18,29 +24,95 @@ type countingWriter struct {
 	N int
 }
 
+func NewCountingWriter(w io.Writer) *countingWriter {
+	return &countingWriter{w, 0}
+}
+
 func (w *countingWriter) Write(p []byte) (int, error) {
 	n, err := w.Writer.Write(p)
 	w.N += n
 	return n, err
 }
 
-type prependReader struct {
-	io.ReadCloser
-	prepend []byte
+type eofReader struct{}
+
+func (r eofReader) Read(p []byte) (int, error) {
+	return 0, io.EOF
+}
+
+func (r eofReader) Close() error {
+	return nil
+}
+
+type concatFileReader struct {
+	filenames []string
+	opener    func(string) (io.ReadCloser, error)
+	sep       []byte
+
+	cur     io.ReadCloser
+	sepLeft int
+}
+
+// NewConcatFileReader reads from a list of filenames, and lazily loads files as it needs it.
+// It is a reader that reads a concatenation of those files separated by the separator.
+// You must call Close to close the last file in the list.
+func NewConcatFileReader(filenames []string, opener func(string) (io.ReadCloser, error)) (*concatFileReader, error) {
+	var cur io.ReadCloser
+	if len(filenames) > 0 {
+		var filename string
+		filename, filenames = filenames[0], filenames[1:]
+
+		var err error
+		if cur, err = opener(filename); err != nil {
+			return nil, err
+		}
+	} else {
+		cur = eofReader{}
+	}
+	return &concatFileReader{filenames, opener, nil, cur, 0}, nil
 }
 
-func NewPrependReader(r io.ReadCloser, prepend []byte) *prependReader {
-	return &prependReader{r, prepend}
+func (r *concatFileReader) SetSeparator(sep []byte) {
+	r.sep = sep
 }
 
-func (r *prependReader) Read(p []byte) (int, error) {
-	if r.prepend != nil {
-		n := copy(p, r.prepend)
-		if n != len(r.prepend) {
-			return n, io.ErrShortBuffer
+func (r *concatFileReader) Read(p []byte) (int, error) {
+	m := r.writeSep(p)
+	n, err := r.cur.Read(p[m:])
+	n += m
+
+	// current reader is finished, load in the new reader
+	if err == io.EOF && len(r.filenames) > 0 {
+		if err := r.cur.Close(); err != nil {
+			return n, err
+		}
+
+		var filename string
+		filename, r.filenames = r.filenames[0], r.filenames[1:]
+		if r.cur, err = r.opener(filename); err != nil {
+			return n, err
+		}
+		r.sepLeft = len(r.sep)
+
+		// if previous read returned (0, io.EOF), read from the new reader
+		if n == 0 {
+			return r.Read(p)
+		} else {
+			n += r.writeSep(p[n:])
 		}
-		r.prepend = nil
-		return n, nil
 	}
-	return r.ReadCloser.Read(p)
+	return n, err
+}
+
+func (r *concatFileReader) writeSep(p []byte) int {
+	m := 0
+	if r.sepLeft > 0 {
+		m = copy(p, r.sep[len(r.sep)-r.sepLeft:])
+		r.sepLeft -= m
+	}
+	return m
+}
+
+func (r *concatFileReader) Close() error {
+	return r.cur.Close()
 }

+ 152 - 0
vendor/github.com/tdewolff/minify/cmd/minify/util_test.go

@@ -0,0 +1,152 @@
+package main
+
+import (
+	"bytes"
+	"io"
+	"io/ioutil"
+	"testing"
+
+	"github.com/tdewolff/test"
+)
+
+func testOpener(filename string) (io.ReadCloser, error) {
+	if filename == "err" {
+		return nil, test.ErrPlain
+	} else if filename == "empty" {
+		return ioutil.NopCloser(test.NewEmptyReader()), nil
+	}
+	return ioutil.NopCloser(bytes.NewReader([]byte(filename))), nil
+}
+
+func TestConcat(t *testing.T) {
+	r, err := NewConcatFileReader([]string{"test", "test"}, testOpener)
+	test.T(t, err, nil)
+
+	buf, err := ioutil.ReadAll(r)
+	test.T(t, err, nil)
+	test.Bytes(t, buf, []byte("testtest"))
+
+	n, err := r.Read(buf)
+	test.T(t, n, 0)
+	test.T(t, err, io.EOF)
+}
+
+func TestConcatErr(t *testing.T) {
+	r, err := NewConcatFileReader([]string{"err"}, testOpener)
+	test.T(t, err, test.ErrPlain)
+
+	r, err = NewConcatFileReader([]string{"test", "err"}, testOpener)
+	test.T(t, err, nil)
+
+	buf := make([]byte, 10)
+	n, err := r.Read(buf)
+	test.T(t, n, 4)
+	test.T(t, err, nil)
+	test.Bytes(t, buf[:n], []byte("test"))
+
+	n, err = r.Read(buf)
+	test.T(t, n, 0)
+	test.T(t, err, test.ErrPlain)
+}
+
+func TestConcatSep(t *testing.T) {
+	r, err := NewConcatFileReader([]string{"test", "test"}, testOpener)
+	test.T(t, err, nil)
+	r.SetSeparator([]byte("_"))
+
+	buf := make([]byte, 10)
+	n, err := r.Read(buf)
+	test.T(t, n, 4)
+	test.T(t, err, nil)
+	test.Bytes(t, buf[:n], []byte("test"))
+
+	n, err = r.Read(buf[n:])
+	test.T(t, n, 5)
+	test.T(t, err, nil)
+	test.Bytes(t, buf[:4+n], []byte("test_test"))
+}
+
+func TestConcatSepShort1(t *testing.T) {
+	r, err := NewConcatFileReader([]string{"test", "test"}, testOpener)
+	test.T(t, err, nil)
+	r.SetSeparator([]byte("_"))
+
+	// insufficient room for separator
+	buf := make([]byte, 4)
+	n, err := r.Read(buf)
+	test.T(t, n, 4)
+	test.T(t, err, nil)
+	test.Bytes(t, buf, []byte("test"))
+
+	n, err = r.Read(buf[4:])
+	test.T(t, n, 0)
+	test.T(t, err, nil)
+}
+
+func TestConcatSepShort2(t *testing.T) {
+	r, err := NewConcatFileReader([]string{"test", "test"}, testOpener)
+	test.T(t, err, nil)
+	r.SetSeparator([]byte("_"))
+
+	// insufficient room after separator
+	buf := make([]byte, 5)
+	_, _ = r.Read(buf)
+
+	n, err := r.Read(buf[4:])
+	test.T(t, n, 1)
+	test.T(t, err, nil)
+	test.Bytes(t, buf, []byte("test_"))
+}
+
+func TestConcatSepShort3(t *testing.T) {
+	r, err := NewConcatFileReader([]string{"test", "test"}, testOpener)
+	test.T(t, err, nil)
+	r.SetSeparator([]byte("_"))
+
+	// insufficient room after separator
+	buf := make([]byte, 6)
+	_, _ = r.Read(buf)
+
+	n, err := r.Read(buf[4:])
+	test.T(t, n, 2)
+	test.T(t, err, nil)
+	test.Bytes(t, buf, []byte("test_t"))
+}
+
+func TestConcatSepShort4(t *testing.T) {
+	r, err := NewConcatFileReader([]string{"test", "test"}, testOpener)
+	test.T(t, err, nil)
+	r.SetSeparator([]byte("xx"))
+
+	// insufficient room after separator
+	buf := make([]byte, 5)
+	_, _ = r.Read(buf)
+
+	n, err := r.Read(buf[4:])
+	test.T(t, n, 1)
+	test.T(t, err, nil)
+	test.Bytes(t, buf, []byte("testx"))
+
+	n, err = r.Read(buf[5:])
+	test.T(t, n, 0)
+	test.T(t, err, nil)
+
+	buf2 := make([]byte, 5)
+	n, err = r.Read(buf2)
+	test.T(t, n, 5)
+	test.T(t, err, nil)
+	test.Bytes(t, buf2, []byte("xtest"))
+}
+
+func TestConcatSepEmpty(t *testing.T) {
+	r, err := NewConcatFileReader([]string{"empty", "empty"}, testOpener)
+	test.T(t, err, nil)
+	r.SetSeparator([]byte("_"))
+
+	// insufficient room after separator
+	buf := make([]byte, 1)
+	n, err := r.Read(buf)
+	test.T(t, n, 1)
+	test.T(t, err, io.EOF)
+	test.Bytes(t, buf, []byte("_"))
+}

+ 1 - 1
vendor/github.com/tdewolff/minify/cmd/minify/watch.go

@@ -87,7 +87,7 @@ func (rw *RecursiveWatcher) Run() chan string {
 							}
 						}
 					} else if validFile(info) {
-						if event.Op&fsnotify.Create == fsnotify.Create || event.Op&fsnotify.Write == fsnotify.Write {
+						if event.Op&fsnotify.Write == fsnotify.Write {
 							files <- event.Name
 						}
 					}

+ 137 - 14
vendor/github.com/tdewolff/minify/common.go

@@ -12,8 +12,8 @@ import (
 // Epsilon is the closest number to zero that is not considered to be zero.
 var Epsilon = 0.00001
 
-// ContentType minifies a given mediatype by removing all whitespace.
-func ContentType(b []byte) []byte {
+// Mediatype minifies a given mediatype by removing all whitespace.
+func Mediatype(b []byte) []byte {
 	j := 0
 	start := 0
 	inString := false
@@ -79,6 +79,107 @@ func DataURI(m *M, dataURI []byte) []byte {
 const MaxInt = int(^uint(0) >> 1)
 const MinInt = -MaxInt - 1
 
+// Decimal minifies a given byte slice containing a number (see parse.Number) and removes superfluous characters.
+// It does not parse or output exponents.
+func Decimal(num []byte, prec int) []byte {
+	// omit first + and register mantissa start and end, whether it's negative and the exponent
+	neg := false
+	start := 0
+	dot := -1
+	end := len(num)
+	if 0 < end && (num[0] == '+' || num[0] == '-') {
+		if num[0] == '-' {
+			neg = true
+		}
+		start++
+	}
+	for i, c := range num[start:] {
+		if c == '.' {
+			dot = start + i
+			break
+		}
+	}
+	if dot == -1 {
+		dot = end
+	}
+
+	// trim leading zeros but leave at least one digit
+	for start < end-1 && num[start] == '0' {
+		start++
+	}
+	// trim trailing zeros
+	i := end - 1
+	for ; i > dot; i-- {
+		if num[i] != '0' {
+			end = i + 1
+			break
+		}
+	}
+	if i == dot {
+		end = dot
+		if start == end {
+			num[start] = '0'
+			return num[start : start+1]
+		}
+	} else if start == end-1 && num[start] == '0' {
+		return num[start:end]
+	}
+
+	// apply precision
+	if prec > -1 && dot+1+prec < end {
+		end = dot + 1 + prec
+		inc := num[end] >= '5'
+		if inc || num[end-1] == '0' {
+			for i := end - 1; i > start; i-- {
+				if i == dot {
+					end--
+				} else if inc {
+					if num[i] == '9' {
+						if i > dot {
+							end--
+						} else {
+							num[i] = '0'
+						}
+					} else {
+						num[i]++
+						inc = false
+						break
+					}
+				} else if i > dot && num[i] == '0' {
+					end--
+				}
+			}
+		}
+		if dot == start && end == start+1 {
+			if inc {
+				num[start] = '1'
+			} else {
+				num[start] = '0'
+			}
+		} else {
+			if dot+1 == end {
+				end--
+			}
+			if inc {
+				if num[start] == '9' {
+					num[start] = '0'
+					copy(num[start+1:], num[start:end])
+					end++
+					num[start] = '1'
+				} else {
+					num[start]++
+				}
+			}
+		}
+	}
+
+	if neg {
+		start--
+		num[start] = '-'
+	}
+	return num[start:end]
+}
+
 // Number minifies a given byte slice containing a number (see parse.Number) and removes superfluous characters.
 func Number(num []byte, prec int) []byte {
 	// omit first + and register mantissa start and end, whether it's negative and the exponent
@@ -311,24 +412,46 @@ func Number(num []byte, prec int) []byte {
 		}
 	} else {
 		// case 3
-		if dot < end {
-			if dot == start {
-				copy(num[start:], num[end-n:end])
-				end = start + n
-			} else {
-				copy(num[dot:], num[dot+1:end])
-				end--
+
+		// find new end, considering moving numbers to the front, removing the dot and increasing the length of the exponent
+		newEnd := end
+		if dot == start {
+			newEnd = start + n
+		} else {
+			newEnd--
+		}
+		newEnd += 2 + lenIntExp
+
+		exp := intExp
+		lenExp := lenIntExp
+		if newEnd < len(num) {
+			// it saves space to convert the decimal to an integer and decrease the exponent
+			if dot < end {
+				if dot == start {
+					copy(num[start:], num[end-n:end])
+					end = start + n
+				} else {
+					copy(num[dot:], num[dot+1:end])
+					end--
+				}
+			}
+		} else {
+			// it does not save space and will panic, so we revert to the original representation
+			exp = origExp
+			lenExp = 1
+			if origExp <= -10 || origExp >= 10 {
+				lenExp = strconv.LenInt(int64(origExp))
 			}
 		}
 		num[end] = 'e'
 		num[end+1] = '-'
 		end += 2
-		intExp = -intExp
-		for i := end + lenIntExp - 1; i >= end; i-- {
-			num[i] = byte(intExp%10) + '0'
-			intExp /= 10
+		exp = -exp
+		for i := end + lenExp - 1; i >= end; i-- {
+			num[i] = byte(exp%10) + '0'
+			exp /= 10
 		}
-		end += lenIntExp
+		end += lenExp
 	}
 
 	if neg {

+ 103 - 14
vendor/github.com/tdewolff/minify/common_test.go

@@ -12,20 +12,20 @@ import (
 	"github.com/tdewolff/test"
 )
 
-func TestContentType(t *testing.T) {
-	contentTypeTests := []struct {
-		contentType string
-		expected    string
+func TestMediatype(t *testing.T) {
+	mediatypeTests := []struct {
+		mediatype string
+		expected  string
 	}{
 		{"text/html", "text/html"},
 		{"text/html; charset=UTF-8", "text/html;charset=utf-8"},
 		{"text/html; charset=UTF-8 ; param = \" ; \"", "text/html;charset=utf-8;param=\" ; \""},
 		{"text/html, text/css", "text/html,text/css"},
 	}
-	for _, tt := range contentTypeTests {
-		t.Run(tt.contentType, func(t *testing.T) {
-			contentType := ContentType([]byte(tt.contentType))
-			test.Minify(t, tt.contentType, nil, string(contentType), tt.expected)
+	for _, tt := range mediatypeTests {
+		t.Run(tt.mediatype, func(t *testing.T) {
+			mediatype := Mediatype([]byte(tt.mediatype))
+			test.Minify(t, tt.mediatype, nil, string(mediatype), tt.expected)
 		})
 	}
 }
@@ -62,6 +62,72 @@ func TestDataURI(t *testing.T) {
 	}
 }
 
+func TestDecimal(t *testing.T) {
+	numberTests := []struct {
+		number   string
+		expected string
+	}{
+		{"0", "0"},
+		{".0", "0"},
+		{"1.0", "1"},
+		{"0.1", ".1"},
+		{"+1", "1"},
+		{"-1", "-1"},
+		{"-0.1", "-.1"},
+		{"10", "10"},
+		{"100", "100"},
+		{"1000", "1000"},
+		{"0.001", ".001"},
+		{"0.0001", ".0001"},
+		{"0.252", ".252"},
+		{"1.252", "1.252"},
+		{"-1.252", "-1.252"},
+		{"0.075", ".075"},
+		{"789012345678901234567890123456789e9234567890123456789", "789012345678901234567890123456789e9234567890123456789"},
+		{".000100009", ".000100009"},
+		{".0001000009", ".0001000009"},
+		{".0001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009", ".0001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009"},
+		{"E\x1f", "E\x1f"}, // fuzz
+	}
+	for _, tt := range numberTests {
+		t.Run(tt.number, func(t *testing.T) {
+			number := Decimal([]byte(tt.number), -1)
+			test.Minify(t, tt.number, nil, string(number), tt.expected)
+		})
+	}
+}
+
+func TestDecimalTruncate(t *testing.T) {
+	numberTests := []struct {
+		number   string
+		truncate int
+		expected string
+	}{
+		{"0.1", 1, ".1"},
+		{"0.0001", 1, "0"},
+		{"0.111", 1, ".1"},
+		{"0.111", 0, "0"},
+		{"0.075", 1, ".1"},
+		{"0.025", 1, "0"},
+		{"9.99", 1, "10"},
+		{"8.88", 1, "8.9"},
+		{"8.88", 0, "9"},
+		{"8.00", 0, "8"},
+		{".88", 0, "1"},
+		{"1.234", 1, "1.2"},
+		{"33.33", 0, "33"},
+		{"29.666", 0, "30"},
+		{"1.51", 1, "1.5"},
+		{"1.01", 1, "1"},
+	}
+	for _, tt := range numberTests {
+		t.Run(tt.number, func(t *testing.T) {
+			number := Decimal([]byte(tt.number), tt.truncate)
+			test.Minify(t, tt.number, nil, string(number), tt.expected, "truncate to", tt.truncate)
+		})
+	}
+}
+
 func TestNumber(t *testing.T) {
 	numberTests := []struct {
 		number   string
@@ -82,6 +148,8 @@ func TestNumber(t *testing.T) {
 		{"100e1", "1e3"},
 		{"1.1e+1", "11"},
 		{"1.1e6", "11e5"},
+		{"1.1e", "1.1e"},   // broken number, don't parse
+		{"1.1e+", "1.1e+"}, // broken number, don't parse
 		{"0.252", ".252"},
 		{"1.252", "1.252"},
 		{"-1.252", "-1.252"},
@@ -90,6 +158,7 @@ func TestNumber(t *testing.T) {
 		{".000100009", "100009e-9"},
 		{".0001000009", ".0001000009"},
 		{".0001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009", ".0001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009"},
+		{".6000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003e-9", ".6000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003e-9"},
 		{"E\x1f", "E\x1f"}, // fuzz
 		{"1e9223372036854775807", "1e9223372036854775807"},
 		{"11e9223372036854775807", "11e9223372036854775807"},
@@ -108,11 +177,11 @@ func TestNumber(t *testing.T) {
 		{".12345e-2", ".0012345"},
 		{".12345e-3", "12345e-8"},
 		{".12345e-4", "12345e-9"},
-		{".12345e-5", "12345e-10"},
+		{".12345e-5", ".12345e-5"},
 
 		{".123456e-3", "123456e-9"},
 		{".123456e-2", ".00123456"},
-		{".1234567e-4", "1234567e-11"},
+		{".1234567e-4", ".1234567e-4"},
 		{".1234567e-3", ".0001234567"},
 
 		{"12345678e-1", "1234567.8"},
@@ -155,6 +224,7 @@ func TestNumberTruncate(t *testing.T) {
 		{"33.33", 0, "33"},
 		{"29.666", 0, "30"},
 		{"1.51", 1, "1.5"},
+		{"1.01", 1, "1"},
 	}
 	for _, tt := range numberTests {
 		t.Run(tt.number, func(t *testing.T) {
@@ -164,13 +234,32 @@ func TestNumberTruncate(t *testing.T) {
 	}
 }
 
+func TestDecimalRandom(t *testing.T) {
+	N := int(1e4)
+	if testing.Short() {
+		N = 0
+	}
+	for i := 0; i < N; i++ {
+		b := RandNumBytes(false)
+		f, _ := strconv.ParseFloat(string(b), 64)
+
+		b2 := make([]byte, len(b))
+		copy(b2, b)
+		b2 = Decimal(b2, -1)
+		f2, _ := strconv.ParseFloat(string(b2), 64)
+		if math.Abs(f-f2) > 1e-6 {
+			fmt.Println("Bad:", f, "!=", f2, "in", string(b), "to", string(b2))
+		}
+	}
+}
+
 func TestNumberRandom(t *testing.T) {
 	N := int(1e4)
 	if testing.Short() {
 		N = 0
 	}
 	for i := 0; i < N; i++ {
-		b := RandNumBytes()
+		b := RandNumBytes(true)
 		f, _ := strconv.ParseFloat(string(b), 64)
 
 		b2 := make([]byte, len(b))
@@ -191,11 +280,11 @@ var numbers [][]byte
 func TestMain(t *testing.T) {
 	numbers = make([][]byte, 0, n)
 	for j := 0; j < n; j++ {
-		numbers = append(numbers, RandNumBytes())
+		numbers = append(numbers, RandNumBytes(true))
 	}
 }
 
-func RandNumBytes() []byte {
+func RandNumBytes(withExp bool) []byte {
 	var b []byte
 	n := rand.Int() % 10
 	for i := 0; i < n; i++ {
@@ -208,7 +297,7 @@ func RandNumBytes() []byte {
 			b = append(b, byte(rand.Int()%10)+'0')
 		}
 	}
-	if rand.Int()%2 == 0 {
+	if withExp && rand.Int()%2 == 0 {
 		b = append(b, 'e')
 		if rand.Int()%2 == 0 {
 			b = append(b, '-')

+ 321 - 199
vendor/github.com/tdewolff/minify/css/css.go

@@ -4,6 +4,7 @@ package css // import "github.com/tdewolff/minify/css"
 import (
 	"bytes"
 	"encoding/hex"
+	"fmt"
 	"io"
 	"strconv"
 
@@ -29,16 +30,19 @@ type cssMinifier struct {
 	w io.Writer
 	p *css.Parser
 	o *Minifier
+
+	valuesBuffer []Token
 }
 
 ////////////////////////////////////////////////////////////////
 
 // DefaultMinifier is the default minifier.
-var DefaultMinifier = &Minifier{Decimals: -1}
+var DefaultMinifier = &Minifier{Decimals: -1, KeepCSS2: false}
 
 // Minifier is a CSS minifier.
 type Minifier struct {
 	Decimals int
+	KeepCSS2 bool
 }
 
 // Minify minifies CSS data, it reads from r and writes to w.
@@ -108,7 +112,19 @@ func (c *cssMinifier) minifyGrammar() error {
 			if _, err := c.w.Write(data); err != nil {
 				return err
 			}
-			for _, val := range c.p.Values() {
+			values := c.p.Values()
+			if css.ToHash(data[1:]) == css.Import && len(values) == 2 && values[1].TokenType == css.URLToken {
+				url := values[1].Data
+				if url[4] != '"' && url[4] != '\'' {
+					url = url[3:]
+					url[0] = '"'
+					url[len(url)-1] = '"'
+				} else {
+					url = url[4 : len(url)-1]
+				}
+				values[1].Data = url
+			}
+			for _, val := range values {
 				if _, err := c.w.Write(val.Data); err != nil {
 					return err
 				}
@@ -216,138 +232,238 @@ func (c *cssMinifier) minifySelectors(property []byte, values []css.Token) error
 	return nil
 }
 
-func (c *cssMinifier) minifyDeclaration(property []byte, values []css.Token) error {
-	if len(values) == 0 {
+type Token struct {
+	css.TokenType
+	Data       []byte
+	Components []css.Token // only filled for functions
+}
+
+func (t Token) String() string {
+	if len(t.Components) == 0 {
+		return t.TokenType.String() + "(" + string(t.Data) + ")"
+	} else {
+		return fmt.Sprint(t.Components)
+	}
+}
+
+func (c *cssMinifier) minifyDeclaration(property []byte, components []css.Token) error {
+	if len(components) == 0 {
 		return nil
 	}
+
 	prop := css.ToHash(property)
-	inProgid := false
-	for i, value := range values {
-		if inProgid {
-			if value.TokenType == css.FunctionToken {
-				inProgid = false
-			}
-			continue
-		} else if value.TokenType == css.IdentToken && css.ToHash(value.Data) == css.Progid {
-			inProgid = true
-			continue
+
+	// Strip !important from the component list, this will be added later separately
+	important := false
+	if len(components) > 2 && components[len(components)-2].TokenType == css.DelimToken && components[len(components)-2].Data[0] == '!' && css.ToHash(components[len(components)-1].Data) == css.Important {
+		components = components[:len(components)-2]
+		important = true
+	}
+
+	// Check if this is a simple list of values separated by whitespace or commas, otherwise we'll not be processing
+	simple := true
+	prevSep := true
+	values := c.valuesBuffer[:0]
+	for i := 0; i < len(components); i++ {
+		comp := components[i]
+		if comp.TokenType == css.LeftParenthesisToken || comp.TokenType == css.LeftBraceToken || comp.TokenType == css.LeftBracketToken || comp.TokenType == css.RightParenthesisToken || comp.TokenType == css.RightBraceToken || comp.TokenType == css.RightBracketToken {
+			simple = false
+			break
 		}
-		value.TokenType, value.Data = c.shortenToken(prop, value.TokenType, value.Data)
-		if prop == css.Font || prop == css.Font_Family || prop == css.Font_Weight {
-			if value.TokenType == css.IdentToken && (prop == css.Font || prop == css.Font_Weight) {
-				val := css.ToHash(value.Data)
-				if val == css.Normal && prop == css.Font_Weight {
-					// normal could also be specified for font-variant, not just font-weight
-					value.TokenType = css.NumberToken
-					value.Data = []byte("400")
-				} else if val == css.Bold {
-					value.TokenType = css.NumberToken
-					value.Data = []byte("700")
-				}
-			} else if value.TokenType == css.StringToken && (prop == css.Font || prop == css.Font_Family) && len(value.Data) > 2 {
-				unquote := true
-				parse.ToLower(value.Data)
-				s := value.Data[1 : len(value.Data)-1]
-				if len(s) > 0 {
-					for _, split := range bytes.Split(s, spaceBytes) {
-						val := css.ToHash(split)
-						// if len is zero, it contains two consecutive spaces
-						if val == css.Inherit || val == css.Serif || val == css.Sans_Serif || val == css.Monospace || val == css.Fantasy || val == css.Cursive || val == css.Initial || val == css.Default ||
-							len(split) == 0 || !css.IsIdent(split) {
-							unquote = false
-							break
-						}
+
+		if !prevSep && comp.TokenType != css.WhitespaceToken && comp.TokenType != css.CommaToken {
+			simple = false
+			break
+		}
+
+		if comp.TokenType == css.WhitespaceToken || comp.TokenType == css.CommaToken {
+			prevSep = true
+			if comp.TokenType == css.CommaToken {
+				values = append(values, Token{components[i].TokenType, components[i].Data, nil})
+			}
+		} else if comp.TokenType == css.FunctionToken {
+			prevSep = false
+			j := i + 1
+			level := 0
+			for ; j < len(components); j++ {
+				if components[j].TokenType == css.LeftParenthesisToken {
+					level++
+				} else if components[j].TokenType == css.RightParenthesisToken {
+					if level == 0 {
+						j++
+						break
 					}
+					level--
 				}
-				if unquote {
-					value.Data = s
-				}
-			}
-		} else if prop == css.Outline || prop == css.Border || prop == css.Border_Bottom || prop == css.Border_Left || prop == css.Border_Right || prop == css.Border_Top {
-			if css.ToHash(value.Data) == css.None {
-				value.TokenType = css.NumberToken
-				value.Data = zeroBytes
 			}
+			values = append(values, Token{components[i].TokenType, components[i].Data, components[i:j]})
+			i = j - 1
+		} else {
+			prevSep = false
+			values = append(values, Token{components[i].TokenType, components[i].Data, nil})
 		}
-		values[i].TokenType, values[i].Data = value.TokenType, value.Data
 	}
+	c.valuesBuffer = values
 
-	important := false
-	if len(values) > 2 && values[len(values)-2].TokenType == css.DelimToken && values[len(values)-2].Data[0] == '!' && css.ToHash(values[len(values)-1].Data) == css.Important {
-		values = values[:len(values)-2]
-		important = true
-	}
+	// Do not process complex values (eg. containing blocks or is not alternated between whitespace/commas and flat values
+	if !simple {
+		if prop == css.Filter && len(components) == 11 {
+			if bytes.Equal(components[0].Data, []byte("progid")) &&
+				components[1].TokenType == css.ColonToken &&
+				bytes.Equal(components[2].Data, []byte("DXImageTransform")) &&
+				components[3].Data[0] == '.' &&
+				bytes.Equal(components[4].Data, []byte("Microsoft")) &&
+				components[5].Data[0] == '.' &&
+				bytes.Equal(components[6].Data, []byte("Alpha(")) &&
+				bytes.Equal(parse.ToLower(components[7].Data), []byte("opacity")) &&
+				components[8].Data[0] == '=' &&
+				components[10].Data[0] == ')' {
+				components = components[6:]
+				components[0].Data = []byte("alpha(")
+			}
+		}
 
-	if len(values) == 1 {
-		if prop == css.Background && css.ToHash(values[0].Data) == css.None {
-			values[0].Data = backgroundNoneBytes
-		} else if bytes.Equal(property, msfilterBytes) {
-			alpha := []byte("progid:DXImageTransform.Microsoft.Alpha(Opacity=")
-			if values[0].TokenType == css.StringToken && bytes.HasPrefix(values[0].Data[1:len(values[0].Data)-1], alpha) {
-				values[0].Data = append(append([]byte{values[0].Data[0]}, []byte("alpha(opacity=")...), values[0].Data[1+len(alpha):]...)
+		for _, component := range components {
+			if _, err := c.w.Write(component.Data); err != nil {
+				return err
 			}
 		}
-	} else {
-		if prop == css.Margin || prop == css.Padding || prop == css.Border_Width {
-			if (values[0].TokenType == css.NumberToken || values[0].TokenType == css.DimensionToken || values[0].TokenType == css.PercentageToken) && (len(values)+1)%2 == 0 {
-				valid := true
-				for i := 1; i < len(values); i += 2 {
-					if values[i].TokenType != css.WhitespaceToken || values[i+1].TokenType != css.NumberToken && values[i+1].TokenType != css.DimensionToken && values[i+1].TokenType != css.PercentageToken {
-						valid = false
+		if important {
+			if _, err := c.w.Write([]byte("!important")); err != nil {
+				return err
+			}
+		}
+		return nil
+	}
+
+	for i := range values {
+		values[i].TokenType, values[i].Data = c.shortenToken(prop, values[i].TokenType, values[i].Data)
+	}
+
+	if len(values) > 0 {
+		switch prop {
+		case css.Font, css.Font_Weight, css.Font_Family:
+			if prop == css.Font {
+				// in "font:" shorthand all values before the size have "normal"
+				// as valid and, at the same time, default value, so just skip them
+				for i, value := range values {
+					if !(value.TokenType == css.IdentToken && css.ToHash(value.Data) == css.Normal) {
+						values = values[i:]
 						break
 					}
 				}
-				if valid {
-					n := (len(values) + 1) / 2
-					if n == 2 {
-						if bytes.Equal(values[0].Data, values[2].Data) {
-							values = values[:1]
-						}
-					} else if n == 3 {
-						if bytes.Equal(values[0].Data, values[2].Data) && bytes.Equal(values[0].Data, values[4].Data) {
-							values = values[:1]
-						} else if bytes.Equal(values[0].Data, values[4].Data) {
-							values = values[:3]
-						}
-					} else if n == 4 {
-						if bytes.Equal(values[0].Data, values[2].Data) && bytes.Equal(values[0].Data, values[4].Data) && bytes.Equal(values[0].Data, values[6].Data) {
-							values = values[:1]
-						} else if bytes.Equal(values[0].Data, values[4].Data) && bytes.Equal(values[2].Data, values[6].Data) {
-							values = values[:3]
-						} else if bytes.Equal(values[2].Data, values[6].Data) {
-							values = values[:5]
+			}
+			for i, value := range values {
+				if value.TokenType == css.IdentToken {
+					val := css.ToHash(value.Data)
+					if prop == css.Font_Weight && val == css.Normal {
+						values[i].TokenType = css.NumberToken
+						values[i].Data = []byte("400")
+					} else if val == css.Bold {
+						values[i].TokenType = css.NumberToken
+						values[i].Data = []byte("700")
+					}
+				} else if value.TokenType == css.StringToken && len(value.Data) > 2 {
+					unquote := true
+					parse.ToLower(value.Data)
+					s := value.Data[1 : len(value.Data)-1]
+					if len(s) > 0 {
+						for _, split := range bytes.Split(s, spaceBytes) {
+							val := css.ToHash(split)
+							// if len is zero, it contains two consecutive spaces
+							if val == css.Inherit || val == css.Serif || val == css.Sans_Serif || val == css.Monospace || val == css.Fantasy || val == css.Cursive || val == css.Initial || val == css.Default ||
+								len(split) == 0 || !css.IsIdent(split) {
+								unquote = false
+								break
+							}
 						}
 					}
+					if unquote {
+						values[i].Data = s
+					}
 				}
 			}
-		} else if prop == css.Filter && len(values) == 11 {
-			if bytes.Equal(values[0].Data, []byte("progid")) &&
-				values[1].TokenType == css.ColonToken &&
-				bytes.Equal(values[2].Data, []byte("DXImageTransform")) &&
-				values[3].Data[0] == '.' &&
-				bytes.Equal(values[4].Data, []byte("Microsoft")) &&
-				values[5].Data[0] == '.' &&
-				bytes.Equal(values[6].Data, []byte("Alpha(")) &&
-				bytes.Equal(parse.ToLower(values[7].Data), []byte("opacity")) &&
-				values[8].Data[0] == '=' &&
-				values[10].Data[0] == ')' {
-				values = values[6:]
-				values[0].Data = []byte("alpha(")
+		case css.Margin, css.Padding, css.Border_Width:
+			n := len(values)
+			if n == 2 {
+				if bytes.Equal(values[0].Data, values[1].Data) {
+					values = values[:1]
+				}
+			} else if n == 3 {
+				if bytes.Equal(values[0].Data, values[1].Data) && bytes.Equal(values[0].Data, values[2].Data) {
+					values = values[:1]
+				} else if bytes.Equal(values[0].Data, values[2].Data) {
+					values = values[:2]
+				}
+			} else if n == 4 {
+				if bytes.Equal(values[0].Data, values[1].Data) && bytes.Equal(values[0].Data, values[2].Data) && bytes.Equal(values[0].Data, values[3].Data) {
+					values = values[:1]
+				} else if bytes.Equal(values[0].Data, values[2].Data) && bytes.Equal(values[1].Data, values[3].Data) {
+					values = values[:2]
+				} else if bytes.Equal(values[1].Data, values[3].Data) {
+					values = values[:3]
+				}
+			}
+		case css.Outline, css.Border, css.Border_Bottom, css.Border_Left, css.Border_Right, css.Border_Top:
+			none := false
+			iZero := -1
+			for i, value := range values {
+				if len(value.Data) == 1 && value.Data[0] == '0' {
+					iZero = i
+				} else if css.ToHash(value.Data) == css.None {
+					values[i].TokenType = css.NumberToken
+					values[i].Data = zeroBytes
+					none = true
+				}
+			}
+			if none && iZero != -1 {
+				values = append(values[:iZero], values[iZero+1:]...)
+			}
+		case css.Background:
+			ident := css.ToHash(values[0].Data)
+			if len(values) == 1 && (ident == css.None || ident == css.Transparent) {
+				values[0].Data = backgroundNoneBytes
+			}
+		case css.Box_Shadow:
+			if len(values) == 4 && len(values[0].Data) == 1 && values[0].Data[0] == '0' && len(values[1].Data) == 1 && values[1].Data[0] == '0' && len(values[2].Data) == 1 && values[2].Data[0] == '0' && len(values[3].Data) == 1 && values[3].Data[0] == '0' {
+				values = values[:2]
+			}
+		default:
+			if bytes.Equal(property, msfilterBytes) {
+				alpha := []byte("progid:DXImageTransform.Microsoft.Alpha(Opacity=")
+				if values[0].TokenType == css.StringToken && bytes.HasPrefix(values[0].Data[1:len(values[0].Data)-1], alpha) {
+					values[0].Data = append(append([]byte{values[0].Data[0]}, []byte("alpha(opacity=")...), values[0].Data[1+len(alpha):]...)
+				}
 			}
 		}
 	}
 
-	for i := 0; i < len(values); i++ {
-		if values[i].TokenType == css.FunctionToken {
-			n, err := c.minifyFunction(values[i:])
+	prevComma := true
+	for _, value := range values {
+		if !prevComma && value.TokenType != css.CommaToken {
+			if _, err := c.w.Write([]byte(" ")); err != nil {
+				return err
+			}
+		}
+
+		if value.TokenType == css.FunctionToken {
+			err := c.minifyFunction(value.Components)
 			if err != nil {
 				return err
 			}
-			i += n - 1
-		} else if _, err := c.w.Write(values[i].Data); err != nil {
-			return err
+		} else {
+			if _, err := c.w.Write(value.Data); err != nil {
+				return err
+			}
+		}
+
+		if value.TokenType == css.CommaToken {
+			prevComma = true
+		} else {
+			prevComma = false
 		}
 	}
+
 	if important {
 		if _, err := c.w.Write([]byte("!important")); err != nil {
 			return err
@@ -356,104 +472,76 @@ func (c *cssMinifier) minifyDeclaration(property []byte, values []css.Token) err
 	return nil
 }
 
-func (c *cssMinifier) minifyFunction(values []css.Token) (int, error) {
-	n := 1
-	simple := true
-	for i, value := range values[1:] {
-		if value.TokenType == css.RightParenthesisToken {
-			n++
-			break
-		}
-		if i%2 == 0 && (value.TokenType != css.NumberToken && value.TokenType != css.PercentageToken) || (i%2 == 1 && value.TokenType != css.CommaToken) {
-			simple = false
-		}
-		n++
-	}
-	values = values[:n]
-	if simple && (n-1)%2 == 0 {
-		fun := css.ToHash(values[0].Data[:len(values[0].Data)-1])
-		nArgs := (n - 1) / 2
-		if (fun == css.Rgba || fun == css.Hsla) && nArgs == 4 {
-			d, _ := strconv.ParseFloat(string(values[7].Data), 32) // can never fail because if simple == true than this is a NumberToken or PercentageToken
-			if d-1.0 > -minify.Epsilon {
-				if fun == css.Rgba {
-					values[0].Data = []byte("rgb(")
-					fun = css.Rgb
-				} else {
-					values[0].Data = []byte("hsl(")
-					fun = css.Hsl
-				}
-				values = values[:len(values)-2]
-				values[len(values)-1].Data = []byte(")")
-				nArgs = 3
-			} else if d < minify.Epsilon {
-				values[0].Data = []byte("transparent")
-				values = values[:1]
-				fun = 0
-				nArgs = 0
+func (c *cssMinifier) minifyFunction(values []css.Token) error {
+	n := len(values)
+	if n > 2 {
+		simple := true
+		for i, value := range values[1 : n-1] {
+			if i%2 == 0 && (value.TokenType != css.NumberToken && value.TokenType != css.PercentageToken) || (i%2 == 1 && value.TokenType != css.CommaToken) {
+				simple = false
 			}
 		}
-		if fun == css.Rgb && nArgs == 3 {
-			var err [3]error
-			rgb := [3]byte{}
-			for j := 0; j < 3; j++ {
-				val := values[j*2+1]
-				if val.TokenType == css.NumberToken {
-					var d int64
-					d, err[j] = strconv.ParseInt(string(val.Data), 10, 32)
-					if d < 0 {
-						d = 0
-					} else if d > 255 {
-						d = 255
-					}
-					rgb[j] = byte(d)
-				} else if val.TokenType == css.PercentageToken {
-					var d float64
-					d, err[j] = strconv.ParseFloat(string(val.Data[:len(val.Data)-1]), 32)
-					if d < 0.0 {
-						d = 0.0
-					} else if d > 100.0 {
-						d = 100.0
+
+		if simple && n%2 == 1 {
+			fun := css.ToHash(values[0].Data[0 : len(values[0].Data)-1])
+			for i := 1; i < n; i += 2 {
+				values[i].TokenType, values[i].Data = c.shortenToken(0, values[i].TokenType, values[i].Data)
+			}
+
+			nArgs := (n - 1) / 2
+			if (fun == css.Rgba || fun == css.Hsla) && nArgs == 4 {
+				d, _ := strconv.ParseFloat(string(values[7].Data), 32) // can never fail because if simple == true than this is a NumberToken or PercentageToken
+				if d-1.0 > -minify.Epsilon {
+					if fun == css.Rgba {
+						values[0].Data = []byte("rgb(")
+						fun = css.Rgb
+					} else {
+						values[0].Data = []byte("hsl(")
+						fun = css.Hsl
 					}
-					rgb[j] = byte((d / 100.0 * 255.0) + 0.5)
+					values = values[:len(values)-2]
+					values[len(values)-1].Data = []byte(")")
+					nArgs = 3
+				} else if d < minify.Epsilon {
+					values[0].Data = []byte("transparent")
+					values = values[:1]
+					fun = 0
+					nArgs = 0
 				}
 			}
-			if err[0] == nil && err[1] == nil && err[2] == nil {
-				val := make([]byte, 7)
-				val[0] = '#'
-				hex.Encode(val[1:], rgb[:])
-				parse.ToLower(val)
-				if s, ok := ShortenColorHex[string(val)]; ok {
-					if _, err := c.w.Write(s); err != nil {
-						return 0, err
-					}
-				} else {
-					if len(val) == 7 && val[1] == val[2] && val[3] == val[4] && val[5] == val[6] {
-						val[2] = val[3]
-						val[3] = val[5]
-						val = val[:4]
-					}
-					if _, err := c.w.Write(val); err != nil {
-						return 0, err
+			if fun == css.Rgb && nArgs == 3 {
+				var err [3]error
+				rgb := [3]byte{}
+				for j := 0; j < 3; j++ {
+					val := values[j*2+1]
+					if val.TokenType == css.NumberToken {
+						var d int64
+						d, err[j] = strconv.ParseInt(string(val.Data), 10, 32)
+						if d < 0 {
+							d = 0
+						} else if d > 255 {
+							d = 255
+						}
+						rgb[j] = byte(d)
+					} else if val.TokenType == css.PercentageToken {
+						var d float64
+						d, err[j] = strconv.ParseFloat(string(val.Data[:len(val.Data)-1]), 32)
+						if d < 0.0 {
+							d = 0.0
+						} else if d > 100.0 {
+							d = 100.0
+						}
+						rgb[j] = byte((d / 100.0 * 255.0) + 0.5)
 					}
 				}
-				return n, nil
-			}
-		} else if fun == css.Hsl && nArgs == 3 {
-			if values[1].TokenType == css.NumberToken && values[3].TokenType == css.PercentageToken && values[5].TokenType == css.PercentageToken {
-				h, err1 := strconv.ParseFloat(string(values[1].Data), 32)
-				s, err2 := strconv.ParseFloat(string(values[3].Data[:len(values[3].Data)-1]), 32)
-				l, err3 := strconv.ParseFloat(string(values[5].Data[:len(values[5].Data)-1]), 32)
-				if err1 == nil && err2 == nil && err3 == nil {
-					r, g, b := css.HSL2RGB(h/360.0, s/100.0, l/100.0)
-					rgb := []byte{byte((r * 255.0) + 0.5), byte((g * 255.0) + 0.5), byte((b * 255.0) + 0.5)}
+				if err[0] == nil && err[1] == nil && err[2] == nil {
 					val := make([]byte, 7)
 					val[0] = '#'
 					hex.Encode(val[1:], rgb[:])
 					parse.ToLower(val)
 					if s, ok := ShortenColorHex[string(val)]; ok {
 						if _, err := c.w.Write(s); err != nil {
-							return 0, err
+							return err
 						}
 					} else {
 						if len(val) == 7 && val[1] == val[2] && val[3] == val[4] && val[5] == val[6] {
@@ -462,20 +550,50 @@ func (c *cssMinifier) minifyFunction(values []css.Token) (int, error) {
 							val = val[:4]
 						}
 						if _, err := c.w.Write(val); err != nil {
-							return 0, err
+							return err
 						}
 					}
-					return n, nil
+					return nil
+				}
+			} else if fun == css.Hsl && nArgs == 3 {
+				if values[1].TokenType == css.NumberToken && values[3].TokenType == css.PercentageToken && values[5].TokenType == css.PercentageToken {
+					h, err1 := strconv.ParseFloat(string(values[1].Data), 32)
+					s, err2 := strconv.ParseFloat(string(values[3].Data[:len(values[3].Data)-1]), 32)
+					l, err3 := strconv.ParseFloat(string(values[5].Data[:len(values[5].Data)-1]), 32)
+					if err1 == nil && err2 == nil && err3 == nil {
+						r, g, b := css.HSL2RGB(h/360.0, s/100.0, l/100.0)
+						rgb := []byte{byte((r * 255.0) + 0.5), byte((g * 255.0) + 0.5), byte((b * 255.0) + 0.5)}
+						val := make([]byte, 7)
+						val[0] = '#'
+						hex.Encode(val[1:], rgb[:])
+						parse.ToLower(val)
+						if s, ok := ShortenColorHex[string(val)]; ok {
+							if _, err := c.w.Write(s); err != nil {
+								return err
+							}
+						} else {
+							if len(val) == 7 && val[1] == val[2] && val[3] == val[4] && val[5] == val[6] {
+								val[2] = val[3]
+								val[3] = val[5]
+								val = val[:4]
+							}
+							if _, err := c.w.Write(val); err != nil {
+								return err
+							}
+						}
+						return nil
+					}
 				}
 			}
 		}
 	}
+
 	for _, value := range values {
 		if _, err := c.w.Write(value.Data); err != nil {
-			return 0, err
+			return err
 		}
 	}
-	return n, nil
+	return nil
 }
 
 func (c *cssMinifier) shortenToken(prop css.Hash, tt css.TokenType, data []byte) (css.TokenType, []byte) {
@@ -491,11 +609,15 @@ func (c *cssMinifier) shortenToken(prop css.Hash, tt css.TokenType, data []byte)
 		}
 		dim := data[n:]
 		parse.ToLower(dim)
-		data = minify.Number(data[:n], c.o.Decimals)
-		if tt == css.PercentageToken && (len(data) != 1 || data[0] != '0' || prop == css.Color) {
-			data = append(data, '%')
-		} else if tt == css.DimensionToken && (len(data) != 1 || data[0] != '0' || requiredDimension[string(dim)]) {
+		if !c.o.KeepCSS2 {
+			data = minify.Number(data[:n], c.o.Decimals)
+		} else {
+			data = minify.Decimal(data[:n], c.o.Decimals) // don't use exponents
+		}
+		if tt == css.DimensionToken && (len(data) != 1 || data[0] != '0' || !optionalZeroDimension[string(dim)] || prop == css.Flex) {
 			data = append(data, dim...)
+		} else if tt == css.PercentageToken {
+			data = append(data, '%') // TODO: drop percentage for properties that accept <percentage> and <length>
 		}
 	} else if tt == css.IdentToken {
 		//parse.ToLower(data) // TODO: not all identifiers are case-insensitive; all <custom-ident> properties are case-sensitive
@@ -541,7 +663,7 @@ func (c *cssMinifier) shortenToken(prop css.Hash, tt css.TokenType, data []byte)
 	} else if tt == css.URLToken {
 		parse.ToLower(data[:3])
 		if len(data) > 10 {
-			uri := data[4 : len(data)-1]
+			uri := parse.TrimWhitespace(data[4 : len(data)-1])
 			delim := byte('"')
 			if uri[0] == '\'' || uri[0] == '"' {
 				delim = uri[0]

+ 39 - 1
vendor/github.com/tdewolff/minify/css/css_test.go

@@ -23,6 +23,8 @@ func TestCSS(t *testing.T) {
 		{".cla[id ^= L] { x:y; }", ".cla[id^=L]{x:y}"},
 		{"area:focus { outline : 0;}", "area:focus{outline:0}"},
 		{"@import 'file';", "@import 'file'"},
+		{"@import url('file');", "@import 'file'"},
+		{"@import url(//url);", `@import "//url"`},
 		{"@font-face { x:y; }", "@font-face{x:y}"},
 
 		{"input[type=\"radio\"]{x:y}", "input[type=radio]{x:y}"},
@@ -51,6 +53,7 @@ func TestCSS(t *testing.T) {
 		// go-fuzz
 		{"input[type=\"\x00\"] {  a: b\n}.a{}", "input[type=\"\x00\"]{a:b}.a{}"},
 		{"a{a:)'''", "a{a:)'''}"},
+		{"{T:l(", "{t:l(}"},
 	}
 
 	m := minify.New()
@@ -91,11 +94,17 @@ func TestCSSInline(t *testing.T) {
 		{"color: hsla(1,2%,3%,1);", "color:#080807"},
 		{"color: hsla(1,2%,3%,0);", "color:transparent"},
 		{"color: hsl(48,100%,50%);", "color:#fc0"},
+		{"background: hsla(0,0%,100%,.7);", "background:hsla(0,0%,100%,.7)"},
 		{"font-weight: bold; font-weight: normal;", "font-weight:700;font-weight:400"},
 		{"font: bold \"Times new Roman\",\"Sans-Serif\";", "font:700 times new roman,\"sans-serif\""},
+		{"font: normal normal normal normal 20px normal", "font:20px normal"},
 		{"outline: none;", "outline:0"},
+		{"outline: solid black 0;", "outline:solid #000 0"},
+		{"outline: none black 5px;", "outline:0 #000 5px"},
 		{"outline: none !important;", "outline:0!important"},
 		{"border-left: none;", "border-left:0"},
+		{"border-left: none 0;", "border-left:0"},
+		{"border-left: 0 dashed red;", "border-left:0 dashed red"},
 		{"margin: 1 1 1 1;", "margin:1"},
 		{"margin: 1 2 1 2;", "margin:1 2"},
 		{"margin: 1 2 3 2;", "margin:1 2 3"},
@@ -106,11 +115,13 @@ func TestCSSInline(t *testing.T) {
 		{"margin: 0em;", "margin:0"},
 		{"font-family:'Arial', 'Times New Roman';", "font-family:arial,times new roman"},
 		{"background:url('http://domain.com/image.png');", "background:url(http://domain.com/image.png)"},
+		{"background:url( 'http://domain.com/image.png' );", "background:url(http://domain.com/image.png)"},
 		{"filter: progid : DXImageTransform.Microsoft.BasicImage(rotation=1);", "filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1)"},
 		{"filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=0);", "filter:alpha(opacity=0)"},
 		{"content: \"a\\\nb\";", "content:\"ab\""},
 		{"content: \"a\\\r\nb\\\r\nc\";", "content:\"abc\""},
 		{"content: \"\";", "content:\"\""},
+		{"x: white , white", "x:#fff,#fff"},
 
 		{"font:27px/13px arial,sans-serif", "font:27px/13px arial,sans-serif"},
 		{"text-decoration: none !important", "text-decoration:none!important"},
@@ -139,9 +150,15 @@ func TestCSSInline(t *testing.T) {
 		{"margin:0 0 18px 0;", "margin:0 0 18px"},
 		{"background:none", "background:0 0"},
 		{"background:none 1 1", "background:none 1 1"},
+		{"background:transparent", "background:0 0"},
+		{"background:transparent no-repeat", "background:transparent no-repeat"},
 		{"z-index:1000", "z-index:1000"},
+		{"box-shadow:0 0 0 0", "box-shadow:0 0"},
+		{"flex:0px", "flex:0px"},
 
 		{"any:0deg 0s 0ms 0dpi 0dpcm 0dppx 0hz 0khz", "any:0 0s 0ms 0dpi 0dpcm 0dppx 0hz 0khz"},
+		{"width:calc(0%-0px)", "width:calc(0%-0px)"},
+		{"border-left:0 none", "border-left:0"},
 		{"--custom-variable:0px;", "--custom-variable:0px"},
 		{"--foo: if(x > 5) this.width = 10", "--foo: if(x > 5) this.width = 10"},
 		{"--foo: ;", "--foo: "},
@@ -156,7 +173,7 @@ func TestCSSInline(t *testing.T) {
 		{"margin: 1 1 1;", "margin:1"},
 		{"margin: 1 2 1;", "margin:1 2"},
 		{"margin: 1 2 3;", "margin:1 2 3"},
-		{"margin: 0%;", "margin:0"},
+		// {"margin: 0%;", "margin:0"},
 		{"color: rgb(255,64,64);", "color:#ff4040"},
 		{"color: rgb(256,-34,2342435);", "color:#f0f"},
 		{"color: rgb(120%,-45%,234234234%);", "color:#f0f"},
@@ -181,6 +198,27 @@ func TestCSSInline(t *testing.T) {
 	}
 }
 
+func TestCSSKeepCSS2(t *testing.T) {
+	tests := []struct {
+		css      string
+		expected string
+	}{
+		{`margin:5000em`, `margin:5000em`},
+	}
+
+	m := minify.New()
+	params := map[string]string{"inline": "1"}
+	cssMinifier := &Minifier{Decimals: -1, KeepCSS2: true}
+	for _, tt := range tests {
+		t.Run(tt.css, func(t *testing.T) {
+			r := bytes.NewBufferString(tt.css)
+			w := &bytes.Buffer{}
+			err := cssMinifier.Minify(m, w, r, params)
+			test.Minify(t, tt.css, err, w.String(), tt.expected)
+		})
+	}
+}
+
 func TestReaderErrors(t *testing.T) {
 	r := test.NewErrorReader(0)
 	w := &bytes.Buffer{}

+ 20 - 8
vendor/github.com/tdewolff/minify/css/table.go

@@ -2,14 +2,26 @@ package css
 
 import "github.com/tdewolff/parse/css"
 
-var requiredDimension = map[string]bool{
-	"s":    true,
-	"ms":   true,
-	"dpi":  true,
-	"dpcm": true,
-	"dppx": true,
-	"hz":   true,
-	"khz":  true,
+var optionalZeroDimension = map[string]bool{
+	"px":   true,
+	"mm":   true,
+	"q":    true,
+	"cm":   true,
+	"in":   true,
+	"pt":   true,
+	"pc":   true,
+	"ch":   true,
+	"em":   true,
+	"ex":   true,
+	"rem":  true,
+	"vh":   true,
+	"vw":   true,
+	"vmin": true,
+	"vmax": true,
+	"deg":  true,
+	"grad": true,
+	"rad":  true,
+	"turn": true,
 }
 
 // Uses http://www.w3.org/TR/2010/PR-css3-color-20101028/ for colors

+ 14 - 10
vendor/github.com/tdewolff/minify/html/html.go

@@ -80,10 +80,10 @@ func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, _ map[string]st
 				return err
 			}
 		case html.CommentToken:
-			if o.KeepConditionalComments && len(t.Text) > 6 && (bytes.HasPrefix(t.Text, []byte("[if ")) || bytes.Equal(t.Text, []byte("[endif]"))) {
+			if o.KeepConditionalComments && len(t.Text) > 6 && (bytes.HasPrefix(t.Text, []byte("[if ")) || bytes.Equal(t.Text, []byte("[endif]")) || bytes.Equal(t.Text, []byte("<![endif]"))) {
 				// [if ...] is always 7 or more characters, [endif] is only encountered for downlevel-revealed
 				// see https://msdn.microsoft.com/en-us/library/ms537512(v=vs.85).aspx#syntax
-				if bytes.HasPrefix(t.Data, []byte("<!--[if ")) { // downlevel-hidden
+				if bytes.HasPrefix(t.Data, []byte("<!--[if ")) && len(t.Data) > len("<!--[if ]><![endif]-->") { // downlevel-hidden
 					begin := bytes.IndexByte(t.Data, '>') + 1
 					end := len(t.Data) - len("<![endif]-->")
 					if _, err := w.Write(t.Data[:begin]); err != nil {
@@ -95,7 +95,7 @@ func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, _ map[string]st
 					if _, err := w.Write(t.Data[end:]); err != nil {
 						return err
 					}
-				} else if _, err := w.Write(t.Data); err != nil { // downlevel-revealed
+				} else if _, err := w.Write(t.Data); err != nil { // downlevel-revealed or short downlevel-hidden
 					return err
 				}
 			}
@@ -281,13 +281,16 @@ func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, _ map[string]st
 					attrs := tb.Attributes(html.Content, html.Http_Equiv, html.Charset, html.Name)
 					if content := attrs[0]; content != nil {
 						if httpEquiv := attrs[1]; httpEquiv != nil {
-							content.AttrVal = minify.ContentType(content.AttrVal)
-							if charset := attrs[2]; charset == nil && parse.EqualFold(httpEquiv.AttrVal, []byte("content-type")) && bytes.Equal(content.AttrVal, []byte("text/html;charset=utf-8")) {
-								httpEquiv.Text = nil
-								content.Text = []byte("charset")
-								content.Hash = html.Charset
-								content.AttrVal = []byte("utf-8")
+							if charset := attrs[2]; charset == nil && parse.EqualFold(httpEquiv.AttrVal, []byte("content-type")) {
+								content.AttrVal = minify.Mediatype(content.AttrVal)
+								if bytes.Equal(content.AttrVal, []byte("text/html;charset=utf-8")) {
+									httpEquiv.Text = nil
+									content.Text = []byte("charset")
+									content.Hash = html.Charset
+									content.AttrVal = []byte("utf-8")
+								}
 							} else if parse.EqualFold(httpEquiv.AttrVal, []byte("content-style-type")) {
+								content.AttrVal = minify.Mediatype(content.AttrVal)
 								defaultStyleType, defaultStyleParams = parse.Mediatype(content.AttrVal)
 								if defaultStyleParams != nil {
 									defaultInlineStyleParams = defaultStyleParams
@@ -296,6 +299,7 @@ func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, _ map[string]st
 									defaultInlineStyleParams = map[string]string{"inline": "1"}
 								}
 							} else if parse.EqualFold(httpEquiv.AttrVal, []byte("content-script-type")) {
+								content.AttrVal = minify.Mediatype(content.AttrVal)
 								defaultScriptType, defaultScriptParams = parse.Mediatype(content.AttrVal)
 							}
 						}
@@ -365,7 +369,7 @@ func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, _ map[string]st
 					if attr.Traits&caselessAttr != 0 {
 						val = parse.ToLower(val)
 						if attr.Hash == html.Enctype || attr.Hash == html.Codetype || attr.Hash == html.Accept || attr.Hash == html.Type && (t.Hash == html.A || t.Hash == html.Link || t.Hash == html.Object || t.Hash == html.Param || t.Hash == html.Script || t.Hash == html.Style || t.Hash == html.Source) {
-							val = minify.ContentType(val)
+							val = minify.Mediatype(val)
 						}
 					}
 					if rawTagHash != 0 && attr.Hash == html.Type {

+ 2 - 0
vendor/github.com/tdewolff/minify/html/html_test.go

@@ -32,6 +32,7 @@ func TestHTML(t *testing.T) {
 		{`<html><head></head><body>x</body></html>`, `x`},
 		{`<meta http-equiv="content-type" content="text/html; charset=utf-8">`, `<meta charset=utf-8>`},
 		{`<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />`, `<meta charset=utf-8>`},
+		{`<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src https://*; child-src 'none';">`, `<meta http-equiv=content-security-policy content="default-src 'self'; img-src https://*; child-src 'none';">`},
 		{`<meta name="keywords" content="a, b">`, `<meta name=keywords content=a,b>`},
 		{`<meta name="viewport" content="width = 996" />`, `<meta name=viewport content="width=996">`},
 		{`<span attr="test"></span>`, `<span attr=test></span>`},
@@ -178,6 +179,7 @@ func TestHTMLKeepConditionalComments(t *testing.T) {
 	}{
 		{`<!--[if IE 6]> <b> </b> <![endif]-->`, `<!--[if IE 6]><b></b><![endif]-->`},
 		{`<![if IE 6]> <b> </b> <![endif]>`, `<![if IE 6]><b></b><![endif]>`},
+		{`<!--[if !mso]><!--> <b> </b> <!--<![endif]-->`, `<!--[if !mso]><!--><b></b><!--<![endif]-->`},
 	}
 
 	m := minify.New()

+ 0 - 1
vendor/github.com/tdewolff/minify/html/table.go

@@ -124,7 +124,6 @@ var attrMap = map[html.Hash]traits{
 	html.Defer:           booleanAttr,
 	html.Dir:             caselessAttr,
 	html.Disabled:        booleanAttr,
-	html.Draggable:       booleanAttr,
 	html.Enabled:         booleanAttr,
 	html.Enctype:         caselessAttr,
 	html.Face:            caselessAttr,

+ 14 - 5
vendor/github.com/tdewolff/minify/js/js.go

@@ -48,29 +48,38 @@ func (o *Minifier) Minify(_ *minify.M, w io.Writer, r io.Reader, _ map[string]st
 			lineTerminatorQueued = true
 		} else if tt == js.WhitespaceToken {
 			whitespaceQueued = true
-		} else if tt == js.CommentToken {
+		} else if tt == js.SingleLineCommentToken || tt == js.MultiLineCommentToken {
 			if len(data) > 5 && data[1] == '*' && data[2] == '!' {
 				if _, err := w.Write(data[:3]); err != nil {
 					return err
 				}
-				comment := parse.TrimWhitespace(parse.ReplaceMultipleWhitespace(data[3 : len(data)-2]))
+				comment := parse.ReplaceMultipleWhitespace(data[3 : len(data)-2])
+				if tt != js.MultiLineCommentToken {
+					// don't trim newlines in multiline comments as that might change ASI
+					// (we could do a more expensive check post-factum but it's not worth it)
+					comment = parse.TrimWhitespace(comment)
+				}
 				if _, err := w.Write(comment); err != nil {
 					return err
 				}
 				if _, err := w.Write(data[len(data)-2:]); err != nil {
 					return err
 				}
+			} else if tt == js.MultiLineCommentToken {
+				lineTerminatorQueued = true
+			} else {
+				whitespaceQueued = true
 			}
 		} else {
 			first := data[0]
-			if (prev == js.IdentifierToken || prev == js.NumericToken || prev == js.PunctuatorToken || prev == js.StringToken || prev == js.RegexpToken) &&
-				(tt == js.IdentifierToken || tt == js.NumericToken || tt == js.StringToken || tt == js.PunctuatorToken || tt == js.RegexpToken) {
+			if (prev == js.IdentifierToken || prev == js.NumericToken || prev == js.PunctuatorToken || prev == js.StringToken || prev == js.TemplateToken || prev == js.RegexpToken) &&
+				(tt == js.IdentifierToken || tt == js.NumericToken || tt == js.StringToken || tt == js.TemplateToken || tt == js.PunctuatorToken || tt == js.RegexpToken) {
 				if lineTerminatorQueued && (prev != js.PunctuatorToken || prevLast == '}' || prevLast == ']' || prevLast == ')' || prevLast == '+' || prevLast == '-' || prevLast == '"' || prevLast == '\'') &&
 					(tt != js.PunctuatorToken || first == '{' || first == '[' || first == '(' || first == '+' || first == '-' || first == '!' || first == '~') {
 					if _, err := w.Write(newlineBytes); err != nil {
 						return err
 					}
-				} else if whitespaceQueued && (prev != js.StringToken && prev != js.PunctuatorToken && tt != js.PunctuatorToken || (prevLast == '+' || prevLast == '-') && first == prevLast) {
+				} else if whitespaceQueued && (prev != js.StringToken && prev != js.PunctuatorToken && tt != js.PunctuatorToken || (prevLast == '+' || prevLast == '-' || prevLast == '/') && first == prevLast) {
 					if _, err := w.Write(spaceBytes); err != nil {
 						return err
 					}

+ 11 - 0
vendor/github.com/tdewolff/minify/js/js_test.go

@@ -40,6 +40,17 @@ func TestJS(t *testing.T) {
 		{"false\n\"string\"", "false\n\"string\""},                     // #109
 		{"`\n", "`"},                                                   // go fuzz
 		{"a\n~b", "a\n~b"},                                             // #132
+		{"x / /\\d+/.exec(s)[0]", "x/ /\\d+/.exec(s)[0]"},              // #183
+
+		{"function(){}\n`string`", "function(){}\n`string`"}, // #181
+		{"false\n`string`", "false\n`string`"},               // #181
+		{"`string`\nwhatever()", "`string`\nwhatever()"},     // #181
+
+		{"x+/**/++y", "x+ ++y"},                          // #185
+		{"x+\n++y", "x+\n++y"},                           // #185
+		{"f()/*!com\nment*/g()", "f()/*!com\nment*/g()"}, // #185
+		{"f()/*com\nment*/g()", "f()\ng()"},              // #185
+		{"f()/*!\n*/g()", "f()/*!\n*/g()"},               // #185
 
 		// go-fuzz
 		{`/\`, `/\`},

+ 9 - 1
vendor/github.com/tdewolff/minify/svg/pathdata.go

@@ -33,6 +33,8 @@ func NewPathData(o *Minifier) *PathData {
 	}
 }
 
+// ShortenPathData takes a full pathdata string and returns a shortened version. The original string is overwritten.
+// It parses all commands (M, A, Z, ...) and coordinates (numbers) and calls copyInstruction for each command.
 func (p *PathData) ShortenPathData(b []byte) []byte {
 	var x0, y0 float64
 	var cmd byte
@@ -74,6 +76,8 @@ func (p *PathData) ShortenPathData(b []byte) []byte {
 	return b[:j]
 }
 
+// copyInstruction copies pathdata of a single command, but may be comprised of multiple sets for that command. For example, L takes two coordinates, but this function may process 2*N coordinates. Lowercase commands are relative commands, where the coordinates are relative to the previous point. Uppercase commands have absolute coordinates.
+// We update p.x and p.y (the current coordinates) according to the commands given. For each set of coordinates we call shortenCurPosInstruction and shortenAltPosInstruction. The former just minifies the coordinates, the latter will inverse the lowercase/uppercase of the command, and see if the coordinates get smaller due to that. The shortest is chosen and copied to `b`.
 func (p *PathData) copyInstruction(b []byte, cmd byte) int {
 	n := len(p.coords)
 	if n == 0 {
@@ -191,6 +195,7 @@ func (p *PathData) copyInstruction(b []byte, cmd byte) int {
 	return j
 }
 
+// shortenCurPosInstruction only minifies the coordinates.
 func (p *PathData) shortenCurPosInstruction(cmd byte, coords [][]byte) PathDataState {
 	state := p.state
 	p.curBuffer = p.curBuffer[:0]
@@ -202,7 +207,8 @@ func (p *PathData) shortenCurPosInstruction(cmd byte, coords [][]byte) PathDataS
 	}
 	for i, coord := range coords {
 		isFlag := false
-		if (cmd == 'A' || cmd == 'a') && (i%7 == 3 || i%7 == 4) {
+		// Arc has boolean flags that can only be 0 or 1. Setting isFlag prevents from adding a dot before a zero (instead of a space). However, when the dot already was there, the command is malformed and could make the path longer than before, introducing bugs.
+		if (cmd == 'A' || cmd == 'a') && (i%7 == 3 || i%7 == 4) && coord[0] != '.' {
 			isFlag = true
 		}
 
@@ -212,6 +218,7 @@ func (p *PathData) shortenCurPosInstruction(cmd byte, coords [][]byte) PathDataS
 	return state
 }
 
+// shortenAltPosInstruction toggles the command between absolute / relative coordinates and minifies the coordinates.
 func (p *PathData) shortenAltPosInstruction(cmd byte, coordFloats []float64, x, y float64) PathDataState {
 	state := p.state
 	p.altBuffer = p.altBuffer[:0]
@@ -250,6 +257,7 @@ func (p *PathData) shortenAltPosInstruction(cmd byte, coordFloats []float64, x,
 	return state
 }
 
+// copyNumber will copy a number to the destination buffer, taking into account space or dot insertion to guarantee the shortest pathdata.
 func (state *PathDataState) copyNumber(buffer *[]byte, coord []byte, isFlag bool) {
 	if state.prevDigit && (coord[0] >= '0' && coord[0] <= '9' || coord[0] == '.' && state.prevDigitIsInt) {
 		if coord[0] == '0' && !state.prevDigitIsInt {

+ 8 - 3
vendor/github.com/tdewolff/minify/svg/pathdata_test.go

@@ -28,8 +28,9 @@ func TestPathData(t *testing.T) {
 
 		{"M.0.1", "M0 .1"},
 		{"M200.0.1", "M2e2.1"},
-		{"M0 0a3.28 3.28.0.0.0 3.279 3.28", "M0 0a3.28 3.28.0 0 0 3.279 3.28"}, // #114
-		{"A1.1.0.0.0.0.2.3", "A1.1.0.0 0 0 .2."},                               // bad input (sweep and large-arc are not booleans) gives bad output
+		{"M0 0a3.28 3.28.0.0.0 3.279 3.28", "M0 0a3.28 3.28.0.0.0 3.279 3.28"}, // #114
+		{"A1.1.0.0.0.0.2.3", "A1.1.0.0.0.0.2.3"},                               // bad input (sweep and large-arc are not booleans) gives bad output
+		{"A.0.0.4.0.0.0.3", "A0 0 .4.0.0.0.3"},                                 // bad input, keep dot for booleans
 
 		// fuzz
 		{"", ""},
@@ -37,7 +38,11 @@ func TestPathData(t *testing.T) {
 		{".8.00c0", ""},
 		{".1.04h0e6.0e6.0e0.0", "h0 0 0 0"},
 		{"M.1.0.0.2Z", "M.1.0.0.2z"},
-		{"A.0.0.0.0.3.2e3.7.0.0.0.0.0.1.3.0.0.0.0.2.3.2.0.0.0.0.20.2e-10.0.0.0.0.0.0.0.0", "A0 0 0 0 .3 2e2.7.0.0.0 0 0 .1.3 30 0 0 0 .2.3.2 3 20 0 0 .2 2e-1100 11 0 0 0 "}, // bad input (sweep and large-arc are not booleans) gives bad output
+		{"A.0.0.0.0.3.2e3.7.0.0.0.0.0.1.3.0.0.0.0.2.3.2.0.0.0.0.20.2e-10.0.0.0.0.0.0.0.0", "A0 0 0 0 .3 2e2.7.0.0.0.0.0.1.3.0.0.0.0.2.3.2.0.0.0.0.2 2e-11.0.0.0.0.0.0.0.0"}, // bad input (sweep and large-arc are not booleans) gives bad output
+		{
+			"A.0.0.4.0.0.0.3.0.0.0.0.0.4.2.0.0.0.0.2.0.4.0.0.0.4.2.8.2.0.0.0.2.9.28.0.0.0.0.0.2.3.0.0.0.0.0.0.2.3.2.09e-03.0.0.0.0.8.0.0.0.0.0.0.0",
+			"A0 0 .4.0.0.0.3.0.0.0.0.0.4.2.0.0.0.0.2.0.4.0.0.0.4.2.8.2.0.0.0.2.9.28.0.0.0.0.0.2.3.0.0.0.0.0.0.2.3.2 9e-5.0.0.0.0.8.0.0.0.0.0.0.0",
+		},
 	}
 
 	p := NewPathData(&Minifier{Decimals: -1})

+ 2 - 33
vendor/github.com/tdewolff/minify/svg/svg.go

@@ -51,7 +51,6 @@ func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, _ map[string]st
 	p := NewPathData(o)
 	minifyBuffer := buffer.NewWriter(make([]byte, 0, 64))
 	attrByteBuffer := make([]byte, 0, 64)
-	gStack := make([]bool, 0)
 
 	l := xml.NewLexer(r)
 	defer l.Restore()
@@ -59,7 +58,6 @@ func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, _ map[string]st
 	tb := NewTokenBuffer(l)
 	for {
 		t := *tb.Shift()
-	SWITCH:
 		switch t.TokenType {
 		case xml.ErrorToken:
 			if l.Err() == io.EOF {
@@ -113,29 +111,7 @@ func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, _ map[string]st
 			}
 		case xml.StartTagToken:
 			tag = t.Hash
-			if containerTagMap[tag] { // skip empty containers
-				i := 0
-				for {
-					next := tb.Peek(i)
-					i++
-					if next.TokenType == xml.EndTagToken && next.Hash == tag || next.TokenType == xml.StartTagCloseVoidToken || next.TokenType == xml.ErrorToken {
-						for j := 0; j < i; j++ {
-							tb.Shift()
-						}
-						break SWITCH
-					} else if next.TokenType != xml.AttributeToken && next.TokenType != xml.StartTagCloseToken {
-						break
-					}
-				}
-				if tag == svg.G {
-					if tb.Peek(0).TokenType == xml.StartTagCloseToken {
-						gStack = append(gStack, false)
-						tb.Shift()
-						break
-					}
-					gStack = append(gStack, true)
-				}
-			} else if tag == svg.Metadata {
+			if tag == svg.Metadata {
 				skipTag(tb, tag)
 				break
 			} else if tag == svg.Line {
@@ -184,7 +160,7 @@ func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, _ map[string]st
 			}
 
 			if tag == svg.Svg && attr == svg.ContentStyleType {
-				val = minify.ContentType(val)
+				val = minify.Mediatype(val)
 				defaultStyleType = val
 			} else if attr == svg.Style {
 				minifyBuffer.Reset()
@@ -266,13 +242,6 @@ func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, _ map[string]st
 			}
 		case xml.EndTagToken:
 			tag = 0
-			if t.Hash == svg.G && len(gStack) > 0 {
-				if !gStack[len(gStack)-1] {
-					gStack = gStack[:len(gStack)-1]
-					break
-				}
-				gStack = gStack[:len(gStack)-1]
-			}
 			if len(t.Data) > 3+len(t.Text) {
 				t.Data[2+len(t.Text)] = '>'
 				t.Data = t.Data[:3+len(t.Text)]

+ 3 - 3
vendor/github.com/tdewolff/minify/svg/svg_test.go

@@ -41,9 +41,9 @@ func TestSVG(t *testing.T) {
 		{`<path d="M20 20l-10-10z"/>`, `<path d="M20 20 10 10z"/>`},
 		{`<?xml version="1.0" encoding="utf-8"?>`, ``},
 		{`<svg viewbox="0 0 16 16"><path/></svg>`, `<svg viewbox="0 0 16 16"><path/></svg>`},
-		{`<g></g>`, ``},
-		{`<g><path/></g>`, `<path/>`},
-		{`<g id="a"><g><path/></g></g>`, `<g id="a"><path/></g>`},
+		{`<g></g>`, `<g/>`},
+		{`<g><path/></g>`, `<g><path/></g>`},
+		{`<g id="a"><g><path/></g></g>`, `<g id="a"><g><path/></g></g>`},
 		{`<path fill="#ffffff"/>`, `<path fill="#fff"/>`},
 		{`<path fill="#fff"/>`, `<path fill="#fff"/>`},
 		{`<path fill="white"/>`, `<path fill="#fff"/>`},

+ 0 - 12
vendor/github.com/tdewolff/minify/svg/table.go

@@ -2,18 +2,6 @@ package svg // import "github.com/tdewolff/minify/svg"
 
 import "github.com/tdewolff/parse/svg"
 
-var containerTagMap = map[svg.Hash]bool{
-	svg.A:             true,
-	svg.Defs:          true,
-	svg.G:             true,
-	svg.Marker:        true,
-	svg.Mask:          true,
-	svg.Missing_Glyph: true,
-	svg.Pattern:       true,
-	svg.Switch:        true,
-	svg.Symbol:        true,
-}
-
 var colorAttrMap = map[svg.Hash]bool{
 	svg.Color:          true,
 	svg.Fill:           true,

+ 610 - 597
vendor/github.com/tdewolff/parse/css/hash.go

@@ -10,291 +10,297 @@ type Hash uint32
 
 // Unique hash definitions to be used instead of strings
 const (
-	Accelerator                 Hash = 0x47f0b // accelerator
-	Aliceblue                   Hash = 0x52509 // aliceblue
-	Alpha                       Hash = 0x5af05 // alpha
-	Antiquewhite                Hash = 0x45c0c // antiquewhite
-	Aquamarine                  Hash = 0x7020a // aquamarine
-	Azimuth                     Hash = 0x5b307 // azimuth
+	Accelerator                 Hash = 0x4b30b // accelerator
+	Aliceblue                   Hash = 0x56109 // aliceblue
+	Alpha                       Hash = 0x5eb05 // alpha
+	Antiquewhite                Hash = 0x4900c // antiquewhite
+	Aquamarine                  Hash = 0x73a0a // aquamarine
+	Azimuth                     Hash = 0x5ef07 // azimuth
 	Background                  Hash = 0xa     // background
-	Background_Attachment       Hash = 0x3a15  // background-attachment
-	Background_Color            Hash = 0x11c10 // background-color
-	Background_Image            Hash = 0x99210 // background-image
+	Background_Attachment       Hash = 0x9115  // background-attachment
+	Background_Color            Hash = 0x11210 // background-color
+	Background_Image            Hash = 0x9ca10 // background-image
 	Background_Position         Hash = 0x13    // background-position
-	Background_Position_X       Hash = 0x80815 // background-position-x
+	Background_Position_X       Hash = 0x84015 // background-position-x
 	Background_Position_Y       Hash = 0x15    // background-position-y
 	Background_Repeat           Hash = 0x1511  // background-repeat
 	Behavior                    Hash = 0x3108  // behavior
-	Black                       Hash = 0x6005  // black
-	Blanchedalmond              Hash = 0x650e  // blanchedalmond
-	Blueviolet                  Hash = 0x52a0a // blueviolet
-	Bold                        Hash = 0x7a04  // bold
-	Border                      Hash = 0x8506  // border
-	Border_Bottom               Hash = 0x850d  // border-bottom
-	Border_Bottom_Color         Hash = 0x8513  // border-bottom-color
-	Border_Bottom_Style         Hash = 0xbe13  // border-bottom-style
-	Border_Bottom_Width         Hash = 0xe113  // border-bottom-width
-	Border_Collapse             Hash = 0x1020f // border-collapse
-	Border_Color                Hash = 0x1350c // border-color
-	Border_Left                 Hash = 0x15c0b // border-left
-	Border_Left_Color           Hash = 0x15c11 // border-left-color
-	Border_Left_Style           Hash = 0x17911 // border-left-style
-	Border_Left_Width           Hash = 0x18a11 // border-left-width
-	Border_Right                Hash = 0x19b0c // border-right
-	Border_Right_Color          Hash = 0x19b12 // border-right-color
-	Border_Right_Style          Hash = 0x1ad12 // border-right-style
-	Border_Right_Width          Hash = 0x1bf12 // border-right-width
-	Border_Spacing              Hash = 0x1d10e // border-spacing
-	Border_Style                Hash = 0x1f40c // border-style
-	Border_Top                  Hash = 0x2000a // border-top
-	Border_Top_Color            Hash = 0x20010 // border-top-color
-	Border_Top_Style            Hash = 0x21010 // border-top-style
-	Border_Top_Width            Hash = 0x22010 // border-top-width
-	Border_Width                Hash = 0x2300c // border-width
-	Bottom                      Hash = 0x8c06  // bottom
-	Burlywood                   Hash = 0x23c09 // burlywood
-	Cadetblue                   Hash = 0x25809 // cadetblue
-	Caption_Side                Hash = 0x2610c // caption-side
-	Charset                     Hash = 0x44207 // charset
-	Chartreuse                  Hash = 0x2730a // chartreuse
-	Chocolate                   Hash = 0x27d09 // chocolate
-	Clear                       Hash = 0x2ab05 // clear
-	Clip                        Hash = 0x2b004 // clip
-	Color                       Hash = 0x9305  // color
-	Content                     Hash = 0x2e507 // content
-	Cornflowerblue              Hash = 0x2ff0e // cornflowerblue
-	Cornsilk                    Hash = 0x30d08 // cornsilk
-	Counter_Increment           Hash = 0x31511 // counter-increment
-	Counter_Reset               Hash = 0x3540d // counter-reset
-	Cue                         Hash = 0x36103 // cue
-	Cue_After                   Hash = 0x36109 // cue-after
-	Cue_Before                  Hash = 0x36a0a // cue-before
-	Cursive                     Hash = 0x37b07 // cursive
-	Cursor                      Hash = 0x38e06 // cursor
-	Darkblue                    Hash = 0x7208  // darkblue
-	Darkcyan                    Hash = 0x7d08  // darkcyan
-	Darkgoldenrod               Hash = 0x2440d // darkgoldenrod
-	Darkgray                    Hash = 0x25008 // darkgray
-	Darkgreen                   Hash = 0x79209 // darkgreen
-	Darkkhaki                   Hash = 0x88509 // darkkhaki
-	Darkmagenta                 Hash = 0x4f40b // darkmagenta
-	Darkolivegreen              Hash = 0x7210e // darkolivegreen
-	Darkorange                  Hash = 0x7860a // darkorange
-	Darkorchid                  Hash = 0x87c0a // darkorchid
-	Darksalmon                  Hash = 0x8c00a // darksalmon
-	Darkseagreen                Hash = 0x9240c // darkseagreen
-	Darkslateblue               Hash = 0x3940d // darkslateblue
-	Darkslategray               Hash = 0x3a10d // darkslategray
-	Darkturquoise               Hash = 0x3ae0d // darkturquoise
-	Darkviolet                  Hash = 0x3bb0a // darkviolet
-	Deeppink                    Hash = 0x26b08 // deeppink
-	Deepskyblue                 Hash = 0x8930b // deepskyblue
-	Default                     Hash = 0x57b07 // default
-	Direction                   Hash = 0x9f109 // direction
-	Display                     Hash = 0x3c507 // display
-	Document                    Hash = 0x3d308 // document
-	Dodgerblue                  Hash = 0x3db0a // dodgerblue
-	Elevation                   Hash = 0x4a009 // elevation
-	Empty_Cells                 Hash = 0x4c20b // empty-cells
-	Fantasy                     Hash = 0x5ce07 // fantasy
-	Filter                      Hash = 0x59806 // filter
-	Firebrick                   Hash = 0x3e509 // firebrick
-	Float                       Hash = 0x3ee05 // float
-	Floralwhite                 Hash = 0x3f30b // floralwhite
-	Font                        Hash = 0xd804  // font
-	Font_Face                   Hash = 0xd809  // font-face
-	Font_Family                 Hash = 0x41d0b // font-family
-	Font_Size                   Hash = 0x42809 // font-size
-	Font_Size_Adjust            Hash = 0x42810 // font-size-adjust
-	Font_Stretch                Hash = 0x4380c // font-stretch
-	Font_Style                  Hash = 0x4490a // font-style
-	Font_Variant                Hash = 0x4530c // font-variant
-	Font_Weight                 Hash = 0x46e0b // font-weight
-	Forestgreen                 Hash = 0x3700b // forestgreen
-	Fuchsia                     Hash = 0x47907 // fuchsia
-	Gainsboro                   Hash = 0x14c09 // gainsboro
-	Ghostwhite                  Hash = 0x1de0a // ghostwhite
-	Goldenrod                   Hash = 0x24809 // goldenrod
-	Greenyellow                 Hash = 0x7960b // greenyellow
-	Height                      Hash = 0x68506 // height
-	Honeydew                    Hash = 0x5b908 // honeydew
-	Hsl                         Hash = 0xf303  // hsl
-	Hsla                        Hash = 0xf304  // hsla
-	Ime_Mode                    Hash = 0x88d08 // ime-mode
-	Import                      Hash = 0x4e306 // import
-	Important                   Hash = 0x4e309 // important
-	Include_Source              Hash = 0x7f20e // include-source
-	Indianred                   Hash = 0x4ec09 // indianred
-	Inherit                     Hash = 0x51907 // inherit
-	Initial                     Hash = 0x52007 // initial
-	Keyframes                   Hash = 0x40109 // keyframes
-	Lavender                    Hash = 0xf508  // lavender
-	Lavenderblush               Hash = 0xf50d  // lavenderblush
-	Lawngreen                   Hash = 0x4da09 // lawngreen
-	Layer_Background_Color      Hash = 0x11616 // layer-background-color
-	Layer_Background_Image      Hash = 0x98c16 // layer-background-image
-	Layout_Flow                 Hash = 0x5030b // layout-flow
-	Layout_Grid                 Hash = 0x53f0b // layout-grid
-	Layout_Grid_Char            Hash = 0x53f10 // layout-grid-char
-	Layout_Grid_Char_Spacing    Hash = 0x53f18 // layout-grid-char-spacing
-	Layout_Grid_Line            Hash = 0x55710 // layout-grid-line
-	Layout_Grid_Mode            Hash = 0x56d10 // layout-grid-mode
-	Layout_Grid_Type            Hash = 0x58210 // layout-grid-type
-	Left                        Hash = 0x16304 // left
-	Lemonchiffon                Hash = 0xcf0c  // lemonchiffon
-	Letter_Spacing              Hash = 0x5310e // letter-spacing
-	Lightblue                   Hash = 0x59e09 // lightblue
-	Lightcoral                  Hash = 0x5a70a // lightcoral
-	Lightcyan                   Hash = 0x5d509 // lightcyan
-	Lightgoldenrodyellow        Hash = 0x5de14 // lightgoldenrodyellow
-	Lightgray                   Hash = 0x60509 // lightgray
-	Lightgreen                  Hash = 0x60e0a // lightgreen
-	Lightpink                   Hash = 0x61809 // lightpink
-	Lightsalmon                 Hash = 0x6210b // lightsalmon
-	Lightseagreen               Hash = 0x62c0d // lightseagreen
-	Lightskyblue                Hash = 0x6390c // lightskyblue
-	Lightslateblue              Hash = 0x6450e // lightslateblue
-	Lightsteelblue              Hash = 0x6530e // lightsteelblue
-	Lightyellow                 Hash = 0x6610b // lightyellow
-	Limegreen                   Hash = 0x67709 // limegreen
-	Line_Break                  Hash = 0x5630a // line-break
-	Line_Height                 Hash = 0x6800b // line-height
-	List_Style                  Hash = 0x68b0a // list-style
-	List_Style_Image            Hash = 0x68b10 // list-style-image
-	List_Style_Position         Hash = 0x69b13 // list-style-position
-	List_Style_Type             Hash = 0x6ae0f // list-style-type
-	Magenta                     Hash = 0x4f807 // magenta
-	Margin                      Hash = 0x2c006 // margin
-	Margin_Bottom               Hash = 0x2c00d // margin-bottom
-	Margin_Left                 Hash = 0x2cc0b // margin-left
-	Margin_Right                Hash = 0x3320c // margin-right
-	Margin_Top                  Hash = 0x7cd0a // margin-top
-	Marker_Offset               Hash = 0x6bd0d // marker-offset
-	Marks                       Hash = 0x6ca05 // marks
-	Max_Height                  Hash = 0x6e90a // max-height
-	Max_Width                   Hash = 0x6f309 // max-width
-	Media                       Hash = 0xa1405 // media
-	Mediumaquamarine            Hash = 0x6fc10 // mediumaquamarine
-	Mediumblue                  Hash = 0x70c0a // mediumblue
-	Mediumorchid                Hash = 0x7160c // mediumorchid
-	Mediumpurple                Hash = 0x72f0c // mediumpurple
-	Mediumseagreen              Hash = 0x73b0e // mediumseagreen
-	Mediumslateblue             Hash = 0x7490f // mediumslateblue
-	Mediumspringgreen           Hash = 0x75811 // mediumspringgreen
-	Mediumturquoise             Hash = 0x7690f // mediumturquoise
-	Mediumvioletred             Hash = 0x7780f // mediumvioletred
-	Midnightblue                Hash = 0x7a60c // midnightblue
-	Min_Height                  Hash = 0x7b20a // min-height
-	Min_Width                   Hash = 0x7bc09 // min-width
-	Mintcream                   Hash = 0x7c509 // mintcream
-	Mistyrose                   Hash = 0x7e309 // mistyrose
-	Moccasin                    Hash = 0x7ec08 // moccasin
-	Monospace                   Hash = 0x8c709 // monospace
-	Namespace                   Hash = 0x49809 // namespace
-	Navajowhite                 Hash = 0x4a80b // navajowhite
-	None                        Hash = 0x4bf04 // none
-	Normal                      Hash = 0x4d506 // normal
-	Olivedrab                   Hash = 0x80009 // olivedrab
-	Orangered                   Hash = 0x78a09 // orangered
-	Orphans                     Hash = 0x48807 // orphans
-	Outline                     Hash = 0x81d07 // outline
-	Outline_Color               Hash = 0x81d0d // outline-color
-	Outline_Style               Hash = 0x82a0d // outline-style
-	Outline_Width               Hash = 0x8370d // outline-width
-	Overflow                    Hash = 0x2db08 // overflow
-	Overflow_X                  Hash = 0x2db0a // overflow-x
-	Overflow_Y                  Hash = 0x8440a // overflow-y
-	Padding                     Hash = 0x2b307 // padding
-	Padding_Bottom              Hash = 0x2b30e // padding-bottom
-	Padding_Left                Hash = 0x5f90c // padding-left
-	Padding_Right               Hash = 0x7d60d // padding-right
-	Padding_Top                 Hash = 0x8d90b // padding-top
-	Page                        Hash = 0x84e04 // page
-	Page_Break_After            Hash = 0x8e310 // page-break-after
-	Page_Break_Before           Hash = 0x84e11 // page-break-before
-	Page_Break_Inside           Hash = 0x85f11 // page-break-inside
-	Palegoldenrod               Hash = 0x8700d // palegoldenrod
-	Palegreen                   Hash = 0x89e09 // palegreen
-	Paleturquoise               Hash = 0x8a70d // paleturquoise
-	Palevioletred               Hash = 0x8b40d // palevioletred
-	Papayawhip                  Hash = 0x8d00a // papayawhip
-	Pause                       Hash = 0x8f305 // pause
-	Pause_After                 Hash = 0x8f30b // pause-after
-	Pause_Before                Hash = 0x8fe0c // pause-before
-	Peachpuff                   Hash = 0x59009 // peachpuff
-	Pitch                       Hash = 0x90a05 // pitch
-	Pitch_Range                 Hash = 0x90a0b // pitch-range
-	Play_During                 Hash = 0x3c80b // play-during
+	Black                       Hash = 0x5805  // black
+	Blanchedalmond              Hash = 0x5d0e  // blanchedalmond
+	Blueviolet                  Hash = 0x5660a // blueviolet
+	Bold                        Hash = 0x7204  // bold
+	Border                      Hash = 0x7d06  // border
+	Border_Bottom               Hash = 0x7d0d  // border-bottom
+	Border_Bottom_Color         Hash = 0x7d13  // border-bottom-color
+	Border_Bottom_Style         Hash = 0xb413  // border-bottom-style
+	Border_Bottom_Width         Hash = 0xd713  // border-bottom-width
+	Border_Collapse             Hash = 0xf80f  // border-collapse
+	Border_Color                Hash = 0x1480c // border-color
+	Border_Left                 Hash = 0x15d0b // border-left
+	Border_Left_Color           Hash = 0x15d11 // border-left-color
+	Border_Left_Style           Hash = 0x18911 // border-left-style
+	Border_Left_Width           Hash = 0x19a11 // border-left-width
+	Border_Right                Hash = 0x1ab0c // border-right
+	Border_Right_Color          Hash = 0x1ab12 // border-right-color
+	Border_Right_Style          Hash = 0x1c912 // border-right-style
+	Border_Right_Width          Hash = 0x1db12 // border-right-width
+	Border_Spacing              Hash = 0x1ed0e // border-spacing
+	Border_Style                Hash = 0x2100c // border-style
+	Border_Top                  Hash = 0x21c0a // border-top
+	Border_Top_Color            Hash = 0x21c10 // border-top-color
+	Border_Top_Style            Hash = 0x22c10 // border-top-style
+	Border_Top_Width            Hash = 0x23c10 // border-top-width
+	Border_Width                Hash = 0x24c0c // border-width
+	Bottom                      Hash = 0x8406  // bottom
+	Box_Shadow                  Hash = 0x2580a // box-shadow
+	Burlywood                   Hash = 0x26b09 // burlywood
+	Cadetblue                   Hash = 0x28a09 // cadetblue
+	Calc                        Hash = 0x28704 // calc
+	Caption_Side                Hash = 0x2930c // caption-side
+	Charset                     Hash = 0x47607 // charset
+	Chartreuse                  Hash = 0x2a50a // chartreuse
+	Chocolate                   Hash = 0x2af09 // chocolate
+	Clear                       Hash = 0x2dd05 // clear
+	Clip                        Hash = 0x2e204 // clip
+	Color                       Hash = 0x8b05  // color
+	Content                     Hash = 0x31e07 // content
+	Cornflowerblue              Hash = 0x3530e // cornflowerblue
+	Cornsilk                    Hash = 0x36108 // cornsilk
+	Counter_Increment           Hash = 0x36911 // counter-increment
+	Counter_Reset               Hash = 0x3840d // counter-reset
+	Cue                         Hash = 0x39103 // cue
+	Cue_After                   Hash = 0x39109 // cue-after
+	Cue_Before                  Hash = 0x39a0a // cue-before
+	Cursive                     Hash = 0x3ab07 // cursive
+	Cursor                      Hash = 0x3be06 // cursor
+	Darkblue                    Hash = 0x6a08  // darkblue
+	Darkcyan                    Hash = 0x7508  // darkcyan
+	Darkgoldenrod               Hash = 0x2730d // darkgoldenrod
+	Darkgray                    Hash = 0x27f08 // darkgray
+	Darkgreen                   Hash = 0x7ca09 // darkgreen
+	Darkkhaki                   Hash = 0x8bd09 // darkkhaki
+	Darkmagenta                 Hash = 0x5280b // darkmagenta
+	Darkolivegreen              Hash = 0x7590e // darkolivegreen
+	Darkorange                  Hash = 0x7be0a // darkorange
+	Darkorchid                  Hash = 0x8b40a // darkorchid
+	Darksalmon                  Hash = 0x8f80a // darksalmon
+	Darkseagreen                Hash = 0x95c0c // darkseagreen
+	Darkslateblue               Hash = 0x3c40d // darkslateblue
+	Darkslategray               Hash = 0x3d10d // darkslategray
+	Darkturquoise               Hash = 0x3de0d // darkturquoise
+	Darkviolet                  Hash = 0x3eb0a // darkviolet
+	Deeppink                    Hash = 0x29d08 // deeppink
+	Deepskyblue                 Hash = 0x8cb0b // deepskyblue
+	Default                     Hash = 0x5b707 // default
+	Direction                   Hash = 0xa2909 // direction
+	Display                     Hash = 0x3f507 // display
+	Document                    Hash = 0x40308 // document
+	Dodgerblue                  Hash = 0x40b0a // dodgerblue
+	Elevation                   Hash = 0x4d409 // elevation
+	Empty_Cells                 Hash = 0x4f60b // empty-cells
+	Fantasy                     Hash = 0x60a07 // fantasy
+	Filter                      Hash = 0x5d406 // filter
+	Firebrick                   Hash = 0x41509 // firebrick
+	Flex                        Hash = 0x41e04 // flex
+	Float                       Hash = 0x42205 // float
+	Floralwhite                 Hash = 0x4270b // floralwhite
+	Font                        Hash = 0xce04  // font
+	Font_Face                   Hash = 0xce09  // font-face
+	Font_Family                 Hash = 0x4510b // font-family
+	Font_Size                   Hash = 0x45c09 // font-size
+	Font_Size_Adjust            Hash = 0x45c10 // font-size-adjust
+	Font_Stretch                Hash = 0x46c0c // font-stretch
+	Font_Style                  Hash = 0x47d0a // font-style
+	Font_Variant                Hash = 0x4870c // font-variant
+	Font_Weight                 Hash = 0x4a20b // font-weight
+	Forestgreen                 Hash = 0x3a00b // forestgreen
+	Fuchsia                     Hash = 0x4ad07 // fuchsia
+	Gainsboro                   Hash = 0x17909 // gainsboro
+	Ghostwhite                  Hash = 0x1fa0a // ghostwhite
+	Goldenrod                   Hash = 0x27709 // goldenrod
+	Greenyellow                 Hash = 0x7ce0b // greenyellow
+	Height                      Hash = 0x6ae06 // height
+	Honeydew                    Hash = 0x5f508 // honeydew
+	Hsl                         Hash = 0xe903  // hsl
+	Hsla                        Hash = 0xe904  // hsla
+	Ime_Mode                    Hash = 0x8c508 // ime-mode
+	Import                      Hash = 0x51706 // import
+	Important                   Hash = 0x51709 // important
+	Include_Source              Hash = 0x82a0e // include-source
+	Indianred                   Hash = 0x52009 // indianred
+	Inherit                     Hash = 0x55507 // inherit
+	Initial                     Hash = 0x55c07 // initial
+	Keyframes                   Hash = 0x43509 // keyframes
+	Lavender                    Hash = 0xeb08  // lavender
+	Lavenderblush               Hash = 0xeb0d  // lavenderblush
+	Lawngreen                   Hash = 0x50e09 // lawngreen
+	Layer_Background_Color      Hash = 0x10c16 // layer-background-color
+	Layer_Background_Image      Hash = 0x9c416 // layer-background-image
+	Layout_Flow                 Hash = 0x5370b // layout-flow
+	Layout_Grid                 Hash = 0x57b0b // layout-grid
+	Layout_Grid_Char            Hash = 0x57b10 // layout-grid-char
+	Layout_Grid_Char_Spacing    Hash = 0x57b18 // layout-grid-char-spacing
+	Layout_Grid_Line            Hash = 0x59310 // layout-grid-line
+	Layout_Grid_Mode            Hash = 0x5a910 // layout-grid-mode
+	Layout_Grid_Type            Hash = 0x5be10 // layout-grid-type
+	Left                        Hash = 0x16404 // left
+	Lemonchiffon                Hash = 0xc50c  // lemonchiffon
+	Letter_Spacing              Hash = 0x56d0e // letter-spacing
+	Lightblue                   Hash = 0x5da09 // lightblue
+	Lightcoral                  Hash = 0x5e30a // lightcoral
+	Lightcyan                   Hash = 0x61109 // lightcyan
+	Lightgoldenrodyellow        Hash = 0x61a14 // lightgoldenrodyellow
+	Lightgray                   Hash = 0x63909 // lightgray
+	Lightgreen                  Hash = 0x6420a // lightgreen
+	Lightpink                   Hash = 0x64c09 // lightpink
+	Lightsalmon                 Hash = 0x6550b // lightsalmon
+	Lightseagreen               Hash = 0x6600d // lightseagreen
+	Lightskyblue                Hash = 0x66d0c // lightskyblue
+	Lightslateblue              Hash = 0x6790e // lightslateblue
+	Lightsteelblue              Hash = 0x6870e // lightsteelblue
+	Lightyellow                 Hash = 0x6950b // lightyellow
+	Limegreen                   Hash = 0x6a009 // limegreen
+	Line_Break                  Hash = 0x59f0a // line-break
+	Line_Height                 Hash = 0x6a90b // line-height
+	Linear_Gradient             Hash = 0x6b40f // linear-gradient
+	List_Style                  Hash = 0x6c30a // list-style
+	List_Style_Image            Hash = 0x6c310 // list-style-image
+	List_Style_Position         Hash = 0x6d313 // list-style-position
+	List_Style_Type             Hash = 0x6e60f // list-style-type
+	Magenta                     Hash = 0x52c07 // magenta
+	Margin                      Hash = 0x2f206 // margin
+	Margin_Bottom               Hash = 0x2f20d // margin-bottom
+	Margin_Left                 Hash = 0x2fe0b // margin-left
+	Margin_Right                Hash = 0x3310c // margin-right
+	Margin_Top                  Hash = 0x8050a // margin-top
+	Marker_Offset               Hash = 0x6f50d // marker-offset
+	Marks                       Hash = 0x70205 // marks
+	Max_Height                  Hash = 0x7210a // max-height
+	Max_Width                   Hash = 0x72b09 // max-width
+	Media                       Hash = 0xa4c05 // media
+	Mediumaquamarine            Hash = 0x73410 // mediumaquamarine
+	Mediumblue                  Hash = 0x7440a // mediumblue
+	Mediumorchid                Hash = 0x74e0c // mediumorchid
+	Mediumpurple                Hash = 0x7670c // mediumpurple
+	Mediumseagreen              Hash = 0x7730e // mediumseagreen
+	Mediumslateblue             Hash = 0x7810f // mediumslateblue
+	Mediumspringgreen           Hash = 0x79011 // mediumspringgreen
+	Mediumturquoise             Hash = 0x7a10f // mediumturquoise
+	Mediumvioletred             Hash = 0x7b00f // mediumvioletred
+	Midnightblue                Hash = 0x7de0c // midnightblue
+	Min_Height                  Hash = 0x7ea0a // min-height
+	Min_Width                   Hash = 0x7f409 // min-width
+	Mintcream                   Hash = 0x7fd09 // mintcream
+	Mistyrose                   Hash = 0x81b09 // mistyrose
+	Moccasin                    Hash = 0x82408 // moccasin
+	Monospace                   Hash = 0x8ff09 // monospace
+	Namespace                   Hash = 0x4cc09 // namespace
+	Navajowhite                 Hash = 0x4dc0b // navajowhite
+	None                        Hash = 0x4f304 // none
+	Normal                      Hash = 0x50906 // normal
+	Olivedrab                   Hash = 0x83809 // olivedrab
+	Orangered                   Hash = 0x7c209 // orangered
+	Orphans                     Hash = 0x4bc07 // orphans
+	Outline                     Hash = 0x85507 // outline
+	Outline_Color               Hash = 0x8550d // outline-color
+	Outline_Style               Hash = 0x8620d // outline-style
+	Outline_Width               Hash = 0x86f0d // outline-width
+	Overflow                    Hash = 0xaa08  // overflow
+	Overflow_X                  Hash = 0xaa0a  // overflow-x
+	Overflow_Y                  Hash = 0x87c0a // overflow-y
+	Padding                     Hash = 0x2e507 // padding
+	Padding_Bottom              Hash = 0x2e50e // padding-bottom
+	Padding_Left                Hash = 0x5490c // padding-left
+	Padding_Right               Hash = 0x80e0d // padding-right
+	Padding_Top                 Hash = 0x9110b // padding-top
+	Page                        Hash = 0x88604 // page
+	Page_Break_After            Hash = 0x91b10 // page-break-after
+	Page_Break_Before           Hash = 0x88611 // page-break-before
+	Page_Break_Inside           Hash = 0x89711 // page-break-inside
+	Palegoldenrod               Hash = 0x8a80d // palegoldenrod
+	Palegreen                   Hash = 0x8d609 // palegreen
+	Paleturquoise               Hash = 0x8df0d // paleturquoise
+	Palevioletred               Hash = 0x8ec0d // palevioletred
+	Papayawhip                  Hash = 0x9080a // papayawhip
+	Pause                       Hash = 0x92b05 // pause
+	Pause_After                 Hash = 0x92b0b // pause-after
+	Pause_Before                Hash = 0x9360c // pause-before
+	Peachpuff                   Hash = 0x5cc09 // peachpuff
+	Pitch                       Hash = 0x94205 // pitch
+	Pitch_Range                 Hash = 0x9420b // pitch-range
+	Play_During                 Hash = 0x3f80b // play-during
 	Position                    Hash = 0xb08   // position
-	Powderblue                  Hash = 0x9150a // powderblue
-	Progid                      Hash = 0x91f06 // progid
-	Quotes                      Hash = 0x93006 // quotes
-	Rgb                         Hash = 0x3803  // rgb
-	Rgba                        Hash = 0x3804  // rgba
-	Richness                    Hash = 0x9708  // richness
-	Right                       Hash = 0x1a205 // right
-	Rosybrown                   Hash = 0x15309 // rosybrown
-	Royalblue                   Hash = 0xb509  // royalblue
-	Ruby_Align                  Hash = 0x12b0a // ruby-align
-	Ruby_Overhang               Hash = 0x1400d // ruby-overhang
-	Ruby_Position               Hash = 0x16c0d // ruby-position
-	Saddlebrown                 Hash = 0x48e0b // saddlebrown
-	Sandybrown                  Hash = 0x4cc0a // sandybrown
-	Sans_Serif                  Hash = 0x5c50a // sans-serif
-	Scrollbar_3d_Light_Color    Hash = 0x9e18  // scrollbar-3d-light-color
-	Scrollbar_Arrow_Color       Hash = 0x29615 // scrollbar-arrow-color
-	Scrollbar_Base_Color        Hash = 0x40914 // scrollbar-base-color
-	Scrollbar_Dark_Shadow_Color Hash = 0x6ce1b // scrollbar-dark-shadow-color
-	Scrollbar_Face_Color        Hash = 0x93514 // scrollbar-face-color
-	Scrollbar_Highlight_Color   Hash = 0x9ce19 // scrollbar-highlight-color
-	Scrollbar_Shadow_Color      Hash = 0x94916 // scrollbar-shadow-color
-	Scrollbar_Track_Color       Hash = 0x95f15 // scrollbar-track-color
-	Seagreen                    Hash = 0x63108 // seagreen
-	Seashell                    Hash = 0x10f08 // seashell
-	Serif                       Hash = 0x5ca05 // serif
-	Size                        Hash = 0x42d04 // size
-	Slateblue                   Hash = 0x39809 // slateblue
-	Slategray                   Hash = 0x3a509 // slategray
-	Speak                       Hash = 0x97405 // speak
-	Speak_Header                Hash = 0x9740c // speak-header
-	Speak_Numeral               Hash = 0x9800d // speak-numeral
-	Speak_Punctuation           Hash = 0x9a211 // speak-punctuation
-	Speech_Rate                 Hash = 0x9b30b // speech-rate
-	Springgreen                 Hash = 0x75e0b // springgreen
-	Steelblue                   Hash = 0x65809 // steelblue
-	Stress                      Hash = 0x29106 // stress
-	Supports                    Hash = 0x9c708 // supports
-	Table_Layout                Hash = 0x4fd0c // table-layout
-	Text_Align                  Hash = 0x2840a // text-align
-	Text_Align_Last             Hash = 0x2840f // text-align-last
-	Text_Autospace              Hash = 0x1e60e // text-autospace
-	Text_Decoration             Hash = 0x4b10f // text-decoration
-	Text_Indent                 Hash = 0x9bc0b // text-indent
+	Powderblue                  Hash = 0x94d0a // powderblue
+	Progid                      Hash = 0x95706 // progid
+	Quotes                      Hash = 0x96806 // quotes
+	Radial_Gradient             Hash = 0x380f  // radial-gradient
+	Rgb                         Hash = 0x8f03  // rgb
+	Rgba                        Hash = 0x8f04  // rgba
+	Richness                    Hash = 0x12108 // richness
+	Right                       Hash = 0x1b205 // right
+	Rosybrown                   Hash = 0x18009 // rosybrown
+	Royalblue                   Hash = 0x13f09 // royalblue
+	Ruby_Align                  Hash = 0x1530a // ruby-align
+	Ruby_Overhang               Hash = 0x16d0d // ruby-overhang
+	Ruby_Position               Hash = 0x1bc0d // ruby-position
+	Saddlebrown                 Hash = 0x4c20b // saddlebrown
+	Sandybrown                  Hash = 0x5000a // sandybrown
+	Sans_Serif                  Hash = 0x6010a // sans-serif
+	Scrollbar_3d_Light_Color    Hash = 0x12818 // scrollbar-3d-light-color
+	Scrollbar_Arrow_Color       Hash = 0x2c815 // scrollbar-arrow-color
+	Scrollbar_Base_Color        Hash = 0x43d14 // scrollbar-base-color
+	Scrollbar_Dark_Shadow_Color Hash = 0x7061b // scrollbar-dark-shadow-color
+	Scrollbar_Face_Color        Hash = 0x96d14 // scrollbar-face-color
+	Scrollbar_Highlight_Color   Hash = 0xa0619 // scrollbar-highlight-color
+	Scrollbar_Shadow_Color      Hash = 0x98116 // scrollbar-shadow-color
+	Scrollbar_Track_Color       Hash = 0x99715 // scrollbar-track-color
+	Seagreen                    Hash = 0x66508 // seagreen
+	Seashell                    Hash = 0x10508 // seashell
+	Serif                       Hash = 0x60605 // serif
+	Size                        Hash = 0x46104 // size
+	Slateblue                   Hash = 0x3c809 // slateblue
+	Slategray                   Hash = 0x3d509 // slategray
+	Speak                       Hash = 0x9ac05 // speak
+	Speak_Header                Hash = 0x9ac0c // speak-header
+	Speak_Numeral               Hash = 0x9b80d // speak-numeral
+	Speak_Punctuation           Hash = 0x9da11 // speak-punctuation
+	Speech_Rate                 Hash = 0x9eb0b // speech-rate
+	Springgreen                 Hash = 0x7960b // springgreen
+	Steelblue                   Hash = 0x68c09 // steelblue
+	Stress                      Hash = 0x2c306 // stress
+	Supports                    Hash = 0x9ff08 // supports
+	Table_Layout                Hash = 0x5310c // table-layout
+	Text_Align                  Hash = 0x2b60a // text-align
+	Text_Align_Last             Hash = 0x2b60f // text-align-last
+	Text_Autospace              Hash = 0x2020e // text-autospace
+	Text_Decoration             Hash = 0x4e50f // text-decoration
+	Text_Indent                 Hash = 0x9f40b // text-indent
 	Text_Justify                Hash = 0x250c  // text-justify
-	Text_Kashida_Space          Hash = 0x4e12  // text-kashida-space
-	Text_Overflow               Hash = 0x2d60d // text-overflow
-	Text_Shadow                 Hash = 0x2eb0b // text-shadow
-	Text_Transform              Hash = 0x3250e // text-transform
-	Text_Underline_Position     Hash = 0x33d17 // text-underline-position
-	Top                         Hash = 0x20703 // top
-	Turquoise                   Hash = 0x3b209 // turquoise
-	Unicode_Bidi                Hash = 0x9e70c // unicode-bidi
-	Vertical_Align              Hash = 0x3800e // vertical-align
-	Visibility                  Hash = 0x9fa0a // visibility
-	Voice_Family                Hash = 0xa040c // voice-family
-	Volume                      Hash = 0xa1006 // volume
-	White                       Hash = 0x1e305 // white
-	White_Space                 Hash = 0x4630b // white-space
-	Whitesmoke                  Hash = 0x3f90a // whitesmoke
-	Widows                      Hash = 0x5c006 // widows
-	Width                       Hash = 0xef05  // width
-	Word_Break                  Hash = 0x2f50a // word-break
-	Word_Spacing                Hash = 0x50d0c // word-spacing
-	Word_Wrap                   Hash = 0x5f109 // word-wrap
-	Writing_Mode                Hash = 0x66b0c // writing-mode
-	Yellow                      Hash = 0x5ec06 // yellow
-	Yellowgreen                 Hash = 0x79b0b // yellowgreen
-	Z_Index                     Hash = 0xa1907 // z-index
+	Text_Kashida_Space          Hash = 0x4612  // text-kashida-space
+	Text_Overflow               Hash = 0xa50d  // text-overflow
+	Text_Shadow                 Hash = 0x3080b // text-shadow
+	Text_Transform              Hash = 0x3240e // text-transform
+	Text_Underline_Position     Hash = 0x33c17 // text-underline-position
+	Top                         Hash = 0x22303 // top
+	Transparent                 Hash = 0x3790b // transparent
+	Turquoise                   Hash = 0x3e209 // turquoise
+	Unicode_Bidi                Hash = 0xa1f0c // unicode-bidi
+	Vertical_Align              Hash = 0x3b00e // vertical-align
+	Visibility                  Hash = 0xa320a // visibility
+	Voice_Family                Hash = 0xa3c0c // voice-family
+	Volume                      Hash = 0xa4806 // volume
+	White                       Hash = 0x1ff05 // white
+	White_Space                 Hash = 0x4970b // white-space
+	Whitesmoke                  Hash = 0x42d0a // whitesmoke
+	Widows                      Hash = 0x5fc06 // widows
+	Width                       Hash = 0xe505  // width
+	Word_Break                  Hash = 0x2610a // word-break
+	Word_Spacing                Hash = 0x3120c // word-spacing
+	Word_Wrap                   Hash = 0x54109 // word-wrap
+	Writing_Mode                Hash = 0x62d0c // writing-mode
+	Yellow                      Hash = 0x62806 // yellow
+	Yellowgreen                 Hash = 0x7d30b // yellowgreen
+	Z_Index                     Hash = 0xa5107 // z-index
 )
 
 // String returns the hash' name.
@@ -342,335 +348,342 @@ NEXT:
 
 const _Hash_hash0 = 0x700e0976
 const _Hash_maxLen = 27
-const _Hash_text = "background-position-ybackground-repeatext-justifybehaviorgba" +
-	"ckground-attachmentext-kashida-spaceblackblanchedalmondarkbl" +
-	"ueboldarkcyanborder-bottom-colorichnesscrollbar-3d-light-col" +
-	"oroyalblueborder-bottom-stylemonchiffont-faceborder-bottom-w" +
-	"idthslavenderblushborder-collapseashellayer-background-color" +
-	"uby-alignborder-coloruby-overhangainsborosybrownborder-left-" +
-	"coloruby-positionborder-left-styleborder-left-widthborder-ri" +
-	"ght-colorborder-right-styleborder-right-widthborder-spacingh" +
-	"ostwhitext-autospaceborder-styleborder-top-colorborder-top-s" +
-	"tyleborder-top-widthborder-widthburlywoodarkgoldenrodarkgray" +
-	"cadetbluecaption-sideeppinkchartreusechocolatext-align-lastr" +
-	"esscrollbar-arrow-colorclearclipadding-bottomargin-bottomarg" +
-	"in-leftext-overflow-xcontentext-shadoword-breakcornflowerblu" +
-	"ecornsilkcounter-incrementext-transformargin-rightext-underl" +
-	"ine-positioncounter-resetcue-aftercue-beforestgreencursivert" +
-	"ical-aligncursordarkslatebluedarkslategraydarkturquoisedarkv" +
-	"ioletdisplay-duringdocumentdodgerbluefirebrickfloatfloralwhi" +
-	"tesmokeyframescrollbar-base-colorfont-familyfont-size-adjust" +
-	"font-stretcharsetfont-stylefont-variantiquewhite-spacefont-w" +
-	"eightfuchsiacceleratorphansaddlebrownamespacelevationavajowh" +
-	"itext-decorationonempty-cellsandybrownormalawngreenimportant" +
-	"indianredarkmagentable-layout-floword-spacinginheritinitiali" +
+const _Hash_text = "background-position-ybackground-repeatext-justifybehavioradi" +
+	"al-gradientext-kashida-spaceblackblanchedalmondarkblueboldar" +
+	"kcyanborder-bottom-colorgbackground-attachmentext-overflow-x" +
+	"border-bottom-stylemonchiffont-faceborder-bottom-widthslaven" +
+	"derblushborder-collapseashellayer-background-colorichnesscro" +
+	"llbar-3d-light-coloroyalblueborder-coloruby-alignborder-left" +
+	"-coloruby-overhangainsborosybrownborder-left-styleborder-lef" +
+	"t-widthborder-right-coloruby-positionborder-right-styleborde" +
+	"r-right-widthborder-spacinghostwhitext-autospaceborder-style" +
+	"border-top-colorborder-top-styleborder-top-widthborder-width" +
+	"box-shadoword-breakburlywoodarkgoldenrodarkgraycalcadetbluec" +
+	"aption-sideeppinkchartreusechocolatext-align-lastresscrollba" +
+	"r-arrow-colorclearclipadding-bottomargin-bottomargin-leftext" +
+	"-shadoword-spacingcontentext-transformargin-rightext-underli" +
+	"ne-positioncornflowerbluecornsilkcounter-incrementransparent" +
+	"counter-resetcue-aftercue-beforestgreencursivertical-aligncu" +
+	"rsordarkslatebluedarkslategraydarkturquoisedarkvioletdisplay" +
+	"-duringdocumentdodgerbluefirebrickflexfloatfloralwhitesmokey" +
+	"framescrollbar-base-colorfont-familyfont-size-adjustfont-str" +
+	"etcharsetfont-stylefont-variantiquewhite-spacefont-weightfuc" +
+	"hsiacceleratorphansaddlebrownamespacelevationavajowhitext-de" +
+	"corationonempty-cellsandybrownormalawngreenimportantindianre" +
+	"darkmagentable-layout-floword-wrapadding-leftinheritinitiali" +
 	"cebluevioletter-spacinglayout-grid-char-spacinglayout-grid-l" +
 	"ine-breaklayout-grid-modefaultlayout-grid-typeachpuffilterli" +
 	"ghtbluelightcoralphazimuthoneydewidowsans-serifantasylightcy" +
-	"anlightgoldenrodyelloword-wrapadding-leftlightgraylightgreen" +
-	"lightpinklightsalmonlightseagreenlightskybluelightslatebluel" +
-	"ightsteelbluelightyellowriting-modelimegreenline-heightlist-" +
-	"style-imagelist-style-positionlist-style-typemarker-offsetma" +
-	"rkscrollbar-dark-shadow-colormax-heightmax-widthmediumaquama" +
-	"rinemediumbluemediumorchidarkolivegreenmediumpurplemediumsea" +
-	"greenmediumslatebluemediumspringgreenmediumturquoisemediumvi" +
-	"oletredarkorangeredarkgreenyellowgreenmidnightbluemin-height" +
-	"min-widthmintcreamargin-topadding-rightmistyrosemoccasinclud" +
-	"e-sourceolivedrabackground-position-xoutline-coloroutline-st" +
-	"yleoutline-widthoverflow-ypage-break-beforepage-break-inside" +
-	"palegoldenrodarkorchidarkkhakime-modeepskybluepalegreenpalet" +
-	"urquoisepalevioletredarksalmonospacepapayawhipadding-topage-" +
-	"break-afterpause-afterpause-beforepitch-rangepowderblueprogi" +
-	"darkseagreenquotescrollbar-face-colorscrollbar-shadow-colors" +
-	"crollbar-track-colorspeak-headerspeak-numeralayer-background" +
-	"-imagespeak-punctuationspeech-ratext-indentsupportscrollbar-" +
-	"highlight-colorunicode-bidirectionvisibilityvoice-familyvolu" +
-	"mediaz-index"
+	"anlightgoldenrodyellowriting-modelightgraylightgreenlightpin" +
+	"klightsalmonlightseagreenlightskybluelightslatebluelightstee" +
+	"lbluelightyellowlimegreenline-heightlinear-gradientlist-styl" +
+	"e-imagelist-style-positionlist-style-typemarker-offsetmarksc" +
+	"rollbar-dark-shadow-colormax-heightmax-widthmediumaquamarine" +
+	"mediumbluemediumorchidarkolivegreenmediumpurplemediumseagree" +
+	"nmediumslatebluemediumspringgreenmediumturquoisemediumviolet" +
+	"redarkorangeredarkgreenyellowgreenmidnightbluemin-heightmin-" +
+	"widthmintcreamargin-topadding-rightmistyrosemoccasinclude-so" +
+	"urceolivedrabackground-position-xoutline-coloroutline-styleo" +
+	"utline-widthoverflow-ypage-break-beforepage-break-insidepale" +
+	"goldenrodarkorchidarkkhakime-modeepskybluepalegreenpaleturqu" +
+	"oisepalevioletredarksalmonospacepapayawhipadding-topage-brea" +
+	"k-afterpause-afterpause-beforepitch-rangepowderblueprogidark" +
+	"seagreenquotescrollbar-face-colorscrollbar-shadow-colorscrol" +
+	"lbar-track-colorspeak-headerspeak-numeralayer-background-ima" +
+	"gespeak-punctuationspeech-ratext-indentsupportscrollbar-high" +
+	"light-colorunicode-bidirectionvisibilityvoice-familyvolumedi" +
+	"az-index"
 
 var _Hash_table = [1 << 9]Hash{
-	0x0:   0x4cc0a, // sandybrown
-	0x1:   0x20703, // top
-	0x4:   0xb509,  // royalblue
-	0x6:   0x4b10f, // text-decoration
-	0xb:   0x5030b, // layout-flow
-	0xc:   0x11c10, // background-color
-	0xd:   0x8c06,  // bottom
-	0x10:  0x62c0d, // lightseagreen
-	0x11:  0x8930b, // deepskyblue
-	0x12:  0x39809, // slateblue
-	0x13:  0x4c20b, // empty-cells
-	0x14:  0x2b004, // clip
-	0x15:  0x70c0a, // mediumblue
-	0x16:  0x49809, // namespace
-	0x18:  0x2c00d, // margin-bottom
-	0x1a:  0x1350c, // border-color
-	0x1b:  0x5b908, // honeydew
-	0x1d:  0x2300c, // border-width
-	0x1e:  0x9740c, // speak-header
-	0x1f:  0x8b40d, // palevioletred
-	0x20:  0x1d10e, // border-spacing
-	0x22:  0x2b307, // padding
-	0x23:  0x3320c, // margin-right
-	0x27:  0x7bc09, // min-width
-	0x29:  0x60509, // lightgray
-	0x2a:  0x6610b, // lightyellow
-	0x2c:  0x8e310, // page-break-after
-	0x2d:  0x2e507, // content
+	0x0:   0x5000a, // sandybrown
+	0x1:   0x22303, // top
+	0x4:   0x13f09, // royalblue
+	0x6:   0x4e50f, // text-decoration
+	0xb:   0x5370b, // layout-flow
+	0xc:   0x11210, // background-color
+	0xd:   0x8406,  // bottom
+	0x10:  0x6600d, // lightseagreen
+	0x11:  0x8cb0b, // deepskyblue
+	0x12:  0x3c809, // slateblue
+	0x13:  0x4f60b, // empty-cells
+	0x14:  0x2e204, // clip
+	0x15:  0x7440a, // mediumblue
+	0x16:  0x4cc09, // namespace
+	0x18:  0x2f20d, // margin-bottom
+	0x1a:  0x1480c, // border-color
+	0x1b:  0x5f508, // honeydew
+	0x1d:  0x24c0c, // border-width
+	0x1e:  0x9ac0c, // speak-header
+	0x1f:  0x8ec0d, // palevioletred
+	0x20:  0x1ed0e, // border-spacing
+	0x22:  0x2e507, // padding
+	0x23:  0x3310c, // margin-right
+	0x27:  0x7f409, // min-width
+	0x29:  0x8f03,  // rgb
+	0x2a:  0x6950b, // lightyellow
+	0x2c:  0x91b10, // page-break-after
+	0x2d:  0x31e07, // content
 	0x30:  0x250c,  // text-justify
-	0x32:  0x2840f, // text-align-last
-	0x34:  0x93514, // scrollbar-face-color
-	0x35:  0x40109, // keyframes
-	0x37:  0x4f807, // magenta
-	0x38:  0x3a509, // slategray
-	0x3a:  0x99210, // background-image
-	0x3c:  0x7f20e, // include-source
-	0x3d:  0x65809, // steelblue
-	0x3e:  0x81d0d, // outline-color
-	0x40:  0x1020f, // border-collapse
-	0x41:  0xf508,  // lavender
-	0x42:  0x9c708, // supports
-	0x44:  0x6800b, // line-height
-	0x45:  0x9a211, // speak-punctuation
-	0x46:  0x9fa0a, // visibility
-	0x47:  0x2ab05, // clear
-	0x4b:  0x52a0a, // blueviolet
-	0x4e:  0x57b07, // default
-	0x50:  0x6bd0d, // marker-offset
-	0x52:  0x31511, // counter-increment
-	0x53:  0x6450e, // lightslateblue
-	0x54:  0x10f08, // seashell
-	0x56:  0x16c0d, // ruby-position
-	0x57:  0x82a0d, // outline-style
-	0x58:  0x63108, // seagreen
-	0x59:  0x9305,  // color
-	0x5c:  0x2610c, // caption-side
-	0x5d:  0x68506, // height
-	0x5e:  0x7490f, // mediumslateblue
-	0x5f:  0x8fe0c, // pause-before
-	0x60:  0xcf0c,  // lemonchiffon
-	0x63:  0x37b07, // cursive
-	0x66:  0x4a80b, // navajowhite
-	0x67:  0xa040c, // voice-family
-	0x68:  0x2440d, // darkgoldenrod
-	0x69:  0x3e509, // firebrick
-	0x6a:  0x4490a, // font-style
-	0x6b:  0x9f109, // direction
-	0x6d:  0x7860a, // darkorange
-	0x6f:  0x4530c, // font-variant
-	0x70:  0x2c006, // margin
-	0x71:  0x84e11, // page-break-before
-	0x73:  0x2d60d, // text-overflow
-	0x74:  0x4e12,  // text-kashida-space
-	0x75:  0x30d08, // cornsilk
-	0x76:  0x46e0b, // font-weight
-	0x77:  0x42d04, // size
-	0x78:  0x53f0b, // layout-grid
-	0x79:  0x8d90b, // padding-top
-	0x7a:  0x44207, // charset
-	0x7d:  0x7e309, // mistyrose
-	0x7e:  0x5b307, // azimuth
-	0x7f:  0x8f30b, // pause-after
-	0x84:  0x38e06, // cursor
-	0x85:  0xf303,  // hsl
-	0x86:  0x5310e, // letter-spacing
-	0x8b:  0x3d308, // document
-	0x8d:  0x36109, // cue-after
-	0x8f:  0x36a0a, // cue-before
-	0x91:  0x5ce07, // fantasy
-	0x94:  0x1400d, // ruby-overhang
-	0x95:  0x2b30e, // padding-bottom
-	0x9a:  0x59e09, // lightblue
-	0x9c:  0x8c00a, // darksalmon
-	0x9d:  0x42810, // font-size-adjust
-	0x9e:  0x61809, // lightpink
-	0xa0:  0x9240c, // darkseagreen
-	0xa2:  0x85f11, // page-break-inside
-	0xa4:  0x24809, // goldenrod
-	0xa6:  0xa1405, // media
-	0xa7:  0x53f18, // layout-grid-char-spacing
-	0xa9:  0x4e309, // important
-	0xaa:  0x7b20a, // min-height
-	0xb0:  0x15c11, // border-left-color
-	0xb1:  0x84e04, // page
-	0xb2:  0x98c16, // layer-background-image
-	0xb5:  0x55710, // layout-grid-line
+	0x32:  0x2b60f, // text-align-last
+	0x34:  0x96d14, // scrollbar-face-color
+	0x35:  0x43509, // keyframes
+	0x36:  0x27f08, // darkgray
+	0x37:  0x52c07, // magenta
+	0x38:  0x3d509, // slategray
+	0x3a:  0x9ca10, // background-image
+	0x3c:  0x82a0e, // include-source
+	0x3d:  0x68c09, // steelblue
+	0x3e:  0x8550d, // outline-color
+	0x40:  0xf80f,  // border-collapse
+	0x41:  0xeb08,  // lavender
+	0x42:  0x9ff08, // supports
+	0x44:  0x6a90b, // line-height
+	0x45:  0x9da11, // speak-punctuation
+	0x46:  0xa320a, // visibility
+	0x47:  0x2dd05, // clear
+	0x4b:  0x5660a, // blueviolet
+	0x4e:  0x5b707, // default
+	0x50:  0x6f50d, // marker-offset
+	0x52:  0x36911, // counter-increment
+	0x53:  0x6790e, // lightslateblue
+	0x54:  0x10508, // seashell
+	0x56:  0x1bc0d, // ruby-position
+	0x57:  0x8620d, // outline-style
+	0x58:  0x66508, // seagreen
+	0x59:  0x8b05,  // color
+	0x5c:  0x2930c, // caption-side
+	0x5d:  0x6ae06, // height
+	0x5e:  0x7810f, // mediumslateblue
+	0x5f:  0x9360c, // pause-before
+	0x60:  0xc50c,  // lemonchiffon
+	0x63:  0x3ab07, // cursive
+	0x66:  0x4dc0b, // navajowhite
+	0x67:  0xa3c0c, // voice-family
+	0x68:  0x2730d, // darkgoldenrod
+	0x69:  0x41509, // firebrick
+	0x6a:  0x47d0a, // font-style
+	0x6b:  0xa2909, // direction
+	0x6d:  0x7be0a, // darkorange
+	0x6f:  0x4870c, // font-variant
+	0x70:  0x2f206, // margin
+	0x71:  0x88611, // page-break-before
+	0x73:  0xa50d,  // text-overflow
+	0x74:  0x4612,  // text-kashida-space
+	0x75:  0x36108, // cornsilk
+	0x76:  0x4a20b, // font-weight
+	0x77:  0x46104, // size
+	0x78:  0x57b0b, // layout-grid
+	0x79:  0x9110b, // padding-top
+	0x7a:  0x47607, // charset
+	0x7d:  0x81b09, // mistyrose
+	0x7e:  0x5ef07, // azimuth
+	0x7f:  0x92b0b, // pause-after
+	0x83:  0x28704, // calc
+	0x84:  0x3be06, // cursor
+	0x85:  0xe903,  // hsl
+	0x86:  0x56d0e, // letter-spacing
+	0x88:  0x7ca09, // darkgreen
+	0x8b:  0x40308, // document
+	0x8d:  0x39109, // cue-after
+	0x8f:  0x39a0a, // cue-before
+	0x91:  0x60a07, // fantasy
+	0x94:  0x16d0d, // ruby-overhang
+	0x95:  0x2e50e, // padding-bottom
+	0x9a:  0x5da09, // lightblue
+	0x9c:  0x8f80a, // darksalmon
+	0x9d:  0x45c10, // font-size-adjust
+	0x9e:  0x64c09, // lightpink
+	0xa0:  0x95c0c, // darkseagreen
+	0xa2:  0x89711, // page-break-inside
+	0xa4:  0x27709, // goldenrod
+	0xa5:  0x63909, // lightgray
+	0xa6:  0xa4c05, // media
+	0xa7:  0x57b18, // layout-grid-char-spacing
+	0xa9:  0x51709, // important
+	0xaa:  0x7ea0a, // min-height
+	0xb0:  0x15d11, // border-left-color
+	0xb1:  0x88604, // page
+	0xb2:  0x9c416, // layer-background-image
+	0xb5:  0x59310, // layout-grid-line
 	0xb6:  0x1511,  // background-repeat
-	0xb7:  0x8513,  // border-bottom-color
-	0xb9:  0x25008, // darkgray
-	0xbb:  0x5f90c, // padding-left
-	0xbc:  0x1a205, // right
-	0xc0:  0x40914, // scrollbar-base-color
-	0xc1:  0x6530e, // lightsteelblue
-	0xc2:  0xef05,  // width
-	0xc5:  0x3b209, // turquoise
-	0xc8:  0x3ee05, // float
-	0xca:  0x12b0a, // ruby-align
+	0xb7:  0x7d13,  // border-bottom-color
+	0xb9:  0x2580a, // box-shadow
+	0xbb:  0x5490c, // padding-left
+	0xbc:  0x1b205, // right
+	0xc0:  0x43d14, // scrollbar-base-color
+	0xc1:  0x41e04, // flex
+	0xc2:  0xe505,  // width
+	0xc5:  0x3e209, // turquoise
+	0xc8:  0x42205, // float
+	0xca:  0x1530a, // ruby-align
 	0xcb:  0xb08,   // position
-	0xcc:  0x7cd0a, // margin-top
-	0xce:  0x2cc0b, // margin-left
-	0xcf:  0x2eb0b, // text-shadow
-	0xd0:  0x2f50a, // word-break
-	0xd4:  0x3f90a, // whitesmoke
-	0xd6:  0x33d17, // text-underline-position
-	0xd7:  0x1bf12, // border-right-width
-	0xd8:  0x80009, // olivedrab
-	0xd9:  0x89e09, // palegreen
-	0xdb:  0x4e306, // import
-	0xdc:  0x6ca05, // marks
-	0xdd:  0x3bb0a, // darkviolet
+	0xcc:  0x8050a, // margin-top
+	0xce:  0x2fe0b, // margin-left
+	0xcf:  0x3080b, // text-shadow
+	0xd0:  0x2610a, // word-break
+	0xd4:  0x42d0a, // whitesmoke
+	0xd6:  0x33c17, // text-underline-position
+	0xd7:  0x1db12, // border-right-width
+	0xd8:  0x83809, // olivedrab
+	0xd9:  0x8d609, // palegreen
+	0xdb:  0x51706, // import
+	0xdc:  0x70205, // marks
+	0xdd:  0x3eb0a, // darkviolet
 	0xde:  0x13,    // background-position
-	0xe0:  0x6fc10, // mediumaquamarine
-	0xe1:  0x7a04,  // bold
-	0xe2:  0x7690f, // mediumturquoise
-	0xe4:  0x8700d, // palegoldenrod
-	0xe5:  0x4f40b, // darkmagenta
-	0xe6:  0x15309, // rosybrown
-	0xe7:  0x18a11, // border-left-width
-	0xe8:  0x88509, // darkkhaki
-	0xea:  0x650e,  // blanchedalmond
-	0xeb:  0x52007, // initial
-	0xec:  0x6ce1b, // scrollbar-dark-shadow-color
-	0xee:  0x48e0b, // saddlebrown
-	0xef:  0x8a70d, // paleturquoise
-	0xf1:  0x19b12, // border-right-color
-	0xf3:  0x1e305, // white
-	0xf7:  0x9ce19, // scrollbar-highlight-color
-	0xf9:  0x56d10, // layout-grid-mode
-	0xfc:  0x1f40c, // border-style
-	0xfe:  0x69b13, // list-style-position
-	0x100: 0x11616, // layer-background-color
-	0x102: 0x58210, // layout-grid-type
-	0x103: 0x15c0b, // border-left
-	0x104: 0x2db08, // overflow
-	0x105: 0x7a60c, // midnightblue
-	0x10b: 0x2840a, // text-align
-	0x10e: 0x21010, // border-top-style
-	0x110: 0x5de14, // lightgoldenrodyellow
-	0x114: 0x8506,  // border
-	0x119: 0xd804,  // font
-	0x11c: 0x7020a, // aquamarine
-	0x11d: 0x60e0a, // lightgreen
-	0x11e: 0x5ec06, // yellow
-	0x120: 0x97405, // speak
-	0x121: 0x4630b, // white-space
-	0x123: 0x3940d, // darkslateblue
-	0x125: 0x1e60e, // text-autospace
-	0x128: 0xf50d,  // lavenderblush
-	0x12c: 0x6210b, // lightsalmon
-	0x12d: 0x51907, // inherit
-	0x131: 0x87c0a, // darkorchid
-	0x132: 0x2000a, // border-top
-	0x133: 0x3c80b, // play-during
-	0x137: 0x22010, // border-top-width
-	0x139: 0x48807, // orphans
-	0x13a: 0x41d0b, // font-family
-	0x13d: 0x3db0a, // dodgerblue
-	0x13f: 0x8d00a, // papayawhip
-	0x140: 0x8f305, // pause
-	0x143: 0x2ff0e, // cornflowerblue
-	0x144: 0x3c507, // display
-	0x146: 0x52509, // aliceblue
-	0x14a: 0x7208,  // darkblue
+	0xe0:  0x73410, // mediumaquamarine
+	0xe1:  0x7204,  // bold
+	0xe2:  0x7a10f, // mediumturquoise
+	0xe4:  0x8a80d, // palegoldenrod
+	0xe5:  0x5280b, // darkmagenta
+	0xe6:  0x18009, // rosybrown
+	0xe7:  0x19a11, // border-left-width
+	0xe8:  0x8bd09, // darkkhaki
+	0xea:  0x5d0e,  // blanchedalmond
+	0xeb:  0x55c07, // initial
+	0xec:  0x7061b, // scrollbar-dark-shadow-color
+	0xee:  0x4c20b, // saddlebrown
+	0xef:  0x8df0d, // paleturquoise
+	0xf1:  0x1ab12, // border-right-color
+	0xf3:  0x1ff05, // white
+	0xf7:  0xa0619, // scrollbar-highlight-color
+	0xf9:  0x5a910, // layout-grid-mode
+	0xfc:  0x2100c, // border-style
+	0xfe:  0x6d313, // list-style-position
+	0x100: 0x10c16, // layer-background-color
+	0x102: 0x5be10, // layout-grid-type
+	0x103: 0x15d0b, // border-left
+	0x104: 0xaa08,  // overflow
+	0x105: 0x7de0c, // midnightblue
+	0x10b: 0x2b60a, // text-align
+	0x10e: 0x22c10, // border-top-style
+	0x110: 0x61a14, // lightgoldenrodyellow
+	0x114: 0x7d06,  // border
+	0x119: 0xce04,  // font
+	0x11c: 0x73a0a, // aquamarine
+	0x11d: 0x6420a, // lightgreen
+	0x11e: 0x62806, // yellow
+	0x120: 0x9ac05, // speak
+	0x121: 0x4970b, // white-space
+	0x123: 0x3c40d, // darkslateblue
+	0x125: 0x2020e, // text-autospace
+	0x128: 0xeb0d,  // lavenderblush
+	0x12c: 0x6550b, // lightsalmon
+	0x12d: 0x55507, // inherit
+	0x131: 0x8b40a, // darkorchid
+	0x132: 0x21c0a, // border-top
+	0x133: 0x3f80b, // play-during
+	0x137: 0x23c10, // border-top-width
+	0x139: 0x4bc07, // orphans
+	0x13a: 0x4510b, // font-family
+	0x13d: 0x40b0a, // dodgerblue
+	0x13f: 0x9080a, // papayawhip
+	0x140: 0x92b05, // pause
+	0x142: 0x6b40f, // linear-gradient
+	0x143: 0x3530e, // cornflowerblue
+	0x144: 0x3f507, // display
+	0x146: 0x56109, // aliceblue
+	0x14a: 0x6a08,  // darkblue
 	0x14b: 0x3108,  // behavior
-	0x14c: 0x3540d, // counter-reset
-	0x14d: 0x7960b, // greenyellow
-	0x14e: 0x75811, // mediumspringgreen
-	0x14f: 0x9150a, // powderblue
-	0x150: 0x53f10, // layout-grid-char
-	0x158: 0x81d07, // outline
-	0x159: 0x23c09, // burlywood
-	0x15b: 0xe113,  // border-bottom-width
-	0x15c: 0x4bf04, // none
-	0x15e: 0x36103, // cue
-	0x15f: 0x4fd0c, // table-layout
-	0x160: 0x90a0b, // pitch-range
-	0x161: 0xa1907, // z-index
-	0x162: 0x29106, // stress
-	0x163: 0x80815, // background-position-x
-	0x165: 0x4d506, // normal
-	0x167: 0x72f0c, // mediumpurple
-	0x169: 0x5a70a, // lightcoral
-	0x16c: 0x6e90a, // max-height
-	0x16d: 0x3804,  // rgba
-	0x16e: 0x68b10, // list-style-image
-	0x170: 0x26b08, // deeppink
-	0x173: 0x91f06, // progid
-	0x175: 0x75e0b, // springgreen
-	0x176: 0x3700b, // forestgreen
-	0x179: 0x7ec08, // moccasin
-	0x17a: 0x7780f, // mediumvioletred
-	0x17e: 0x9bc0b, // text-indent
-	0x181: 0x6ae0f, // list-style-type
-	0x182: 0x14c09, // gainsboro
-	0x183: 0x3ae0d, // darkturquoise
-	0x184: 0x3a10d, // darkslategray
-	0x189: 0x2db0a, // overflow-x
-	0x18b: 0x93006, // quotes
-	0x18c: 0x3a15,  // background-attachment
-	0x18f: 0x19b0c, // border-right
-	0x191: 0x6005,  // black
-	0x192: 0x79b0b, // yellowgreen
-	0x194: 0x59009, // peachpuff
-	0x197: 0x3f30b, // floralwhite
-	0x19c: 0x7210e, // darkolivegreen
-	0x19d: 0x5f109, // word-wrap
-	0x19e: 0x17911, // border-left-style
-	0x1a0: 0x9b30b, // speech-rate
-	0x1a1: 0x8370d, // outline-width
-	0x1a2: 0x9e70c, // unicode-bidi
-	0x1a3: 0x68b0a, // list-style
-	0x1a4: 0x90a05, // pitch
-	0x1a5: 0x95f15, // scrollbar-track-color
-	0x1a6: 0x47907, // fuchsia
-	0x1a8: 0x3800e, // vertical-align
-	0x1ad: 0x5af05, // alpha
-	0x1ae: 0x6f309, // max-width
-	0x1af: 0x9708,  // richness
-	0x1b0: 0x3803,  // rgb
-	0x1b1: 0x7d60d, // padding-right
-	0x1b2: 0x29615, // scrollbar-arrow-color
-	0x1b3: 0x16304, // left
-	0x1b5: 0x4a009, // elevation
-	0x1b6: 0x5630a, // line-break
-	0x1ba: 0x27d09, // chocolate
-	0x1bb: 0x9800d, // speak-numeral
-	0x1bd: 0x47f0b, // accelerator
-	0x1be: 0x67709, // limegreen
-	0x1c1: 0x7d08,  // darkcyan
-	0x1c3: 0x6390c, // lightskyblue
-	0x1c5: 0x5c50a, // sans-serif
-	0x1c6: 0x850d,  // border-bottom
+	0x14c: 0x3840d, // counter-reset
+	0x14d: 0x7ce0b, // greenyellow
+	0x14e: 0x79011, // mediumspringgreen
+	0x14f: 0x94d0a, // powderblue
+	0x150: 0x57b10, // layout-grid-char
+	0x158: 0x85507, // outline
+	0x159: 0x26b09, // burlywood
+	0x15b: 0xd713,  // border-bottom-width
+	0x15c: 0x4f304, // none
+	0x15e: 0x39103, // cue
+	0x15f: 0x5310c, // table-layout
+	0x160: 0x9420b, // pitch-range
+	0x161: 0xa5107, // z-index
+	0x162: 0x2c306, // stress
+	0x163: 0x84015, // background-position-x
+	0x165: 0x50906, // normal
+	0x167: 0x7670c, // mediumpurple
+	0x169: 0x5e30a, // lightcoral
+	0x16c: 0x7210a, // max-height
+	0x16d: 0x8f04,  // rgba
+	0x16e: 0x6c310, // list-style-image
+	0x170: 0x29d08, // deeppink
+	0x173: 0x95706, // progid
+	0x175: 0x7960b, // springgreen
+	0x176: 0x3a00b, // forestgreen
+	0x178: 0x3790b, // transparent
+	0x179: 0x82408, // moccasin
+	0x17a: 0x7b00f, // mediumvioletred
+	0x17e: 0x9f40b, // text-indent
+	0x181: 0x6e60f, // list-style-type
+	0x182: 0x17909, // gainsboro
+	0x183: 0x3de0d, // darkturquoise
+	0x184: 0x3d10d, // darkslategray
+	0x189: 0xaa0a,  // overflow-x
+	0x18b: 0x96806, // quotes
+	0x18c: 0x9115,  // background-attachment
+	0x18f: 0x1ab0c, // border-right
+	0x191: 0x5805,  // black
+	0x192: 0x7d30b, // yellowgreen
+	0x194: 0x5cc09, // peachpuff
+	0x197: 0x4270b, // floralwhite
+	0x19c: 0x7590e, // darkolivegreen
+	0x19d: 0x54109, // word-wrap
+	0x19e: 0x18911, // border-left-style
+	0x1a0: 0x9eb0b, // speech-rate
+	0x1a1: 0x86f0d, // outline-width
+	0x1a2: 0xa1f0c, // unicode-bidi
+	0x1a3: 0x6c30a, // list-style
+	0x1a4: 0x94205, // pitch
+	0x1a5: 0x99715, // scrollbar-track-color
+	0x1a6: 0x4ad07, // fuchsia
+	0x1a8: 0x3b00e, // vertical-align
+	0x1ad: 0x5eb05, // alpha
+	0x1ae: 0x72b09, // max-width
+	0x1af: 0x12108, // richness
+	0x1b0: 0x380f,  // radial-gradient
+	0x1b1: 0x80e0d, // padding-right
+	0x1b2: 0x2c815, // scrollbar-arrow-color
+	0x1b3: 0x16404, // left
+	0x1b5: 0x4d409, // elevation
+	0x1b6: 0x59f0a, // line-break
+	0x1ba: 0x2af09, // chocolate
+	0x1bb: 0x9b80d, // speak-numeral
+	0x1bd: 0x4b30b, // accelerator
+	0x1be: 0x6a009, // limegreen
+	0x1c1: 0x7508,  // darkcyan
+	0x1c3: 0x66d0c, // lightskyblue
+	0x1c5: 0x6010a, // sans-serif
+	0x1c6: 0x7d0d,  // border-bottom
 	0x1c7: 0xa,     // background
-	0x1c8: 0xa1006, // volume
-	0x1ca: 0x66b0c, // writing-mode
-	0x1cb: 0x9e18,  // scrollbar-3d-light-color
-	0x1cc: 0x5c006, // widows
-	0x1cf: 0x42809, // font-size
+	0x1c8: 0xa4806, // volume
+	0x1ca: 0x62d0c, // writing-mode
+	0x1cb: 0x12818, // scrollbar-3d-light-color
+	0x1cc: 0x5fc06, // widows
+	0x1cf: 0x45c09, // font-size
 	0x1d0: 0x15,    // background-position-y
-	0x1d1: 0x5d509, // lightcyan
-	0x1d4: 0x4ec09, // indianred
-	0x1d7: 0x1de0a, // ghostwhite
-	0x1db: 0x78a09, // orangered
-	0x1dc: 0x45c0c, // antiquewhite
-	0x1dd: 0x4da09, // lawngreen
-	0x1df: 0x73b0e, // mediumseagreen
-	0x1e0: 0x20010, // border-top-color
-	0x1e2: 0xf304,  // hsla
-	0x1e4: 0x3250e, // text-transform
-	0x1e6: 0x7160c, // mediumorchid
-	0x1e9: 0x8c709, // monospace
-	0x1ec: 0x94916, // scrollbar-shadow-color
-	0x1ed: 0x79209, // darkgreen
-	0x1ef: 0x25809, // cadetblue
-	0x1f0: 0x59806, // filter
-	0x1f1: 0x1ad12, // border-right-style
-	0x1f6: 0x8440a, // overflow-y
-	0x1f7: 0xd809,  // font-face
-	0x1f8: 0x50d0c, // word-spacing
-	0x1fa: 0xbe13,  // border-bottom-style
-	0x1fb: 0x4380c, // font-stretch
-	0x1fc: 0x7c509, // mintcream
-	0x1fd: 0x88d08, // ime-mode
-	0x1fe: 0x2730a, // chartreuse
-	0x1ff: 0x5ca05, // serif
+	0x1d1: 0x61109, // lightcyan
+	0x1d4: 0x52009, // indianred
+	0x1d7: 0x1fa0a, // ghostwhite
+	0x1db: 0x7c209, // orangered
+	0x1dc: 0x4900c, // antiquewhite
+	0x1dd: 0x50e09, // lawngreen
+	0x1df: 0x7730e, // mediumseagreen
+	0x1e0: 0x21c10, // border-top-color
+	0x1e2: 0xe904,  // hsla
+	0x1e4: 0x3240e, // text-transform
+	0x1e6: 0x74e0c, // mediumorchid
+	0x1e9: 0x8ff09, // monospace
+	0x1ec: 0x98116, // scrollbar-shadow-color
+	0x1ed: 0x6870e, // lightsteelblue
+	0x1ef: 0x28a09, // cadetblue
+	0x1f0: 0x5d406, // filter
+	0x1f1: 0x1c912, // border-right-style
+	0x1f6: 0x87c0a, // overflow-y
+	0x1f7: 0xce09,  // font-face
+	0x1f8: 0x3120c, // word-spacing
+	0x1fa: 0xb413,  // border-bottom-style
+	0x1fb: 0x46c0c, // font-stretch
+	0x1fc: 0x7fd09, // mintcream
+	0x1fd: 0x8c508, // ime-mode
+	0x1fe: 0x2a50a, // chartreuse
+	0x1ff: 0x60605, // serif
 }

+ 4 - 0
vendor/github.com/tdewolff/parse/css/parse.go

@@ -70,6 +70,10 @@ type Token struct {
 	Data []byte
 }
 
+func (t Token) String() string {
+	return t.TokenType.String() + "('" + string(t.Data) + "')"
+}
+
 // Parser is the state for the parser.
 type Parser struct {
 	l     *Lexer

+ 42 - 25
vendor/github.com/tdewolff/parse/js/lex.go

@@ -23,7 +23,8 @@ const (
 	UnknownToken                         // extra token when no token can be matched
 	WhitespaceToken                      // space \t \v \f
 	LineTerminatorToken                  // \r \n \r\n
-	CommentToken
+	SingleLineCommentToken
+	MultiLineCommentToken // token for comments with line terminators (not just any /*block*/)
 	IdentifierToken
 	PunctuatorToken /* { } ( ) [ ] . ; , < > <= >= == != === !==  + - * % ++ -- << >>
 	   >>> & | ^ ! ~ && || ? : = += -= *= %= <<= >>= >>>= &= |= ^= / /= >= */
@@ -68,8 +69,10 @@ func (tt TokenType) String() string {
 		return "Whitespace"
 	case LineTerminatorToken:
 		return "LineTerminator"
-	case CommentToken:
-		return "Comment"
+	case SingleLineCommentToken:
+		return "SingleLineComment"
+	case MultiLineCommentToken:
+		return "MultiLineComment"
 	case IdentifierToken:
 		return "Identifier"
 	case PunctuatorToken:
@@ -174,15 +177,15 @@ func (l *Lexer) Next() (TokenType, []byte) {
 		l.r.Move(1)
 		tt = PunctuatorToken
 	case '<', '>', '=', '!', '+', '-', '*', '%', '&', '|', '^':
-		if (c == '<' || (l.emptyLine && c == '-')) && l.consumeCommentToken() {
-			return CommentToken, l.r.Shift()
+		if l.consumeHTMLLikeCommentToken() {
+			return SingleLineCommentToken, l.r.Shift()
 		} else if l.consumeLongPunctuatorToken() {
 			l.state = ExprState
 			tt = PunctuatorToken
 		}
 	case '/':
-		if l.consumeCommentToken() {
-			return CommentToken, l.r.Shift()
+		if tt = l.consumeCommentToken(); tt != UnknownToken {
+			return tt, l.r.Shift()
 		} else if l.state == ExprState && l.consumeRegexpToken() {
 			l.state = SubscriptState
 			tt = RegexpToken
@@ -374,46 +377,54 @@ func (l *Lexer) consumeSingleLineComment() {
 
 ////////////////////////////////////////////////////////////////
 
-func (l *Lexer) consumeCommentToken() bool {
+func (l *Lexer) consumeHTMLLikeCommentToken() bool {
+	c := l.r.Peek(0)
+	if c == '<' && l.r.Peek(1) == '!' && l.r.Peek(2) == '-' && l.r.Peek(3) == '-' {
+		// opening HTML-style single line comment
+		l.r.Move(4)
+		l.consumeSingleLineComment()
+		return true
+	} else if l.emptyLine && c == '-' && l.r.Peek(1) == '-' && l.r.Peek(2) == '>' {
+		// closing HTML-style single line comment
+		// (only if current line didn't contain any meaningful tokens)
+		l.r.Move(3)
+		l.consumeSingleLineComment()
+		return true
+	}
+	return false
+}
+
+func (l *Lexer) consumeCommentToken() TokenType {
 	c := l.r.Peek(0)
 	if c == '/' {
 		c = l.r.Peek(1)
 		if c == '/' {
-			// single line
+			// single line comment
 			l.r.Move(2)
 			l.consumeSingleLineComment()
+			return SingleLineCommentToken
 		} else if c == '*' {
-			// multi line
+			// block comment (potentially multiline)
+			tt := SingleLineCommentToken
 			l.r.Move(2)
 			for {
 				c := l.r.Peek(0)
 				if c == '*' && l.r.Peek(1) == '/' {
 					l.r.Move(2)
-					return true
+					break
 				} else if c == 0 {
 					break
 				} else if l.consumeLineTerminator() {
+					tt = MultiLineCommentToken
 					l.emptyLine = true
 				} else {
 					l.r.Move(1)
 				}
 			}
-		} else {
-			return false
+			return tt
 		}
-	} else if c == '<' && l.r.Peek(1) == '!' && l.r.Peek(2) == '-' && l.r.Peek(3) == '-' {
-		// opening HTML-style single line comment
-		l.r.Move(4)
-		l.consumeSingleLineComment()
-	} else if c == '-' && l.r.Peek(1) == '-' && l.r.Peek(2) == '>' {
-		// closing HTML-style single line comment
-		// (only if current line didn't contain any meaningful tokens)
-		l.r.Move(3)
-		l.consumeSingleLineComment()
-	} else {
-		return false
 	}
-	return true
+	return UnknownToken
 }
 
 func (l *Lexer) consumeLongPunctuatorToken() bool {
@@ -643,6 +654,12 @@ func (l *Lexer) consumeTemplateToken() bool {
 			l.state = ExprState
 			l.r.Move(2)
 			return true
+		} else if c == '\\' {
+			l.r.Move(1)
+			if c := l.r.Peek(0); c != 0 {
+				l.r.Move(1)
+			}
+			continue
 		} else if c == 0 {
 			l.r.Rewind(mark)
 			return false

+ 9 - 6
vendor/github.com/tdewolff/parse/js/lex_test.go

@@ -20,7 +20,7 @@ func TestTokens(t *testing.T) {
 		{"\n\r\r\n\u2028\u2029", TTs{LineTerminatorToken}},
 		{"5.2 .04 0x0F 5e99", TTs{NumericToken, NumericToken, NumericToken, NumericToken}},
 		{"a = 'string'", TTs{IdentifierToken, PunctuatorToken, StringToken}},
-		{"/*comment*/ //comment", TTs{CommentToken, CommentToken}},
+		{"/*comment*/ //comment", TTs{SingleLineCommentToken, SingleLineCommentToken}},
 		{"{ } ( ) [ ]", TTs{PunctuatorToken, PunctuatorToken, PunctuatorToken, PunctuatorToken, PunctuatorToken, PunctuatorToken}},
 		{". ; , < > <=", TTs{PunctuatorToken, PunctuatorToken, PunctuatorToken, PunctuatorToken, PunctuatorToken, PunctuatorToken}},
 		{">= == != === !==", TTs{PunctuatorToken, PunctuatorToken, PunctuatorToken, PunctuatorToken, PunctuatorToken}},
@@ -31,12 +31,12 @@ func TestTokens(t *testing.T) {
 		{">>= >>>= &= |= ^= =>", TTs{PunctuatorToken, PunctuatorToken, PunctuatorToken, PunctuatorToken, PunctuatorToken, PunctuatorToken}},
 		{"a = /.*/g;", TTs{IdentifierToken, PunctuatorToken, RegexpToken, PunctuatorToken}},
 
-		{"/*co\nm\u2028m/*ent*/ //co//mment\u2029//comment", TTs{CommentToken, CommentToken, LineTerminatorToken, CommentToken}},
+		{"/*co\nm\u2028m/*ent*/ //co//mment\u2029//comment", TTs{MultiLineCommentToken, SingleLineCommentToken, LineTerminatorToken, SingleLineCommentToken}},
 		{"<!-", TTs{PunctuatorToken, PunctuatorToken, PunctuatorToken}},
-		{"1<!--2\n", TTs{NumericToken, CommentToken, LineTerminatorToken}},
+		{"1<!--2\n", TTs{NumericToken, SingleLineCommentToken, LineTerminatorToken}},
 		{"x=y-->10\n", TTs{IdentifierToken, PunctuatorToken, IdentifierToken, PunctuatorToken, PunctuatorToken, NumericToken, LineTerminatorToken}},
-		{"  /*comment*/ -->nothing\n", TTs{CommentToken, CommentToken, LineTerminatorToken}},
-		{"1 /*comment\nmultiline*/ -->nothing\n", TTs{NumericToken, CommentToken, CommentToken, LineTerminatorToken}},
+		{"  /*comment*/ -->nothing\n", TTs{SingleLineCommentToken, SingleLineCommentToken, LineTerminatorToken}},
+		{"1 /*comment\nmultiline*/ -->nothing\n", TTs{NumericToken, MultiLineCommentToken, SingleLineCommentToken, LineTerminatorToken}},
 		{"$ _\u200C \\u2000 \u200C", TTs{IdentifierToken, IdentifierToken, IdentifierToken, UnknownToken}},
 		{">>>=>>>>=", TTs{PunctuatorToken, PunctuatorToken, PunctuatorToken}},
 		{"1/", TTs{NumericToken, PunctuatorToken}},
@@ -63,7 +63,7 @@ func TestTokens(t *testing.T) {
 		{"'\n '\u2028", TTs{UnknownToken, LineTerminatorToken, UnknownToken, LineTerminatorToken}},
 		{"'str\\\U00100000ing\\0'", TTs{StringToken}},
 		{"'strin\\00g'", TTs{StringToken}},
-		{"/*comment", TTs{CommentToken}},
+		{"/*comment", TTs{SingleLineCommentToken}},
 		{"a=/regexp", TTs{IdentifierToken, PunctuatorToken, RegexpToken}},
 		{"\\u002", TTs{UnknownToken, IdentifierToken}},
 
@@ -97,6 +97,9 @@ func TestTokens(t *testing.T) {
 		{"function f(){}/1/g", TTs{IdentifierToken, IdentifierToken, PunctuatorToken, PunctuatorToken, PunctuatorToken, PunctuatorToken, RegexpToken}},
 		{"this.return/1/g", TTs{IdentifierToken, PunctuatorToken, IdentifierToken, PunctuatorToken, NumericToken, PunctuatorToken, IdentifierToken}},
 		{"(a+b)/1/g", TTs{PunctuatorToken, IdentifierToken, PunctuatorToken, IdentifierToken, PunctuatorToken, PunctuatorToken, NumericToken, PunctuatorToken, IdentifierToken}},
+		{"`\\``", TTs{TemplateToken}},
+		{"`\\${ 1 }`", TTs{TemplateToken}},
+		{"`\\\r\n`", TTs{TemplateToken}},
 
 		// go fuzz
 		{"`", TTs{UnknownToken}},

+ 6 - 1
vendor/github.com/tdewolff/parse/strconv/int.go

@@ -1,6 +1,8 @@
 package strconv // import "github.com/tdewolff/parse/strconv"
 
-import "math"
+import (
+	"math"
+)
 
 // Int parses a byte-slice and returns the integer it represents.
 // If an invalid character is encountered, it will stop there.
@@ -34,6 +36,9 @@ func ParseInt(b []byte) (int64, int) {
 
 func LenInt(i int64) int {
 	if i < 0 {
+		if i == -9223372036854775808 {
+			return 19
+		}
 		i = -i
 	}
 	switch {

+ 2 - 0
vendor/github.com/tdewolff/parse/strconv/int_test.go

@@ -41,6 +41,8 @@ func TestLenInt(t *testing.T) {
 		{1, 1},
 		{10, 2},
 		{99, 2},
+		{9223372036854775807, 19},
+		{-9223372036854775808, 19},
 
 		// coverage
 		{100, 3},

+ 83 - 0
vendor/github.com/tdewolff/parse/strconv/price.go

@@ -0,0 +1,83 @@
+package strconv
+
+// AppendPrice will append an int64 formatted as a price, where the int64 is the price in cents.
+// It does not display whether a price is negative or not.
+func AppendPrice(b []byte, price int64, dec bool, milSeparator byte, decSeparator byte) []byte {
+	if price < 0 {
+		if price == -9223372036854775808 {
+			x := []byte("92 233 720 368 547 758 08")
+			x[2] = milSeparator
+			x[6] = milSeparator
+			x[10] = milSeparator
+			x[14] = milSeparator
+			x[18] = milSeparator
+			x[22] = decSeparator
+			return append(b, x...)
+		}
+		price = -price
+	}
+
+	// rounding
+	if !dec {
+		firstDec := (price / 10) % 10
+		if firstDec >= 5 {
+			price += 100
+		}
+	}
+
+	// calculate size
+	n := LenInt(price) - 2
+	if n > 0 {
+		n += (n - 1) / 3 // mil separator
+	} else {
+		n = 1
+	}
+	if dec {
+		n += 2 + 1 // decimals + dec separator
+	}
+
+	// resize byte slice
+	i := len(b)
+	if i+n > cap(b) {
+		b = append(b, make([]byte, n)...)
+	} else {
+		b = b[:i+n]
+	}
+
+	// print fractional-part
+	i += n - 1
+	if dec {
+		for j := 0; j < 2; j++ {
+			c := byte(price%10) + '0'
+			price /= 10
+			b[i] = c
+			i--
+		}
+		b[i] = decSeparator
+		i--
+	} else {
+		price /= 100
+	}
+
+	if price == 0 {
+		b[i] = '0'
+		return b
+	}
+
+	// print integer-part
+	j := 0
+	for price > 0 {
+		if j == 3 {
+			b[i] = milSeparator
+			i--
+			j = 0
+		}
+
+		c := byte(price%10) + '0'
+		price /= 10
+		b[i] = c
+		i--
+		j++
+	}
+	return b
+}

+ 29 - 0
vendor/github.com/tdewolff/parse/strconv/price_test.go

@@ -0,0 +1,29 @@
+package strconv // import "github.com/tdewolff/parse/strconv"
+
+import (
+	"testing"
+
+	"github.com/tdewolff/test"
+)
+
+func TestAppendPrice(t *testing.T) {
+	priceTests := []struct {
+		price    int64
+		dec      bool
+		expected string
+	}{
+		{0, false, "0"},
+		{0, true, "0.00"},
+		{100, true, "1.00"},
+		{-100, true, "1.00"},
+		{100000, false, "1,000"},
+		{100000, true, "1,000.00"},
+		{123456789012, true, "1,234,567,890.12"},
+		{9223372036854775807, true, "92,233,720,368,547,758.07"},
+		{-9223372036854775808, true, "92,233,720,368,547,758.08"},
+	}
+	for _, tt := range priceTests {
+		price := AppendPrice([]byte{}, tt.price, tt.dec, ',', '.')
+		test.String(t, string(price), tt.expected, "for", tt.price)
+	}
+}