git.fiddlerwoaroof.com
Browse code

feat: add specificity calculation, refactor

Ed Langley authored on 20/11/2019 18:38:41
Showing 1 changed files
... ...
@@ -16,10 +16,37 @@
16 16
          (return (get-output-stream-string block)))))
17 17
 
18 18
 (defun read-to-block (stream)
19
+  (declare (optimize (speed 3) (safety 1)))
19 20
   (with-output-to-string (s)
20
-    (loop
21
-       until (char= #\{ (peek-char nil stream))
22
-       do (write-char (read-char stream) s))))
21
+    (labels ((initial ()
22
+               (let ((next-char (peek-char nil stream)))
23
+                 (case next-char
24
+                   (#\{ (return-from initial))
25
+                   (#\/ (maybe-comment-start))
26
+                   (t
27
+                    (write-char (read-char stream)
28
+                                s)
29
+                    (initial)))))
30
+             (maybe-comment-start ()
31
+               (let ((stash (read-char stream))
32
+                     (next-char (peek-char nil stream)))
33
+                 (case next-char
34
+                   (#\*
35
+                    (read-char stream)
36
+                    (comment)
37
+                    (initial))
38
+                   (t (unread-char stash
39
+                                   stream)))))
40
+             (comment ()
41
+               (let ((ending nil))
42
+                 (loop
43
+                   (case (read-char stream)
44
+                     (#\* (setf ending t))
45
+                     (#\/ (when ending
46
+                            (return-from comment)))
47
+                     (t (when ending
48
+                          (setf ending nil))))))))
49
+      (initial))))
23 50
 
24 51
 (defun partition (char string &key from-end)
25 52
   (let ((pos (position char string :from-end from-end)))
... ...
@@ -31,22 +58,141 @@
31 58
 
32 59
 (defun parse-rule (block)
33 60
   (remove-if-not #'car
34
-		 (mapcar (serapeum:op
35
-			   (mapcar (serapeum:op
36
-				     (and _1 (serapeum:trim-whitespace _1)))
37
-				   (partition #\: _)))
38
-			 (serapeum:split-sequence #\;
39
-						  (serapeum:collapse-whitespace block)))))
61
+                 (mapcar (serapeum:op
62
+                           (mapcar (lambda (v1)
63
+                                     (declare (ignorable v1))
64
+                                     (progn
65
+                                       (if v1
66
+                                           (serapeum:trim-whitespace v1))))
67
+                                   (partition #\: _)))
68
+                         (serapeum:split-sequence #\;
69
+                                                  (serapeum:collapse-whitespace block)))))
70
+
71
+(defmacro one-of (chars)
72
+  `(lambda (it)
73
+     (case it
74
+       (,(coerce chars 'list) t)
75
+       (t nil))))
76
+
77
+(defun split-to (pred str)
78
+  (let ((split (or (= 0 (length str))
79
+                   (position-if pred str :start 1))))
80
+    (if (and split
81
+             (/= 0 (length str)))
82
+        (values (subseq str 0 split)
83
+                (subseq str split))
84
+        (values str
85
+                nil))))
86
+
87
+(defun repeatedly (fun str &optional acc)
88
+  (declare (optimize (speed 3) (safety 1))
89
+           (type function fun))
90
+  (multiple-value-bind (head tail) (funcall fun str)
91
+    (if tail
92
+        (repeatedly fun tail
93
+                    (cons head acc))
94
+        (nreverse (cons head acc)))))
95
+
96
+(defun split-selector (selector)
97
+  (mapcan 'serapeum:tokens
98
+          (mapcan (lambda (it)
99
+                    (destructuring-bind (h . tt) (repeatedly (lambda (it)
100
+                                                               (split-to (one-of "~+>")
101
+                                                                         it))
102
+                                                             it)
103
+                      (cons h
104
+                            (when tt
105
+                              (mapcan (lambda (it)
106
+                                        (list (subseq it 0 1)
107
+                                              (subseq it 1)))
108
+                                      tt)))))
109
+                  (mapcan (lambda (it)
110
+                            (destructuring-bind (a . r) (remove ":"
111
+                                                                (coerce (fwoar.string-utils:split ":" it)
112
+                                                                        'list)
113
+                                                                :test 'equal)
114
+                              (let ((tail (mapcar (lambda (it)
115
+                                                    (serapeum:concat ":" it))
116
+                                                  r)))
117
+                                (if (equal a "")
118
+                                    tail
119
+                                    (cons a tail)))))
120
+                          (repeatedly (lambda (it)
121
+                                        (let ((first-non-ws (position-if-not 'serapeum:whitespacep
122
+                                                                             it)))
123
+                                          (split-to (one-of "#.[()")
124
+                                                    (subseq it first-non-ws))))
125
+                                      selector)))))
126
+
127
+(defun specificity (selector)
128
+  (macrolet ((matches-pseudo ((&rest types) v)
129
+               (alexandria:once-only (v)
130
+                 `(or ,@(mapcar (lambda (type)
131
+                                  `(alexandria:starts-with-subseq ,(format nil ":~a" (string-downcase type))
132
+                                                                  ,v))
133
+                                types)))))
134
+    (let ((ids 0)
135
+          (classes 0)
136
+          (elements 0))
137
+      (loop for el in selector
138
+            when (consp el)
139
+              do
140
+                 (case (car el)
141
+                   (:id (incf ids))
142
+                   (:class (incf classes))
143
+                   (:attribute (incf classes))
144
+                   (:element (incf elements))
145
+                   (:pseudo (let ((v (cadr el)))
146
+                              (cond
147
+                                ((matches-pseudo (:link :empty :only-of-type :only-child :last-of-type :first-of-type
148
+                                                  :last-child :first-child "nth-of-type(" "nth-last-child("
149
+                                                  "nth-child(" :root :indeterminate :checked :disabled "enabled"
150
+                                                  :lang :target :focus :active :hover :visited)
151
+                                                 v)
152
+                                 (incf classes))
153
+                                ((or (matches-pseudo (:before :after :first-line :first-letter)
154
+                                                     v))
155
+                                 (incf elements))
156
+                                (t (format t "~&unrecognized pseudoclass/element: ~s~%" v)))))))
157
+      (list (vector ids classes elements) selector))))
40 158
 
41 159
 (defun parse-selector (selector)
42
-  (mapcar #'serapeum:trim-whitespace
43
-	  (split-sequence:split-sequence #\, selector)))
160
+  (flet ((categorize (it)
161
+           (case (char-downcase (elt it 0))
162
+             (#\. (list :class it))
163
+             (#\@ (list :media it))
164
+             (#\[ (list :attribute it))
165
+             (#\# (list :id it))
166
+             (#\: (list :pseudo it))
167
+             (#.(loop for c
168
+                      from (char-code #\a)
169
+                        to (char-code #\z)
170
+                      collect (code-char c))
171
+              (list :element it))
172
+             (t it))))
173
+    (let ((selector (string-trim #2=#.(format nil "~c~c" #\space #\tab)
174
+                                 selector)))
175
+      (if (alexandria:starts-with-subseq "@media" selector)
176
+          (list (list #1="@media"
177
+                      (string-trim #2#(subseq selector (length #1#)))))
178
+          (mapcar (alexandria:compose (data-lens:over
179
+                                       (alexandria:compose 
180
+                                        (lambda (it)
181
+                                          (let ((it (serapeum:trim-whitespace it)))
182
+                                            (categorize it)))))
183
+                                      'split-selector)
184
+                  (split-sequence:split-sequence #\, selector))))))
44 185
 
45 186
 (defun read-rule (stream)
46
-  (cons (funcall (alexandria:compose #'parse-selector
47
-				     #'serapeum:collapse-whitespace)
48
-		 (read-to-block stream))
49
-	(parse-rule (read-block stream))))
187
+  (let ((selector (funcall (alexandria:compose #'parse-selector
188
+                                               #'serapeum:collapse-whitespace)
189
+                           (read-to-block stream)))
190
+        (rule (read-block stream)))
191
+    (cons selector
192
+          (if (equal "@media" (caar selector))
193
+              (with-input-from-string (s rule)
194
+                (parse-file s))
195
+              (parse-rule rule)))))
50 196
 
51 197
 (defun parse-file (stream)
52 198
   (loop with result = (list)
... ...
@@ -79,13 +225,13 @@
79 225
 
80 226
 (defun test-read-block ()
81 227
   (let ((strings (list "asdf cda qwer dsfa"
82
-		       (format nil "asdf fdsaf ~% asdf qwerqw~%")
83
-		       (format nil "{asdf fdsaf ~% asdf qwerqw~%}")
84
-		       (format nil "asdf fdsaf {~% asdf qwerqw~%}"))))
228
+                       (format nil "asdf fdsaf ~% asdf qwerqw~%")
229
+                       (format nil "{asdf fdsaf ~% asdf qwerqw~%}")
230
+                       (format nil "asdf fdsaf {~% asdf qwerqw~%}"))))
85 231
     (loop
86
-       for string in strings
87
-       for n from 1
88
-       do
89
-	 (with-input-from-string (s (format nil "{~a}" string))
90
-	   (format t "~&Test ~d: ~:[fail~;pass~]~%" n
91
-		   (string= string (read-block s)))))))
232
+      for string in strings
233
+      for n from 1
234
+      do
235
+         (with-input-from-string (s (format nil "{~a}" string))
236
+           (format t "~&Test ~d: ~:[fail~;pass~]~%" n
237
+                   (string= string (read-block s)))))))