#| This file is a part of LASS (c) 2014 Shirakumo http://tymoon.eu (shinmera@tymoon.eu) Author: Nicolas Hafner |# (in-package #:org.tymoonnext.lass) (defvar *vars* (make-hash-table) "Special variable containing LASS-environment variables. See the definition of the LET block.") (defun read-to-vector (file) (with-open-file (stream file :element-type '(unsigned-byte 8)) (let ((length (file-length stream))) (assert length) (let ((result (make-array length :element-type '(unsigned-byte 8)))) (read-sequence result stream) result)))) (defgeneric resolve (thing) (:documentation "Resolves THING to a value that makes sense for LASS. By default the following types are handled: NULL: NIL STRING: the THING itself ARRAY: the variable stored in *VARS* under THING KEYWORD: Colon-prefixed, downcased symbol-name of THING SYMBOL: Downcased symbol-name of THING PATHNAME: If designating an image, base64 encoded inline image data. T: PRINC-TO-STRING of THING") (:method ((thing null)) NIL) (:method ((thing string)) thing) (:method ((thing array)) (gethash (aref thing 0) *vars*)) (:method ((thing symbol)) (if (keywordp thing) (format NIL ":~a" (string-downcase thing)) (string-downcase thing))) (:method ((file pathname)) (let ((type (mimes:mime-lookup file))) (if (and (< 5 (length type)) (string= type "image" :end1 5)) (format NIL "url('data:~a;base64,~a')" type (base64:usb8-array-to-base64-string (read-to-vector file))) (error "Don't know how to resolve files of type ~a" (or type file))))) (:method ((thing T)) (princ-to-string thing))) (defun make-property (property &optional value) "Creates a property object with PROPERTY as its key and VALUE as its value." (list :property property value)) (defun make-block (selector values) "Creates a block object with SELECTOR and VALUES." (list* :block (list* :selector selector) values)) (defun make-superblock (type selector blocks) "Creates a block object that can contain other blocks, such as @media, etc." (list* :superblock type (when selector (list* :selector selector)) blocks)) (defgeneric compile-property (key value) (:documentation "Compile a property of KEY and VALUE to a list of property objects. By default, the following cases are handled: (T LIST) A list is created with one property object, wherein the property-value is the Space-concatenated list of RESOLVEd VALUEs. The KEY is DOWNCASEd. (T T) A list is created with one property object, wherein the property-value is the RESOLVEd VALUE. The KEY is DOWNCASEd. Special handling of properties may occur. See DEFINE-SPECIAL-PROPERTY") (:method (key (value list)) (list (make-property (string-downcase key) (format NIL "~{~a~^ ~}" (mapcar #'resolve value))))) (:method (key value) (list (make-property (string-downcase key) (resolve value))))) ;; THIS IS SOME PRETTY SHODDY MAGIC CODE HERE ;; BEWARE OF DRAGONS AND ALL THAT ;; YOU HAVE BEEN WARNED. ;; ;; Can't wait for bugs about this to hit me down the line. (defgeneric compile-constraint (func args) (:documentation "Compiles a constraint of type FUNC with arguments ARGS to a list of alternative selectors. By default, the following cases are handled: (T T) Concatenates its ARGS together with spaces. Preserves OR combinations. (NULL NULL) Returns NIL (T NULL) Returns FUNC (:OR T) Passes all ARGS to COMPILE-SELECTOR individually and then APPENDS all the results together. (:AND T) Concatenates its ARGS together without spaces. Preserves OR combinations. Special handling of constraints may occur. See DEFINE-SPECIAL-SELECTOR.") (:method (func args) (let ((cfunc (cond ((listp func) (if (symbolp (first func)) (compile-selector func) func)) (T (list (resolve func))))) (cargs (compile-selector (car args)))) (loop with result = () for func in cfunc do (loop for arg in cargs do (push (list :constraint :combine " " func arg) result)) finally (return (compile-constraint (nreverse result) (cdr args)))))) (:method ((func null) (args null)) NIL) (:method (func (args null)) func) (:method ((func (eql :constraint)) args) (list (list* func args))) (:method ((func (eql :or)) args) (apply #'append (mapcar #'compile-selector args))) (:method ((func (eql :and)) args) (when args (if (cdr args) (let ((cfunc (compile-selector (first args))) (cargs (compile-selector (second args)))) (loop with result = () for func in cfunc do (loop for arg in cargs do (push (list :constraint :combine "" func arg) result)) finally (return (compile-constraint :and (cons (cons :OR (nreverse result)) (cddr args)))))) (if (and (listp (first args)) (eql (first (first args)) :OR)) (rest (first args)) args))))) (defgeneric compile-selector (selector) (:documentation "Compiles the SELECTOR form into a list of alternative selectors. By default, the following cases are handled: (NULL) Returns NIL. (LIST) Calls COMPILE-CONSTRAINT with the SELECTOR's CAR and CDR. (T) Returns a list with the RESOLVEd SELECTOR.") (:method ((selector null)) NIL) (:method ((selector list)) (compile-constraint (car selector) (cdr selector))) (:method ((selector T)) (list (list :constraint :literal (resolve selector))))) (defgeneric compile-media-constraint (func args) (:method (func args) (loop for prop in (compile-property func args) collect (list* :constraint prop))) (:method ((func null) (args null)) NIL) (:method (func (args null)) func) (:method ((func (eql :constraint)) args) (list (list* func args))) (:method ((func (eql :property)) args) (list (list* :constraint func args))) (:method ((func (eql :or)) args) (loop for arg in args nconc (compile-media-query arg))) (:method ((func (eql :and)) args) (cond ((rest args) (let ((cfunc (compile-media-query (first args))) (cargs (compile-media-query (second args)))) (loop with result = () for func in cfunc do (loop for arg in cargs do (push (list :constraint :and func arg) result)) finally (return (compile-media-constraint :and (list* (list* :or (nreverse result)) (cddr args))))))) (args (compile-media-query (first args))))) (:method ((func (eql :url)) args) (list (list* :constraint :func "url" (mapcar #'resolve args)))) (:method ((func (eql :url-prefix)) args) (list (list* :constraint :func "url-prefix" (mapcar #'resolve args)))) (:method ((func (eql :domain)) args) (list (list* :constraint :func "domain" (mapcar #'resolve args)))) (:method ((func (eql :regexp)) args) (list (list* :constraint :func "regexp" (mapcar #'resolve args))))) (defgeneric compile-media-query (query) (:method ((query null)) NIL) (:method ((query list)) (compile-media-constraint (car query) (cdr query))) (:method ((query T)) (list (list :constraint :literal (resolve query))))) (defgeneric consume-item (item readable-list) (:documentation "Consumes items from READABLE-LIST as required by the ITEM. Returned should be two values, the first being a property to compile (or NIL) and the second a block to compile (or NIL). By default, the following cases are handled: (LIST) Simply returns the ITEM as a block. (SYMBOL) If it's a keyword, reads until the next keyword or list and returns that as a property to compile. With property-functions, some sublists may also be read in automatically. See DEFINE-PROPERTY-FUNCTION. If it is a regular symbol, CALL-NEXT-METHOD is invoked. (T) Signals an error.") (:method (thing readable-list) (error "Don't know what to do with ~s (not part of a property)." thing)) (:method ((subblock list) readable-list) (values NIL subblock)) (:method ((property symbol) readable-list) (if (keywordp property) (values (let ((propvals ())) (loop for (next fullp) = (multiple-value-list (peek readable-list)) while fullp do (etypecase next (keyword (return)) (list (return)) (T (push (consume readable-list) propvals)))) (cons property (nreverse propvals))) NIL) (call-next-method)))) (defgeneric compile-block (header fields) (:documentation "Compiles the block with given HEADER and FIELDS list. By default, the following case is handled: (T T) Blocks are handled in the following way: The HEADER is used as a selector and compiled through COMPILE-SELECTOR. Fields are semantically segregated through KEYWORDS and LISTS. Every time a KEYWORD is encountered, it is taken as the current property and all following objects until either a LIST or a KEYWORD is encountered are gathered as the property's values. Every time a LIST is encountered, it is taken as a SUB-BLOCK and is passed to COMPILE-BLOCK with the HEADER being the current block's selector prepended to the selector of the sub-block. Special handling of blocks may occur. See DEFINE-SPECIAL-BLOCK.") (:method (selector fields) (let ((selector (compile-selector selector)) (readable (make-readable-list fields)) (props ()) (blocks ())) ;; compute props and blocks (flet ((add-prop (prop) (when prop (dolist (prop (compile-property (car prop) (cdr prop))) (push prop props)))) (add-block (block) (when block (dolist (block (compile-block (list selector (car block)) (cdr block))) (push block blocks))))) (loop until (empty-p readable) for (prop block) = (multiple-value-list (consume-item (consume readable) readable)) do (add-prop prop) (add-block block))) ;; Returns list of blocks with ours consed to front (cons (make-block selector (nreverse props)) (nreverse blocks))))) (defun compile-sheet (&rest blocks) "Compiles a LASS sheet composed of BLOCKS. Each BLOCK is passed to COMPILE-BLOCK. The results thereof are appended together into one list of blocks and properties." (let ((sheet ())) (dolist (block blocks) (dolist (resulting-block (compile-block (car block) (cdr block))) (push resulting-block sheet))) (nreverse sheet)))