git.fiddlerwoaroof.com
Raw Blame History
(defpackage :css-norm
  (:use :cl))
(in-package :css-norm)

(defun read-block (stream)
  (when (char= (read-char stream) #\{)
    (loop
      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)))))

(defun read-to-block (stream)
  (declare (optimize (speed 3) (safety 1)))
  (with-output-to-string (s)
    (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))))

(defun partition (char string &key from-end)
  (let ((pos (position char string :from-end from-end)))
    (if pos
        (list (subseq string 0 pos)
              (subseq string (1+ pos)))
        (list nil
              string))))

(defun parse-rule (block)
  (remove-if-not #'car
                 (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))))

(defun parse-selector (selector)
  (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))))))

(defun read-rule (stream)
  (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)))))

(defun parse-file (stream)
  (loop with result = (list)
        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))))

(defun collapse-rule (rule)
  (let ((selector (car rule)))
    (mapcan (serapeum:op (mapcar (lambda (x) (list x _))
                                 selector))
            (cdr rule))))

(defun reconstitute (rules)
  (loop for (selector (property value)) in rules
        collect (format nil "~a { ~a: ~a; }" selector property value)))

(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"
                       (format nil "asdf fdsaf ~% asdf qwerqw~%")
                       (format nil "{asdf fdsaf ~% asdf qwerqw~%}")
                       (format nil "asdf fdsaf {~% asdf qwerqw~%}"))))
    (loop
      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)))))))