hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

css.go (7157B)

    1 // Copyright 2011 The Go Authors. All rights reserved.
    2 // Use of this source code is governed by a BSD-style
    3 // license that can be found in the LICENSE file.
    4 
    5 package template
    6 
    7 import (
    8 	"bytes"
    9 	"fmt"
   10 	"strings"
   11 	"unicode"
   12 	"unicode/utf8"
   13 )
   14 
   15 // endsWithCSSKeyword reports whether b ends with an ident that
   16 // case-insensitively matches the lower-case kw.
   17 func endsWithCSSKeyword(b []byte, kw string) bool {
   18 	i := len(b) - len(kw)
   19 	if i < 0 {
   20 		// Too short.
   21 		return false
   22 	}
   23 	if i != 0 {
   24 		r, _ := utf8.DecodeLastRune(b[:i])
   25 		if isCSSNmchar(r) {
   26 			// Too long.
   27 			return false
   28 		}
   29 	}
   30 	// Many CSS keywords, such as "!important" can have characters encoded,
   31 	// but the URI production does not allow that according to
   32 	// https://www.w3.org/TR/css3-syntax/#TOK-URI
   33 	// This does not attempt to recognize encoded keywords. For example,
   34 	// given "\75\72\6c" and "url" this return false.
   35 	return string(bytes.ToLower(b[i:])) == kw
   36 }
   37 
   38 // isCSSNmchar reports whether rune is allowed anywhere in a CSS identifier.
   39 func isCSSNmchar(r rune) bool {
   40 	// Based on the CSS3 nmchar production but ignores multi-rune escape
   41 	// sequences.
   42 	// https://www.w3.org/TR/css3-syntax/#SUBTOK-nmchar
   43 	return 'a' <= r && r <= 'z' ||
   44 		'A' <= r && r <= 'Z' ||
   45 		'0' <= r && r <= '9' ||
   46 		r == '-' ||
   47 		r == '_' ||
   48 		// Non-ASCII cases below.
   49 		0x80 <= r && r <= 0xd7ff ||
   50 		0xe000 <= r && r <= 0xfffd ||
   51 		0x10000 <= r && r <= 0x10ffff
   52 }
   53 
   54 // decodeCSS decodes CSS3 escapes given a sequence of stringchars.
   55 // If there is no change, it returns the input, otherwise it returns a slice
   56 // backed by a new array.
   57 // https://www.w3.org/TR/css3-syntax/#SUBTOK-stringchar defines stringchar.
   58 func decodeCSS(s []byte) []byte {
   59 	i := bytes.IndexByte(s, '\\')
   60 	if i == -1 {
   61 		return s
   62 	}
   63 	// The UTF-8 sequence for a codepoint is never longer than 1 + the
   64 	// number hex digits need to represent that codepoint, so len(s) is an
   65 	// upper bound on the output length.
   66 	b := make([]byte, 0, len(s))
   67 	for len(s) != 0 {
   68 		i := bytes.IndexByte(s, '\\')
   69 		if i == -1 {
   70 			i = len(s)
   71 		}
   72 		b, s = append(b, s[:i]...), s[i:]
   73 		if len(s) < 2 {
   74 			break
   75 		}
   76 		// https://www.w3.org/TR/css3-syntax/#SUBTOK-escape
   77 		// escape ::= unicode | '\' [#x20-#x7E#x80-#xD7FF#xE000-#xFFFD#x10000-#x10FFFF]
   78 		if isHex(s[1]) {
   79 			// https://www.w3.org/TR/css3-syntax/#SUBTOK-unicode
   80 			//   unicode ::= '\' [0-9a-fA-F]{1,6} wc?
   81 			j := 2
   82 			for j < len(s) && j < 7 && isHex(s[j]) {
   83 				j++
   84 			}
   85 			r := hexDecode(s[1:j])
   86 			if r > unicode.MaxRune {
   87 				r, j = r/16, j-1
   88 			}
   89 			n := utf8.EncodeRune(b[len(b):cap(b)], r)
   90 			// The optional space at the end allows a hex
   91 			// sequence to be followed by a literal hex.
   92 			// string(decodeCSS([]byte(`\A B`))) == "\nB"
   93 			b, s = b[:len(b)+n], skipCSSSpace(s[j:])
   94 		} else {
   95 			// `\\` decodes to `\` and `\"` to `"`.
   96 			_, n := utf8.DecodeRune(s[1:])
   97 			b, s = append(b, s[1:1+n]...), s[1+n:]
   98 		}
   99 	}
  100 	return b
  101 }
  102 
  103 // isHex reports whether the given character is a hex digit.
  104 func isHex(c byte) bool {
  105 	return '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F'
  106 }
  107 
  108 // hexDecode decodes a short hex digit sequence: "10" -> 16.
  109 func hexDecode(s []byte) rune {
  110 	n := '\x00'
  111 	for _, c := range s {
  112 		n <<= 4
  113 		switch {
  114 		case '0' <= c && c <= '9':
  115 			n |= rune(c - '0')
  116 		case 'a' <= c && c <= 'f':
  117 			n |= rune(c-'a') + 10
  118 		case 'A' <= c && c <= 'F':
  119 			n |= rune(c-'A') + 10
  120 		default:
  121 			panic(fmt.Sprintf("Bad hex digit in %q", s))
  122 		}
  123 	}
  124 	return n
  125 }
  126 
  127 // skipCSSSpace returns a suffix of c, skipping over a single space.
  128 func skipCSSSpace(c []byte) []byte {
  129 	if len(c) == 0 {
  130 		return c
  131 	}
  132 	// wc ::= #x9 | #xA | #xC | #xD | #x20
  133 	switch c[0] {
  134 	case '\t', '\n', '\f', ' ':
  135 		return c[1:]
  136 	case '\r':
  137 		// This differs from CSS3's wc production because it contains a
  138 		// probable spec error whereby wc contains all the single byte
  139 		// sequences in nl (newline) but not CRLF.
  140 		if len(c) >= 2 && c[1] == '\n' {
  141 			return c[2:]
  142 		}
  143 		return c[1:]
  144 	}
  145 	return c
  146 }
  147 
  148 // isCSSSpace reports whether b is a CSS space char as defined in wc.
  149 func isCSSSpace(b byte) bool {
  150 	switch b {
  151 	case '\t', '\n', '\f', '\r', ' ':
  152 		return true
  153 	}
  154 	return false
  155 }
  156 
  157 // cssEscaper escapes HTML and CSS special characters using \<hex>+ escapes.
  158 func cssEscaper(args ...any) string {
  159 	s, _ := stringify(args...)
  160 	var b strings.Builder
  161 	r, w, written := rune(0), 0, 0
  162 	for i := 0; i < len(s); i += w {
  163 		// See comment in htmlEscaper.
  164 		r, w = utf8.DecodeRuneInString(s[i:])
  165 		var repl string
  166 		switch {
  167 		case int(r) < len(cssReplacementTable) && cssReplacementTable[r] != "":
  168 			repl = cssReplacementTable[r]
  169 		default:
  170 			continue
  171 		}
  172 		if written == 0 {
  173 			b.Grow(len(s))
  174 		}
  175 		b.WriteString(s[written:i])
  176 		b.WriteString(repl)
  177 		written = i + w
  178 		if repl != `\\` && (written == len(s) || isHex(s[written]) || isCSSSpace(s[written])) {
  179 			b.WriteByte(' ')
  180 		}
  181 	}
  182 	if written == 0 {
  183 		return s
  184 	}
  185 	b.WriteString(s[written:])
  186 	return b.String()
  187 }
  188 
  189 var cssReplacementTable = []string{
  190 	0:    `\0`,
  191 	'\t': `\9`,
  192 	'\n': `\a`,
  193 	'\f': `\c`,
  194 	'\r': `\d`,
  195 	// Encode HTML specials as hex so the output can be embedded
  196 	// in HTML attributes without further encoding.
  197 	'"':  `\22`,
  198 	'&':  `\26`,
  199 	'\'': `\27`,
  200 	'(':  `\28`,
  201 	')':  `\29`,
  202 	'+':  `\2b`,
  203 	'/':  `\2f`,
  204 	':':  `\3a`,
  205 	';':  `\3b`,
  206 	'<':  `\3c`,
  207 	'>':  `\3e`,
  208 	'\\': `\\`,
  209 	'{':  `\7b`,
  210 	'}':  `\7d`,
  211 }
  212 
  213 var expressionBytes = []byte("expression")
  214 var mozBindingBytes = []byte("mozbinding")
  215 
  216 // cssValueFilter allows innocuous CSS values in the output including CSS
  217 // quantities (10px or 25%), ID or class literals (#foo, .bar), keyword values
  218 // (inherit, blue), and colors (#888).
  219 // It filters out unsafe values, such as those that affect token boundaries,
  220 // and anything that might execute scripts.
  221 func cssValueFilter(args ...any) string {
  222 	s, t := stringify(args...)
  223 	if t == contentTypeCSS {
  224 		return s
  225 	}
  226 	b, id := decodeCSS([]byte(s)), make([]byte, 0, 64)
  227 
  228 	// CSS3 error handling is specified as honoring string boundaries per
  229 	// https://www.w3.org/TR/css3-syntax/#error-handling :
  230 	//     Malformed declarations. User agents must handle unexpected
  231 	//     tokens encountered while parsing a declaration by reading until
  232 	//     the end of the declaration, while observing the rules for
  233 	//     matching pairs of (), [], {}, "", and '', and correctly handling
  234 	//     escapes. For example, a malformed declaration may be missing a
  235 	//     property, colon (:) or value.
  236 	// So we need to make sure that values do not have mismatched bracket
  237 	// or quote characters to prevent the browser from restarting parsing
  238 	// inside a string that might embed JavaScript source.
  239 	for i, c := range b {
  240 		switch c {
  241 		case 0, '"', '\'', '(', ')', '/', ';', '@', '[', '\\', ']', '`', '{', '}':
  242 			return filterFailsafe
  243 		case '-':
  244 			// Disallow <!-- or -->.
  245 			// -- should not appear in valid identifiers.
  246 			if i != 0 && b[i-1] == '-' {
  247 				return filterFailsafe
  248 			}
  249 		default:
  250 			if c < utf8.RuneSelf && isCSSNmchar(rune(c)) {
  251 				id = append(id, c)
  252 			}
  253 		}
  254 	}
  255 	id = bytes.ToLower(id)
  256 	if bytes.Contains(id, expressionBytes) || bytes.Contains(id, mozBindingBytes) {
  257 		return filterFailsafe
  258 	}
  259 	return string(b)
  260 }