;;
;; chicken-web-colors:
;; Parse and write HTML/CSS color strings in CHICKEN Scheme.
;;
;; Copyright © 2019 John Croisant.
;; All rights reserved.
;;
;; Redistribution and use in source and binary forms, with or without
;; modification, are permitted provided that the following conditions
;; are met:
;;
;; - Redistributions of source code must retain the above copyright
;; notice, this list of conditions and the following disclaimer.
;;
;; - Redistributions in binary form must reproduce the above copyright
;; notice, this list of conditions and the following disclaimer in
;; the documentation and/or other materials provided with the
;; distribution.
;;
;; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
;; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
;; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
;; FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
;; COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
;; INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
;; (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
;; SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
;; HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
;; STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
;; ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
;; OF THE POSSIBILITY OF SUCH DAMAGE.
(export parse-web-color
parse-hex-color
parse-named-color
parse-rgb-color
parse-hsl-color)
(define-type regexp-match (struct regexp-match))
(: parse-web-color (string --> color-list))
(define (parse-web-color str)
(cond
((irregex-match +hex-color-regex+ str)
(%parse-hex-color str))
((irregex-match +named-color-regex+ str)
(parse-named-color str))
((or (irregex-match +rgb-color-regex+ str)
(irregex-match +rgb-slash-color-regex+ str))
=> %handle-rgb-match)
((or (irregex-match +hsl-color-regex+ str)
(irregex-match +hsl-slash-color-regex+ str))
=> %handle-hsl-match)
(else
(error "Invalid web color" str))))
(: parse-hex-color (string --> color-list))
(define (parse-hex-color str)
(assert (irregex-match +hex-color-regex+ str)
"Invalid hex color" str)
(%parse-hex-color str))
(: %parse-hex-color (string --> color-list))
(define (%parse-hex-color str)
(case (string-length str)
;; #RGB or #RGBA
((4 5)
(list
'rgb
(string->number (make-string 2 (string-ref str 1)) 16)
(string->number (make-string 2 (string-ref str 2)) 16)
(string->number (make-string 2 (string-ref str 3)) 16)
(if (= 5 (string-length str))
(/ (string->number (make-string 2 (string-ref str 4)) 16)
255)
1)))
;; #RRGGBB or #RRGGBBAA
((7 9)
(list
'rgb
(string->number (substring str 1 3) 16)
(string->number (substring str 3 5) 16)
(string->number (substring str 5 7) 16)
(if (= 9 (string-length str))
(/ (string->number (substring str 7 9) 16)
255)
1)))
(else
(error "Invalid hex color" str))))
(define +hex-color-regex+
(sre->irregex
`(: bos
"#"
(or (= 3 hex-digit) (= 4 hex-digit)
(= 6 hex-digit) (= 8 hex-digit))
eos)))
(: parse-named-color (string --> color-list))
(define (parse-named-color str)
(cdr (or (assq (string->symbol (string-downcase str))
(*web-color-names*))
(error "Unknown named color" str))))
(define +named-color-regex+
(sre->irregex
`(: bos (+ (or alphanumeric "-")) eos)
'i))
(: parse-rgb-color (string --> color-list))
(define (parse-rgb-color str)
(cond ((or (irregex-match +rgb-color-regex+ str)
(irregex-match +rgb-slash-color-regex+ str))
=> %handle-rgb-match)
(else (error "Invalid rgb color" str))))
(define +separator-pattern+
'(or (: (* whitespace) "," (* whitespace))
(+ whitespace)))
(define +number-pattern+
'(or real
(: "." (+ digit) (? (or "e" "E") integer))))
(define +rgb-color-regex+
(sre->irregex
`(: bos
"rgb" (? "a") "("
(* whitespace)
(=> r ,+number-pattern+)
(=> rgb% (? "%"))
,+separator-pattern+
(=> g ,+number-pattern+)
(backref rgb%)
,+separator-pattern+
(=> b ,+number-pattern+)
(backref rgb%)
(? ,+separator-pattern+
(=> a ,+number-pattern+)
(=> a% (? "%")))
(* whitespace)
")"
eos)
'i))
;; Special case for "rgb[a](R G B / A)" syntax. The slash is only
;; allowed if the RGB are separated by spaces (not commas).
(define +rgb-slash-color-regex+
(sre->irregex
`(: bos
"rgb" (? "a") "("
(* whitespace)
(=> r ,+number-pattern+)
(=> rgb% (? "%"))
(+ whitespace)
(=> g ,+number-pattern+)
(backref rgb%)
(+ whitespace)
(=> b ,+number-pattern+)
(backref rgb%)
(? (: (* whitespace) "/" (* whitespace))
(=> a ,+number-pattern+)
(=> a% (? "%")))
(* whitespace)
")"
eos)
'i))
(: %handle-rgb-match (regexp-match --> color-list))
(define (%handle-rgb-match match)
(let ((r (irregex-match-substring match 'r))
(rgb% (irregex-match-substring match 'rgb%))
(g (irregex-match-substring match 'g))
(b (irregex-match-substring match 'b))
(a (irregex-match-substring match 'a))
(a% (irregex-match-substring match 'a%)))
(list (if (string= rgb% "%") 'rgb% 'rgb)
(%percent r rgb%)
(%percent g rgb%)
(%percent b rgb%)
(if a (%percent a a%) 1))))
(: %percent (string string --> number))
(define (%percent n %)
(if (string= % "%")
(/ (string->number n 10) 100)
(string->number n 10)))
(: parse-hsl-color (string --> color-list))
(define (parse-hsl-color str)
(cond ((or (irregex-match +hsl-color-regex+ str)
(irregex-match +hsl-slash-color-regex+ str))
=> %handle-hsl-match)
(else (error "Invalid hsl color" str))))
(define +hsl-color-regex+
(sre->irregex
`(: bos
"hsl" (? "a") "("
(* whitespace)
(=> h ,+number-pattern+)
(=> hunit (? (or "deg" "rad" "grad" "turn")))
,+separator-pattern+
(=> s ,+number-pattern+)
"%"
,+separator-pattern+
(=> l ,+number-pattern+)
"%"
(? ,+separator-pattern+
(=> a ,+number-pattern+)
(=> a% (? "%")))
(* whitespace)
")"
eos)
'i))
;; Special case for "hsl[a](H S L / A)" syntax. The slash is only
;; allowed if the HSL are separated by spaces (not commas).
(define +hsl-slash-color-regex+
(sre->irregex
`(: bos
"hsl" (? "a") "("
(* whitespace)
(=> h ,+number-pattern+)
(=> hunit (? (or "deg" "rad" "grad" "turn")))
(+ whitespace)
(=> s ,+number-pattern+)
"%"
(+ whitespace)
(=> l ,+number-pattern+)
"%"
(? (: (* whitespace) "/" (* whitespace))
(=> a ,+number-pattern+)
(=> a% (? "%")))
(* whitespace)
")"
eos)
'i))
(: %handle-hsl-match (regexp-match --> color-list))
(define (%handle-hsl-match match)
(let ((h (irregex-match-substring match 'h))
(hunit (irregex-match-substring match 'hunit))
(s (irregex-match-substring match 's))
(l (irregex-match-substring match 'l))
(a (irregex-match-substring match 'a))
(a% (irregex-match-substring match 'a%)))
(list 'hsl
(%hue h hunit)
(%percent s "%")
(%percent l "%")
(if a (%percent a a%) 1))))
(: hue (string string --> number))
(define (%hue n unit)
(let ((n (string->number n 10)))
(case (string->symbol unit)
((deg ||) n)
((rad) (* n 57.29577951308232))
((turn) (* n 360))
((grad) (cond-expand
(chicken-4
(let ((h (* n 360/400)))
(if (and (exact? n) (integer? h))
(inexact->exact h)
h)))
(else
(* n 360/400)))))))