148 lines
6.5 KiB
Common Lisp
148 lines
6.5 KiB
Common Lisp
#|
|
|
This file is a part of LASS
|
|
(c) 2014 Shirakumo http://tymoon.eu (shinmera@tymoon.eu)
|
|
Author: Nicolas Hafner <shinmera@tymoon.eu>
|
|
|#
|
|
|
|
(in-package #:org.tymoonnext.lass)
|
|
|
|
(defvar *property-functions* (make-hash-table :test 'equalp))
|
|
|
|
(defun property-function (name)
|
|
"Returns a function to process a property function of NAME, if any."
|
|
(gethash (string name) *property-functions*))
|
|
|
|
(defun (setf property-function) (function name)
|
|
"Sets FUNCTION as the new processor for the property function NAME."
|
|
(setf (gethash (string name) *property-functions*)
|
|
function))
|
|
|
|
(defun remove-property-function (name)
|
|
"Removes the property function NAME."
|
|
(remhash (string name) *property-functions*))
|
|
|
|
(defmacro define-property-function (name args &body body)
|
|
"Define a new property function NAME, accepting ARGS.
|
|
The body should return a value to use directly, if possible a string.
|
|
The results of a property-function should not be RESOVLEd.
|
|
|
|
Property functions are function calls that occur as (part of) the
|
|
value of a property. Due to ambiguity issues with a general sub-block,
|
|
property functions need to be explicitly defined and may completely
|
|
differ depending on the property. Property functions defined with this
|
|
are only the defaults available for all properties. If you want to
|
|
minimise collision probability or avoid an illegal function for a
|
|
certain property, you should define a direct method on CONSUME-ITEM
|
|
to handle the reading of the property values manually."
|
|
`(setf (property-function ,(string name))
|
|
#'(lambda ,args ,@body)))
|
|
|
|
(defmacro define-simple-property-function (name args)
|
|
"Defines a property function that returns name(arg1,arg2...).
|
|
Only required arguments are allowed."
|
|
(assert (loop for key in '(&key &optional &rest &allow-other-keys)
|
|
never (find key args)) () "Only required arguments are allowed.")
|
|
`(define-property-function ,name ,args
|
|
(format NIL ,(format NIL "~(~a~)(~{~*~~a~^,~})" name args)
|
|
,@(loop for arg in args collect `(resolve ,arg)))))
|
|
|
|
(defun resolve-function (function &rest args)
|
|
"Turns the FUNCTION with its ARGS into a properly usable property value."
|
|
(let ((resolver (property-function function)))
|
|
(if resolver
|
|
(apply resolver args)
|
|
(format NIL "~(~a~)(~{~a~^,~})" function args))))
|
|
|
|
(defmethod resolve ((thing list))
|
|
(apply #'resolve-function (car thing) (cdr thing)))
|
|
|
|
;; We redefine the method here in order to delegate to property functions
|
|
;; if they do actually exist.
|
|
(defmethod consume-item ((property symbol) readable-list)
|
|
(if (keywordp property)
|
|
(values
|
|
(let ((propvals ()))
|
|
(loop for (next fullp) = (multiple-value-list (peek readable-list))
|
|
while fullp
|
|
do (typecase next
|
|
(keyword (return))
|
|
(list
|
|
(or
|
|
(unless (listp (car next))
|
|
(let ((resolver (property-function (car next))))
|
|
(when resolver
|
|
(push (consume readable-list) propvals))))
|
|
(return)))
|
|
(T (push (consume readable-list) propvals))))
|
|
(cons property (nreverse propvals)))
|
|
NIL)
|
|
(call-next-method)))
|
|
|
|
(defmacro define-primitive-property-consumer (specializer (propvals readable next) &body loop-body)
|
|
"Defines a CONSUME-ITEM method for the given item SPECIALIZER.
|
|
|
|
SPECIALIZER --- The method specializer for the item.
|
|
PROPVALS --- The list that should contain the property values.
|
|
READABLE --- The readable-list being operated on currently.
|
|
NEXT --- Bound to the next (unconsumed) item in the readable-list.
|
|
LOOP-BODY --- The body of the reading loop to execute until the readable is empty.
|
|
|
|
The return value of the loop-body is discarded. You can use (RETURN) to exit the loop,
|
|
for example for when you encounter an item you don't want to read."
|
|
(let ((property (gensym "PROPERTY"))
|
|
(fullp (gensym "FULLP")))
|
|
`(defmethod consume-item ((,property ,specializer) ,readable)
|
|
(values
|
|
(let ((,propvals ()))
|
|
(loop for (,next ,fullp) = (multiple-value-list (peek ,readable))
|
|
while ,fullp
|
|
do (progn ,@loop-body))
|
|
(cons ,property (nreverse ,propvals)))
|
|
NIL))))
|
|
|
|
(defmacro define-property-function-case (property (args) &body function-clauses)
|
|
"Defines a CONSUME-ITEM method for PROPERTY that has special handling for property-functions.
|
|
|
|
FUNCTION-CLAUSES ::= function-clause*
|
|
FUNCTION-CLAUSE ::= (function-name form*)
|
|
|
|
Each function-name is compared by STRING-EQUAL and each clause should return the
|
|
property-value to use in its place, or NIL if it should be skipped.
|
|
|
|
You can use (RETURN) in a clause body to stop reading values altogether."
|
|
(let ((propvals (gensym "PROPVALS"))
|
|
(readable (gensym "READABLE"))
|
|
(next (gensym "NEXT"))
|
|
(result (gensym "RESULT")))
|
|
`(define-primitive-property-consumer (eql ,property) (,propvals ,readable ,next)
|
|
(typecase ,next
|
|
(keyword (return))
|
|
(list
|
|
(let* ((,args (cdr ,next))
|
|
(,result
|
|
(cond ,@(loop for (func . forms) in function-clauses
|
|
for alternatives = (if (listp func) func (list func))
|
|
collect `((or ,@(loop for alt in alternatives
|
|
collect `(string-equal (car ,next) ,(string alt))))
|
|
,@forms)))))
|
|
(if ,result
|
|
(progn (push ,result ,propvals) (advance ,readable))
|
|
(return))))
|
|
(T (push (consume ,readable) ,propvals))))))
|
|
|
|
(defmacro define-simple-property-functions (property &rest funcspecs)
|
|
"Defines a CONSUME-ITEM method for PROPERTY that has special handling for property-functions.
|
|
|
|
FUNCSPECS ::= funcspec*
|
|
FUNCSPEC ::= (funcname arg* [&optional arg*] [&key arg*])
|
|
|
|
See DEFINE-PROPERTY-FUNCTION-CASE."
|
|
(let ((arglist (gensym "ARGLIST")))
|
|
`(define-property-function-case ,property (,arglist)
|
|
,@(loop for (name args) in funcspecs
|
|
collect `(,name (destructuring-bind ,args ,arglist
|
|
(format NIL ,(format NIL "~(~a~)(~~a~~@{~~@[,~~a~~]~~})" name)
|
|
,@(loop for arg in args
|
|
unless (find arg '(&optional &key))
|
|
collect `(resolve ,arg)))))))))
|