From 53deb0b8cd1899ec325eca93631b3e137bdd3ec3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Guillot?= Date: Thu, 5 Jul 2018 22:18:51 -0700 Subject: [PATCH] Refactor assets bundler and split Javascript files --- Gopkg.lock | 8 +- generate.go | 206 ++- ui/static/js.go | 5 +- ui/static/js/.jshintrc | 3 + ui/static/js/app.js | 823 ----------- ui/static/js/bootstrap.js | 74 + ui/static/js/confirm_handler.js | 47 + ui/static/js/dom_helper.js | 46 + ui/static/js/entry_handler.js | 110 ++ ui/static/js/form_handler.js | 15 + ui/static/js/keyboard_handler.js | 63 + ui/static/js/menu_handler.js | 27 + ui/static/js/modal_handler.js | 31 + ui/static/js/mouse_handler.js | 11 + ui/static/js/nav_handler.js | 211 +++ ui/static/js/request_builder.js | 43 + ui/static/js/touch_handler.js | 94 ++ ui/static/js/unread_counter_handler.js | 33 + .../tdewolff/minify/.goreleaser.yml | 10 +- vendor/github.com/tdewolff/minify/README.md | 13 +- .../tdewolff/minify/cmd/minify/README.md | 72 +- .../tdewolff/minify/cmd/minify/main.go | 200 +-- .../cmd/minify/minify_bash_tab_completion | 29 + .../tdewolff/minify/cmd/minify/util.go | 100 +- .../tdewolff/minify/cmd/minify/util_test.go | 152 +++ .../tdewolff/minify/cmd/minify/watch.go | 2 +- vendor/github.com/tdewolff/minify/common.go | 151 ++- .../github.com/tdewolff/minify/common_test.go | 117 +- vendor/github.com/tdewolff/minify/css/css.go | 528 ++++--- .../tdewolff/minify/css/css_test.go | 40 +- .../github.com/tdewolff/minify/css/table.go | 28 +- .../github.com/tdewolff/minify/html/html.go | 24 +- .../tdewolff/minify/html/html_test.go | 2 + .../github.com/tdewolff/minify/html/table.go | 1 - vendor/github.com/tdewolff/minify/js/js.go | 19 +- .../github.com/tdewolff/minify/js/js_test.go | 11 + .../tdewolff/minify/svg/pathdata.go | 10 +- .../tdewolff/minify/svg/pathdata_test.go | 11 +- vendor/github.com/tdewolff/minify/svg/svg.go | 35 +- .../tdewolff/minify/svg/svg_test.go | 6 +- .../github.com/tdewolff/minify/svg/table.go | 12 - vendor/github.com/tdewolff/parse/css/hash.go | 1207 +++++++++-------- vendor/github.com/tdewolff/parse/css/parse.go | 4 + vendor/github.com/tdewolff/parse/js/lex.go | 67 +- .../github.com/tdewolff/parse/js/lex_test.go | 15 +- .../github.com/tdewolff/parse/strconv/int.go | 7 +- .../tdewolff/parse/strconv/int_test.go | 2 + .../tdewolff/parse/strconv/price.go | 83 ++ .../tdewolff/parse/strconv/price_test.go | 29 + 49 files changed, 2837 insertions(+), 2000 deletions(-) create mode 100644 ui/static/js/.jshintrc delete mode 100644 ui/static/js/app.js create mode 100644 ui/static/js/bootstrap.js create mode 100644 ui/static/js/confirm_handler.js create mode 100644 ui/static/js/dom_helper.js create mode 100644 ui/static/js/entry_handler.js create mode 100644 ui/static/js/form_handler.js create mode 100644 ui/static/js/keyboard_handler.js create mode 100644 ui/static/js/menu_handler.js create mode 100644 ui/static/js/modal_handler.js create mode 100644 ui/static/js/mouse_handler.js create mode 100644 ui/static/js/nav_handler.js create mode 100644 ui/static/js/request_builder.js create mode 100644 ui/static/js/touch_handler.js create mode 100644 ui/static/js/unread_counter_handler.js create mode 100644 vendor/github.com/tdewolff/minify/cmd/minify/minify_bash_tab_completion create mode 100644 vendor/github.com/tdewolff/minify/cmd/minify/util_test.go create mode 100644 vendor/github.com/tdewolff/parse/strconv/price.go create mode 100644 vendor/github.com/tdewolff/parse/strconv/price_test.go diff --git a/Gopkg.lock b/Gopkg.lock index 50067ee5..fad4585a 100644 --- a/Gopkg.lock +++ b/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" diff --git a/generate.go b/generate.go index e983f113..debc24eb 100644 --- a/generate.go +++ b/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 generateFile(serializer, pkg, mapName, pattern, output string) { - generatedFile := &GeneratedFile{ - Package: pkg, - Map: mapName, - Files: make(map[string]string), - Checksums: make(map[string]string), - } - - files, _ := filepath.Glob(pattern) - for _, file := range files { - basename := path.Base(file) - content, err := ioutil.ReadFile(file) - 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) - } - - generatedFile.Checksums[basename] = fmt.Sprintf("%x", sha256.Sum256(content)) - } - - f, err := os.Create(output) +func (b *Bundle) Write(filename string) { + f, err := os.Create(filename) if err != nil { panic(err) } defer f.Close() - generatedTpl.Execute(f, generatedFile) + bundleTpl.Execute(f, b) +} + +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 { + 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) + } + + bundle.Files[filename] = string(minifiedData) + bundle.Checksums[filename] = checksum(minifiedData) + } + + bundle.Write(bundleFile) +} + +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) + } + + 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) + } + + 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")) } diff --git a/ui/static/js.go b/ui/static/js.go index fff6f72b..7e599868 100644 --- a/ui/static/js.go +++ b/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", } diff --git a/ui/static/js/.jshintrc b/ui/static/js/.jshintrc new file mode 100644 index 00000000..53b202cb --- /dev/null +++ b/ui/static/js/.jshintrc @@ -0,0 +1,3 @@ +{ + "esversion": 6 +} \ No newline at end of file diff --git a/ui/static/js/app.js b/ui/static/js/app.js deleted file mode 100644 index 8ca2f8d3..00000000 --- a/ui/static/js/app.js +++ /dev/null @@ -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)); - } -}); - -})(); diff --git a/ui/static/js/bootstrap.js b/ui/static/js/bootstrap.js new file mode 100644 index 00000000..98ad6351 --- /dev/null +++ b/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)); + } +}); diff --git a/ui/static/js/confirm_handler.js b/ui/static/js/confirm_handler.js new file mode 100644 index 00000000..9fe91337 --- /dev/null +++ b/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); + } +} diff --git a/ui/static/js/dom_helper.js b/ui/static/js/dom_helper.js new file mode 100644 index 00000000..8a0644b1 --- /dev/null +++ b/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; + } +} diff --git a/ui/static/js/entry_handler.js b/ui/static/js/entry_handler.js new file mode 100644 index 00000000..25d45c78 --- /dev/null +++ b/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(); + } +} diff --git a/ui/static/js/form_handler.js b/ui/static/js/form_handler.js new file mode 100644 index 00000000..de417db8 --- /dev/null +++ b/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; + } + }; + }); + } +} diff --git a/ui/static/js/keyboard_handler.js b/ui/static/js/keyboard_handler.js new file mode 100644 index 00000000..df6eefc9 --- /dev/null +++ b/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; + } +} diff --git a/ui/static/js/menu_handler.js b/ui/static/js/menu_handler.js new file mode 100644 index 00000000..0907a912 --- /dev/null +++ b/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"; + } + } +} diff --git a/ui/static/js/modal_handler.js b/ui/static/js/modal_handler.js new file mode 100644 index 00000000..c0e8b137 --- /dev/null +++ b/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); + } + } +} diff --git a/ui/static/js/mouse_handler.js b/ui/static/js/mouse_handler.js new file mode 100644 index 00000000..a70a37af --- /dev/null +++ b/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); + }; + }); + } +} \ No newline at end of file diff --git a/ui/static/js/nav_handler.js b/ui/static/js/nav_handler.js new file mode 100644 index 00000000..c3165078 --- /dev/null +++ b/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; + } +} diff --git a/ui/static/js/request_builder.js b/ui/static/js/request_builder.js new file mode 100644 index 00000000..52ed2a14 --- /dev/null +++ b/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); + } + }); + } +} diff --git a/ui/static/js/touch_handler.js b/ui/static/js/touch_handler.js new file mode 100644 index 00000000..69488a50 --- /dev/null +++ b/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); + }); + } +} diff --git a/ui/static/js/unread_counter_handler.js b/ui/static/js/unread_counter_handler.js new file mode 100644 index 00000000..87a1aaaf --- /dev/null +++ b/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; + } + ); + } + } +} diff --git a/vendor/github.com/tdewolff/minify/.goreleaser.yml b/vendor/github.com/tdewolff/minify/.goreleaser.yml index bcc6975f..afaa1cb5 100644 --- a/vendor/github.com/tdewolff/minify/.goreleaser.yml +++ b/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: diff --git a/vendor/github.com/tdewolff/minify/README.md b/vendor/github.com/tdewolff/minify/README.md index 588b3d63..f051b09c 100644 --- a/vendor/github.com/tdewolff/minify/README.md +++ b/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 diff --git a/vendor/github.com/tdewolff/minify/cmd/minify/README.md b/vendor/github.com/tdewolff/minify/cmd/minify/README.md index 662ca2d6..f042fe3e 100644 --- a/vendor/github.com/tdewolff/minify/cmd/minify/README.md +++ b/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`. +You can enable bash tab completion by using + + source minify_bash_tab_completion + ## Usage + Usage: minify [options] [input] - 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 - 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 + Input: + Files or directories, leave blank to use stdin ### Types diff --git a/vendor/github.com/tdewolff/minify/cmd/minify/main.go b/vendor/github.com/tdewolff/minify/cmd/minify/main.go index 62263baa..2ed981c1 100644 --- a/vendor/github.com/tdewolff/minify/cmd/minify/main.go +++ b/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() } diff --git a/vendor/github.com/tdewolff/minify/cmd/minify/minify_bash_tab_completion b/vendor/github.com/tdewolff/minify/cmd/minify/minify_bash_tab_completion new file mode 100644 index 00000000..ca6391a8 --- /dev/null +++ b/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 diff --git a/vendor/github.com/tdewolff/minify/cmd/minify/util.go b/vendor/github.com/tdewolff/minify/cmd/minify/util.go index d8db16e0..2bc9a6b4 100644 --- a/vendor/github.com/tdewolff/minify/cmd/minify/util.go +++ b/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 NewPrependReader(r io.ReadCloser, prepend []byte) *prependReader { - return &prependReader{r, prepend} +func (r eofReader) Close() error { + return nil } -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 +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 } - r.prepend = nil - return n, nil + } else { + cur = eofReader{} } - return r.ReadCloser.Read(p) + return &concatFileReader{filenames, opener, nil, cur, 0}, nil +} + +func (r *concatFileReader) SetSeparator(sep []byte) { + r.sep = sep +} + +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:]) + } + } + 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() } diff --git a/vendor/github.com/tdewolff/minify/cmd/minify/util_test.go b/vendor/github.com/tdewolff/minify/cmd/minify/util_test.go new file mode 100644 index 00000000..255dfcaa --- /dev/null +++ b/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("_")) +} diff --git a/vendor/github.com/tdewolff/minify/cmd/minify/watch.go b/vendor/github.com/tdewolff/minify/cmd/minify/watch.go index 7fdb3819..d97cbc85 100644 --- a/vendor/github.com/tdewolff/minify/cmd/minify/watch.go +++ b/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 } } diff --git a/vendor/github.com/tdewolff/minify/common.go b/vendor/github.com/tdewolff/minify/common.go index fa3c91d2..d6b76891 100644 --- a/vendor/github.com/tdewolff/minify/common.go +++ b/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 { diff --git a/vendor/github.com/tdewolff/minify/common_test.go b/vendor/github.com/tdewolff/minify/common_test.go index 823e4e20..dbd84a53 100644 --- a/vendor/github.com/tdewolff/minify/common_test.go +++ b/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, '-') diff --git a/vendor/github.com/tdewolff/minify/css/css.go b/vendor/github.com/tdewolff/minify/css/css.go index de0a8c8f..1c34166c 100644 --- a/vendor/github.com/tdewolff/minify/css/css.go +++ b/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 - } - 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 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[i].TokenType, values[i].Data = value.TokenType, value.Data - } + prop := css.ToHash(property) + + // Strip !important from the component list, this will be added later separately 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] + 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 } - 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):]...) + // 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 + } + + 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-- + } + } + 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}) + } + } + c.valuesBuffer = values + + // 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(") } } - } 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 + + for _, component := range components { + if _, err := c.w.Write(component.Data); err != nil { + return err + } + } + 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 + + 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) - } 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) + 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 and } } else if tt == css.IdentToken { //parse.ToLower(data) // TODO: not all identifiers are case-insensitive; all 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] diff --git a/vendor/github.com/tdewolff/minify/css/css_test.go b/vendor/github.com/tdewolff/minify/css/css_test.go index ed88bf53..5db74b74 100644 --- a/vendor/github.com/tdewolff/minify/css/css_test.go +++ b/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{} diff --git a/vendor/github.com/tdewolff/minify/css/table.go b/vendor/github.com/tdewolff/minify/css/table.go index 331b3141..1e9d0b92 100644 --- a/vendor/github.com/tdewolff/minify/css/table.go +++ b/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 diff --git a/vendor/github.com/tdewolff/minify/html/html.go b/vendor/github.com/tdewolff/minify/html/html.go index e6236f05..626fab2e 100644 --- a/vendor/github.com/tdewolff/minify/html/html.go +++ b/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(" len("") { // downlevel-hidden begin := bytes.IndexByte(t.Data, '>') + 1 end := len(t.Data) - len("") 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 { diff --git a/vendor/github.com/tdewolff/minify/html/html_test.go b/vendor/github.com/tdewolff/minify/html/html_test.go index 932dcf77..6864aa38 100644 --- a/vendor/github.com/tdewolff/minify/html/html_test.go +++ b/vendor/github.com/tdewolff/minify/html/html_test.go @@ -32,6 +32,7 @@ func TestHTML(t *testing.T) { {`x`, `x`}, {``, ``}, {``, ``}, + {``, ``}, {``, ``}, {``, ``}, {``, ``}, @@ -178,6 +179,7 @@ func TestHTMLKeepConditionalComments(t *testing.T) { }{ {``, ``}, {` `, ``}, + {` `, ``}, } m := minify.New() diff --git a/vendor/github.com/tdewolff/minify/html/table.go b/vendor/github.com/tdewolff/minify/html/table.go index 7693534b..fa1f074a 100644 --- a/vendor/github.com/tdewolff/minify/html/table.go +++ b/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, diff --git a/vendor/github.com/tdewolff/minify/js/js.go b/vendor/github.com/tdewolff/minify/js/js.go index 1f6c0b82..7d995e15 100644 --- a/vendor/github.com/tdewolff/minify/js/js.go +++ b/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 } diff --git a/vendor/github.com/tdewolff/minify/js/js_test.go b/vendor/github.com/tdewolff/minify/js/js_test.go index d0767259..e452654e 100644 --- a/vendor/github.com/tdewolff/minify/js/js_test.go +++ b/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 {`/\`, `/\`}, diff --git a/vendor/github.com/tdewolff/minify/svg/pathdata.go b/vendor/github.com/tdewolff/minify/svg/pathdata.go index f14e8479..7268879e 100644 --- a/vendor/github.com/tdewolff/minify/svg/pathdata.go +++ b/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 { diff --git a/vendor/github.com/tdewolff/minify/svg/pathdata_test.go b/vendor/github.com/tdewolff/minify/svg/pathdata_test.go index a975aa00..8d4f4a54 100644 --- a/vendor/github.com/tdewolff/minify/svg/pathdata_test.go +++ b/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}) diff --git a/vendor/github.com/tdewolff/minify/svg/svg.go b/vendor/github.com/tdewolff/minify/svg/svg.go index 984544f3..83962e9e 100644 --- a/vendor/github.com/tdewolff/minify/svg/svg.go +++ b/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)] diff --git a/vendor/github.com/tdewolff/minify/svg/svg_test.go b/vendor/github.com/tdewolff/minify/svg/svg_test.go index 7d3cfef5..bc43b0c6 100644 --- a/vendor/github.com/tdewolff/minify/svg/svg_test.go +++ b/vendor/github.com/tdewolff/minify/svg/svg_test.go @@ -41,9 +41,9 @@ func TestSVG(t *testing.T) { {``, ``}, {``, ``}, {``, ``}, - {``, ``}, - {``, ``}, - {``, ``}, + {``, ``}, + {``, ``}, + {``, ``}, {``, ``}, {``, ``}, {``, ``}, diff --git a/vendor/github.com/tdewolff/minify/svg/table.go b/vendor/github.com/tdewolff/minify/svg/table.go index 42a1e785..22d6c2bc 100644 --- a/vendor/github.com/tdewolff/minify/svg/table.go +++ b/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, diff --git a/vendor/github.com/tdewolff/parse/css/hash.go b/vendor/github.com/tdewolff/parse/css/hash.go index 95595c36..77f383f0 100644 --- a/vendor/github.com/tdewolff/parse/css/hash.go +++ b/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 } diff --git a/vendor/github.com/tdewolff/parse/css/parse.go b/vendor/github.com/tdewolff/parse/css/parse.go index e48cd663..cedd237b 100644 --- a/vendor/github.com/tdewolff/parse/css/parse.go +++ b/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 diff --git a/vendor/github.com/tdewolff/parse/js/lex.go b/vendor/github.com/tdewolff/parse/js/lex.go index 3ee73e75..ce4e1d56 100644 --- a/vendor/github.com/tdewolff/parse/js/lex.go +++ b/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 diff --git a/vendor/github.com/tdewolff/parse/js/lex_test.go b/vendor/github.com/tdewolff/parse/js/lex_test.go index b379321c..1866087c 100644 --- a/vendor/github.com/tdewolff/parse/js/lex_test.go +++ b/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}}, {"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}}, diff --git a/vendor/github.com/tdewolff/parse/strconv/int.go b/vendor/github.com/tdewolff/parse/strconv/int.go index 7101daa0..a84ecf3c 100644 --- a/vendor/github.com/tdewolff/parse/strconv/int.go +++ b/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 { diff --git a/vendor/github.com/tdewolff/parse/strconv/int_test.go b/vendor/github.com/tdewolff/parse/strconv/int_test.go index 1719f45b..2df2cdff 100644 --- a/vendor/github.com/tdewolff/parse/strconv/int_test.go +++ b/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}, diff --git a/vendor/github.com/tdewolff/parse/strconv/price.go b/vendor/github.com/tdewolff/parse/strconv/price.go new file mode 100644 index 00000000..94b38343 --- /dev/null +++ b/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 +} diff --git a/vendor/github.com/tdewolff/parse/strconv/price_test.go b/vendor/github.com/tdewolff/parse/strconv/price_test.go new file mode 100644 index 00000000..3b3fccf6 --- /dev/null +++ b/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) + } +}