1a481a69 |
(defpackage :css-norm
(:use :cl))
(in-package :css-norm)
(defun read-block (stream)
(when (char= (read-char stream) #\{)
(loop
|
548a862e |
with block = (make-string-output-stream)
with count = 0
for char = (read-char stream)
until (and (char= char #\}) (= count 0))
when (char= char #\{) do (incf count)
when (char= char #\}) do (decf count)
do (write-char char block)
finally
(return (get-output-stream-string block)))))
|
1a481a69 |
(defun read-to-block (stream)
|
6c7018df |
(declare (optimize (speed 3) (safety 1)))
|
1a481a69 |
(with-output-to-string (s)
|
6c7018df |
(labels ((initial ()
(let ((next-char (peek-char nil stream)))
(case next-char
(#\{ (return-from initial))
(#\/ (maybe-comment-start))
(t
(write-char (read-char stream)
s)
(initial)))))
(maybe-comment-start ()
(let ((stash (read-char stream))
(next-char (peek-char nil stream)))
(case next-char
(#\*
(read-char stream)
(comment)
(initial))
(t (unread-char stash
stream)))))
(comment ()
(let ((ending nil))
(loop
(case (read-char stream)
(#\* (setf ending t))
(#\/ (when ending
(return-from comment)))
(t (when ending
(setf ending nil))))))))
(initial))))
|
1a481a69 |
(defun partition (char string &key from-end)
(let ((pos (position char string :from-end from-end)))
(if pos
|
548a862e |
(list (subseq string 0 pos)
(subseq string (1+ pos)))
(list nil
string))))
|
1a481a69 |
(defun parse-rule (block)
(remove-if-not #'car
|
6c7018df |
(mapcar (serapeum:op
(mapcar (lambda (v1)
(declare (ignorable v1))
(progn
(if v1
(serapeum:trim-whitespace v1))))
(partition #\: _)))
(serapeum:split-sequence #\;
(serapeum:collapse-whitespace block)))))
(defmacro one-of (chars)
`(lambda (it)
(case it
(,(coerce chars 'list) t)
(t nil))))
(defun split-to (pred str)
(let ((split (or (= 0 (length str))
(position-if pred str :start 1))))
(if (and split
(/= 0 (length str)))
(values (subseq str 0 split)
(subseq str split))
(values str
nil))))
(defun repeatedly (fun str &optional acc)
(declare (optimize (speed 3) (safety 1))
(type function fun))
(multiple-value-bind (head tail) (funcall fun str)
(if tail
(repeatedly fun tail
(cons head acc))
(nreverse (cons head acc)))))
(defun split-selector (selector)
(mapcan 'serapeum:tokens
(mapcan (lambda (it)
(destructuring-bind (h . tt) (repeatedly (lambda (it)
(split-to (one-of "~+>")
it))
it)
(cons h
(when tt
(mapcan (lambda (it)
(list (subseq it 0 1)
(subseq it 1)))
tt)))))
(mapcan (lambda (it)
(destructuring-bind (a . r) (remove ":"
(coerce (fwoar.string-utils:split ":" it)
'list)
:test 'equal)
(let ((tail (mapcar (lambda (it)
(serapeum:concat ":" it))
r)))
(if (equal a "")
tail
(cons a tail)))))
(repeatedly (lambda (it)
(let ((first-non-ws (position-if-not 'serapeum:whitespacep
it)))
(split-to (one-of "#.[()")
(subseq it first-non-ws))))
selector)))))
(defun specificity (selector)
(macrolet ((matches-pseudo ((&rest types) v)
(alexandria:once-only (v)
`(or ,@(mapcar (lambda (type)
`(alexandria:starts-with-subseq ,(format nil ":~a" (string-downcase type))
,v))
types)))))
(let ((ids 0)
(classes 0)
(elements 0))
(loop for el in selector
when (consp el)
do
(case (car el)
(:id (incf ids))
(:class (incf classes))
(:attribute (incf classes))
(:element (incf elements))
(:pseudo (let ((v (cadr el)))
(cond
((matches-pseudo (:link :empty :only-of-type :only-child :last-of-type :first-of-type
:last-child :first-child "nth-of-type(" "nth-last-child("
"nth-child(" :root :indeterminate :checked :disabled "enabled"
:lang :target :focus :active :hover :visited)
v)
(incf classes))
((or (matches-pseudo (:before :after :first-line :first-letter)
v))
(incf elements))
(t (format t "~&unrecognized pseudoclass/element: ~s~%" v)))))))
(list (vector ids classes elements) selector))))
|
1a481a69 |
(defun parse-selector (selector)
|
6c7018df |
(flet ((categorize (it)
(case (char-downcase (elt it 0))
(#\. (list :class it))
(#\@ (list :media it))
(#\[ (list :attribute it))
(#\# (list :id it))
(#\: (list :pseudo it))
(#.(loop for c
from (char-code #\a)
to (char-code #\z)
collect (code-char c))
(list :element it))
(t it))))
(let ((selector (string-trim #2=#.(format nil "~c~c" #\space #\tab)
selector)))
(if (alexandria:starts-with-subseq "@media" selector)
(list (list #1="@media"
(string-trim #2#(subseq selector (length #1#)))))
(mapcar (alexandria:compose (data-lens:over
(alexandria:compose
(lambda (it)
(let ((it (serapeum:trim-whitespace it)))
(categorize it)))))
'split-selector)
(split-sequence:split-sequence #\, selector))))))
|
1a481a69 |
(defun read-rule (stream)
|
6c7018df |
(let ((selector (funcall (alexandria:compose #'parse-selector
#'serapeum:collapse-whitespace)
(read-to-block stream)))
(rule (read-block stream)))
(cons selector
(if (equal "@media" (caar selector))
(with-input-from-string (s rule)
(parse-file s))
(parse-rule rule)))))
|
1a481a69 |
(defun parse-file (stream)
(loop with result = (list)
|
548a862e |
with done = nil
until done
do
(handler-case (push (read-rule stream)
result)
(end-of-file (c) c (setf done t)))
finally
(return (nreverse result))))
|
1a481a69 |
(defun collapse-rule (rule)
(let ((selector (car rule)))
(mapcan (serapeum:op (mapcar (lambda (x) (list x _))
|
548a862e |
selector))
(cdr rule))))
|
1a481a69 |
(defun reconstitute (rules)
(loop for (selector (property value)) in rules
|
548a862e |
collect (format nil "~a { ~a: ~a; }" selector property value)))
|
1a481a69 |
(defun normalize-file (stream)
(fw.lu:let-each (:be *)
(parse-file stream)
(mapcan #'collapse-rule *)
(stable-sort * #'string< :key #'caadr)
(reconstitute *)
(serapeum:string-join * #\newline)))
(defun test-read-block ()
(let ((strings (list "asdf cda qwer dsfa"
|
6c7018df |
(format nil "asdf fdsaf ~% asdf qwerqw~%")
(format nil "{asdf fdsaf ~% asdf qwerqw~%}")
(format nil "asdf fdsaf {~% asdf qwerqw~%}"))))
|
1a481a69 |
(loop
|
6c7018df |
for string in strings
for n from 1
do
(with-input-from-string (s (format nil "{~a}" string))
(format t "~&Test ~d: ~:[fail~;pass~]~%" n
(string= string (read-block s)))))))
|