ref: 4e15a670c166c11b3eb19a1ad6316c789b26513e
parent: f548ead614a4ee6fda902670100b49c17070c81b
author: Philip Silva <[email protected]>
date: Sun Jan 30 12:16:28 EST 2022
Parse media queries Ported code from https://github.com/ericf/css-mediaquery
--- a/browser/browser_test.go
+++ b/browser/browser_test.go
@@ -210,7 +210,7 @@
return nil, nil, fmt.Errorf("parse url: %w", err)
}
b.History.Push(u, 0)
- nm, err := style.FetchNodeMap(doc, style.AddOnCSS, 1280)
+ nm, err := style.FetchNodeMap(doc, style.AddOnCSS)
if err != nil {
return nil, nil, fmt.Errorf("FetchNodeMap: %w", err)
}
--- a/browser/experimental_test.go
+++ b/browser/experimental_test.go
@@ -46,7 +46,6 @@
}
fs.SetDOM(nt)
fs.Update(h, nil, scripts)
- js.NewJS(h, nil, nt)
js.Start()
h, _, err = processJS2()
if err != nil {
--- a/browser/website.go
+++ b/browser/website.go
@@ -55,7 +55,7 @@
log.Printf("CSS size %v kB", cssSize/1024)
- nm, err := style.FetchNodeMap(doc, css, 1280)
+ nm, err := style.FetchNodeMap(doc, css)
if err == nil {
if debugPrintHtml {
log.Printf("%v", nm)
@@ -175,9 +175,17 @@
}
case "link":
isStylesheet := n.Attr("rel") == "stylesheet"
- isPrint := n.Attr("media") == "print"
+ if m := n.Attr("media"); m != "" {
+ matches, errMatch := style.MatchQuery(m, style.MediaValues)
+ if errMatch != nil {
+ log.Errorf("match query %v: %v", m, errMatch)
+ }
+ if !matches {
+ return
+ }
+ }
href := n.Attr("href")
- if isStylesheet && !isPrint {
+ if isStylesheet {
url, err := f.LinkedUrl(href)
if err != nil {
log.Errorf("error parsing %v", href)
--- /dev/null
+++ b/style/media.go
@@ -1,0 +1,239 @@
+package style
+
+import (
+ "fmt"
+ "regexp"
+ "strconv"
+ "strings"
+)
+
+// Functions MatchQuery and parseQuery ported from
+// https://github.com/ericf/css-mediaquery
+// originally released as
+// Copyright (c) 2014, Yahoo! Inc. All rights reserved.
+// Copyrights licensed under the New BSD License.
+
+var (
+ reExpressions = regexp.MustCompile(`\([^\)]+\)`)
+ reMediaQuery = regexp.MustCompile(`^(?:(only|not)?\s*([_a-z][_a-z0-9-]*)|(\([^\)]+\)))(?:\s*and\s*(.*))?$`) // TODO: /i,
+ reMqExpression = regexp.MustCompile(`^\(\s*([_a-z-][_a-z0-9-]*)\s*(?:\:\s*([^\)]+))?\s*\)$`)
+ reMqFeature = regexp.MustCompile(`^(?:(min|max)-)?(.+)`)
+ reLengthUnit = regexp.MustCompile(`(em|rem|px|cm|mm|in|pt|pc)?\s*$`)
+ reResolutionUnit = regexp.MustCompile(`(dpi|dpcm|dppx)?\s*$`)
+)
+
+type MediaQuery struct {
+ inverse bool
+ typ string
+ exprs []MediaExpr
+}
+
+type MediaExpr struct {
+ modifier string
+ feature string
+ value string
+}
+
+func MatchQuery(mediaQuery string, values map[string]string) (yes bool, err error) {
+ qs, err := parseQuery(mediaQuery)
+ if err != nil {
+ return false, fmt.Errorf("parse query: %v", err)
+ }
+ for _, q := range qs {
+ inverse := q.inverse
+ typeMatch := q.typ == "all" || values["type"] == q.typ
+ if (typeMatch && inverse) || !(typeMatch || inverse) {
+ continue
+ }
+
+ every := true
+ for _, expr := range q.exprs {
+ var valueFloat float64
+ var expValueFloat float64
+ feature := expr.feature
+ modifier := expr.modifier
+ expValue := expr.value
+ value := values[feature]
+
+ if value == "" {
+ every = false
+ break
+ }
+ switch feature {
+ case "orientation", "scan", "prefers-color-scheme":
+ if strings.ToLower(value) != strings.ToLower(expValue) {
+ every = false
+ break
+ }
+ case "width", "height", "device-width", "device-height":
+ if expValueFloat, err = toPx(expValue); err != nil {
+ break
+ }
+ if valueFloat, err = toPx(value); err != nil {
+ break
+ }
+ case "resolution":
+ if expValueFloat, err = toDpi(expValue); err != nil {
+ break
+ }
+ if valueFloat, err = toDpi(value); err != nil {
+ break
+ }
+ case "aspect-ratio", "device-aspect-ratio", /* Deprecated */ "device-pixel-ratio":
+ if expValueFloat, err = toDecimal(expValue); err != nil {
+ break
+ }
+ if valueFloat, err = toDecimal(value); err != nil {
+ break
+ }
+ case "grid", "color", "color-index", "monochrome":
+ var i int64
+ i, err = strconv.ParseInt(expValue, 10, 64)
+ if err != nil {
+ i = 1
+ err = nil
+ }
+ expValueFloat = float64(i)
+ i, err = strconv.ParseInt(value, 10, 64)
+ if err != nil {
+ i = 0
+ err = nil
+ }
+ valueFloat = float64(i)
+ }
+ switch modifier {
+ case "min":
+ every = valueFloat >= expValueFloat
+ case "max":
+ every = valueFloat <= expValueFloat
+ default:
+ every = valueFloat == expValueFloat
+ }
+ }
+ if (every && !inverse) || (!every && inverse) {
+ return true, nil
+ }
+ }
+ return false, nil
+}
+
+func parseQuery(mediaQuery string) (tokens []MediaQuery, err error) {
+ parts := strings.Split(mediaQuery, ",")
+ for _, q := range parts {
+ q = strings.TrimSpace(q)
+ captures := reMediaQuery.FindStringSubmatch(q)
+
+ if captures == nil {
+ return tokens, fmt.Errorf("Invalid CSS media query: %v", q)
+ }
+ modifier := captures[1]
+ parsed := MediaQuery{}
+ parsed.inverse = strings.ToLower(modifier) == "not"
+ if typ := captures[2]; typ == "" {
+ parsed.typ = "all"
+ } else {
+ parsed.typ = strings.ToLower(typ)
+ }
+
+ var exprs string
+ if len(captures) >= 4 {
+ exprs += captures[3]
+ }
+ if len(captures) >= 5 {
+ exprs += captures[4]
+ }
+ exprs = strings.TrimSpace(exprs)
+ if exprs == "" {
+ tokens = append(tokens, parsed)
+ continue
+ }
+
+ exprsList := reExpressions.FindStringSubmatch(exprs)
+ if exprsList == nil {
+ return tokens, fmt.Errorf("Invalid CSS media query: %v", q)
+ }
+ for _, expr := range exprsList {
+ var captures = reMqExpression.FindStringSubmatch(expr)
+
+ if captures == nil {
+ return tokens, fmt.Errorf("Invalid CSS media query: %v", q)
+ }
+ feature := reMqFeature.FindStringSubmatch(strings.ToLower(captures[1]))
+ parsed.exprs = append(parsed.exprs, MediaExpr{
+ modifier: feature[1],
+ feature: feature[2],
+ value: captures[2],
+ })
+ }
+ tokens = append(tokens, parsed)
+ }
+ return
+}
+
+// -- Utilities ----------------------------------------------------------------
+
+var reQuot = regexp.MustCompile(`^(\d+)\s*\/\s*(\d+)$`)
+
+func toDecimal(ratio string) (decimal float64, err error) {
+ decimal, err = strconv.ParseFloat(ratio, 64)
+ if err != nil {
+ numbers := reQuot.FindStringSubmatch(ratio)
+ if numbers == nil {
+ return 0, fmt.Errorf("cannot parse %v", ratio)
+ }
+ p, err := strconv.ParseFloat(numbers[0], 64)
+ if err != nil {
+ return 0, fmt.Errorf("cannot parse %v", p)
+ }
+ q, err := strconv.ParseFloat(numbers[1], 64)
+ if err != nil {
+ return 0, fmt.Errorf("cannot parse %v", q)
+ }
+ if q == 0 {
+ return 0, fmt.Errorf("division by zero")
+ }
+ decimal = p / q
+ }
+ return
+}
+
+func toDpi(resolution string) (value float64, err error) {
+ if value, err = strconv.ParseFloat(resolution, 64); err != nil {
+ return
+ }
+ units := reResolutionUnit.FindStringSubmatch(resolution)[1]
+
+ switch units {
+ case "dpcm":
+ value /= 2.54
+ case "dppx":
+ value *= 96
+ }
+ return
+}
+
+func toPx(length string) (value float64, err error) {
+ units := reLengthUnit.FindStringSubmatch(length)[1]
+ length = length[:len(length)-len(units)]
+ if value, err = strconv.ParseFloat(length, 64); err != nil {
+ return
+ }
+
+ switch units {
+ case "em":
+ value *= 16
+ case "rem":
+ value *= 16
+ case "cm":
+ value *= 96 / 2.54
+ case "mm":
+ value *= 96 / 2.54 / 10
+ case "in":
+ value *= 96
+ case "pt":
+ value *= 72
+ case "pc":
+ value *= 72 / 12
+ }
+ return
+}
--- /dev/null
+++ b/style/media_test.go
@@ -1,0 +1,50 @@
+package style
+
+import (
+ "testing"
+)
+
+func TestParseQuery(t *testing.T) {
+ mqs, err := parseQuery(`only screen and (max-width: 600px)`)
+ if err != nil {
+ t.Fail()
+ }
+ if len(mqs) != 1 {
+ t.Fail()
+ }
+ mq := mqs[0]
+ if mq.inverse || mq.typ != "screen" || len(mq.exprs) != 1 {
+ t.Fail()
+ }
+ expr := mq.exprs[0]
+ if expr.modifier != "max" || expr.feature != "width" || expr.value != "600px" {
+ t.Fail()
+ }
+}
+
+func TestMatchQuery(t *testing.T) {
+ matching := map[string]string{
+ "type": "screen",
+ "width": "500",
+ }
+ notMatching := map[string]string{
+ "type": "screen",
+ "width": "700",
+ }
+ yes, err := MatchQuery(`only screen and (max-width: 600px)`, matching)
+ if err != nil {
+ t.Fail()
+ }
+ t.Logf("%v", yes)
+ if !yes {
+ t.Fail()
+ }
+ yes, err = MatchQuery(`only screen and (max-width: 600px)`, notMatching)
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+ t.Logf("%v", yes)
+ if yes {
+ t.Fail()
+ }
+}
--- a/style/stylesheets.go
+++ b/style/stylesheets.go
@@ -23,14 +23,18 @@
var dui *duit.DUI
var availableFontNames []string
-var rMinWidth = regexp.MustCompile(`min-width:\s*(\d+)(px|em|rem)`)
-var rMaxWidth = regexp.MustCompile(`max-width:\s*(\d+)(px|em|rem)`)
-
const FontBaseSize = 11.0
var WindowWidth = 1280
var WindowHeight = 1080
+var MediaValues = map[string]string{
+ "type": "screen",
+ "width": fmt.Sprintf("%vpx", WindowWidth),
+ "orientation": "landscape",
+ "prefers-color-scheme": "dark",
+}
+
const AddOnCSS = `
/* https://developer.mozilla.org/en-US/docs/Web/HTML/Inline_elements */
a, abbr, acronym, audio, b, bdi, bdo, big, br, button, canvas, cite, code, data, datalist, del, dfn, em, embed, i, iframe, img, input, ins, kbd, label, map, mark, meter, noscript, object, output, picture, progress, q, ruby, s, samp, script, select, slot, small, span, strong, sub, sup, svg, template, textarea, time, u, tt, var, video, wbr {
@@ -72,8 +76,8 @@
}
}
-func FetchNodeMap(doc *html.Node, cssText string, windowWidth int) (m map[*html.Node]Map, err error) {
- mr, rv, err := FetchNodeRules(doc, cssText, windowWidth)
+func FetchNodeMap(doc *html.Node, cssText string) (m map[*html.Node]Map, err error) {
+ mr, rv, err := FetchNodeRules(doc, cssText)
if err != nil {
return nil, fmt.Errorf("fetch rules: %w", err)
}
@@ -114,7 +118,7 @@
return cascadia.ParseGroup(v)
}
-func FetchNodeRules(doc *html.Node, cssText string, windowWidth int) (m map[*html.Node][]Rule, rVars map[string]string, err error) {
+func FetchNodeRules(doc *html.Node, cssText string) (m map[*html.Node][]Rule, rVars map[string]string, err error) {
m = make(map[*html.Node][]Rule)
rVars = make(map[string]string)
s, err := Parse(cssText, false)
@@ -164,28 +168,13 @@
}
// for media queries
- if strings.Contains(r.Prelude, "print") && !strings.Contains(r.Prelude, "screen") {
- continue
- }
- if rMaxWidth.MatchString(r.Prelude) {
- m := rMaxWidth.FindStringSubmatch(r.Prelude)
- l := m[1] + m[2]
- maxWidth, _, err := length(nil, l)
+ if strings.HasPrefix(r.Prelude, "@media") {
+ p := strings.TrimPrefix(r.Prelude, "@media")
+ p = strings.TrimSpace(p)
+ yes, err := MatchQuery(p, MediaValues)
if err != nil {
- return nil, nil, fmt.Errorf("atoi: %w", err)
- }
- if float64(windowWidth) > maxWidth {
- continue
- }
- }
- if rMinWidth.MatchString(r.Prelude) {
- m := rMinWidth.FindStringSubmatch(r.Prelude)
- l := m[1] + m[2]
- minWidth, _, err := length(nil, l)
- if err != nil {
- return nil, nil, fmt.Errorf("atoi: %w", err)
- }
- if float64(windowWidth) < minWidth {
+ log.Errorf("match query %v: %v", r.Prelude, err)
+ } else if !yes {
continue
}
}
--- a/style/stylesheets_test.go
+++ b/style/stylesheets_test.go
@@ -1,6 +1,7 @@
package style
import (
+ "fmt"
"github.com/mjl-/duit"
"github.com/psilva261/opossum/logger"
"golang.org/x/net/html"
@@ -65,8 +66,9 @@
}
`
for _, w := range []int{400, 800} {
+ MediaValues["width"] = fmt.Sprintf("%vpx", w)
t.Logf("w=%v", w)
- m, _, err := FetchNodeRules(doc, css, w)
+ m, _, err := FetchNodeRules(doc, css)
if err != nil {
t.Fail()
}
@@ -128,7 +130,7 @@
if err != nil {
t.Fail()
}
- m, _, err := FetchNodeRules(doc, AddOnCSS, 1024)
+ m, _, err := FetchNodeRules(doc, AddOnCSS)
if err != nil {
t.Fail()
}
@@ -180,7 +182,7 @@
if err != nil {
t.Fail()
}
- m, err := FetchNodeMap(doc, AddOnCSS, 1024)
+ m, err := FetchNodeMap(doc, AddOnCSS)
if err != nil {
t.Fail()
}
@@ -197,7 +199,7 @@
t.Fail()
}
a := grep(doc, "a")
- m, err := FetchNodeMap(doc, AddOnCSS, 1024)
+ m, err := FetchNodeMap(doc, AddOnCSS)
if err != nil {
t.Fail()
}
@@ -205,7 +207,7 @@
if nodeMap[a].Css("color") != "blue" {
t.Fatalf("%v", nodeMap[a])
}
- m2, err := FetchNodeMap(doc, `.link { color: red; }`, 1024)
+ m2, err := FetchNodeMap(doc, `.link { color: red; }`)
if err != nil {
t.Fail()
}
@@ -454,7 +456,7 @@
}
`
- _, rv, err := FetchNodeRules(doc, css, 1280)
+ _, rv, err := FetchNodeRules(doc, css)
if err != nil {
t.Fail()
}
@@ -475,7 +477,7 @@
}
f(doc)
- nm, err := FetchNodeMap(doc, css, 1280)
+ nm, err := FetchNodeMap(doc, css)
if err != nil {
t.Fail()
}