(in-package #:phos/gemtext) (defclass element () ((text :initform "" :initarg :text :accessor text :type string))) (defclass title (element) ((level :initarg :level :accessor level :type integer :documentation "The nesting level of the title. Synonymous to the HTML heading levels, i.e. level 1 is

tag, level 2 is

tag etc."))) (defclass link (element) ((url :initarg :url :accessor url :type quri:uri))) (defclass item (element) ()) (defclass paragraph (element) ()) (defclass blockquote (element) ()) (defclass verbatim (element) ((alt :initform nil :initarg :alt :accessor alt :type (or null string) :documentation "The alternative text for the verbatim block. Is usually put at the same line as the opening backquotes. Can be a programming language name or alternative text for, e.g., ASCII art."))) (defun element-p (element) (typep element 'element)) (defun title-p (title) (typep title 'title)) (defun link-p (link) (typep link 'link)) (defun item-p (item) (typep item 'item)) (defun paragraph-p (paragraph) (typep paragraph 'paragraph)) (defun blockquote-p (blockquote) (typep blockquote 'blockquote)) (defun verbatim-p (verbatim) (typep verbatim 'verbatim)) (defun make-link (url &optional text) (make-instance 'link :url (quri:uri url) :text text)) (defun parse-link (s) "Parse a line into link." (match (cl-ppcre:split "\\s+" s :limit 2) ((list url) (make-instance 'link :url (quri:uri url))) ((list url text) (make-instance 'link :url (quri:uri url) :text text)))) (defun parse-line (s) (flet ((strim (s n) (string-trim '(#\Space #\Tab) (subseq s n))) (prefix-p (prfx str) (uiop:string-prefix-p prfx str))) (cond ((prefix-p "###" s) (make-instance 'title :level 3 :text (strim s 3))) ((prefix-p "##" s) (make-instance 'title :level 2 :text (strim s 2))) ((prefix-p "#" s) (make-instance 'title :level 1 :text (strim s 1))) ((prefix-p "=>" s) (let ((s (strim s 2))) (if (string-equal s "") (make-instance 'paragraph :text "=>") (parse-link s)))) ((prefix-p "* " s) (make-instance 'item :text (strim s 1))) ((prefix-p ">" s) (make-instance 'blockquote :text (strim s 1))) (t (make-instance 'paragraph :text (strim s 0)))))) (defmacro markerp (line) `(uiop:string-prefix-p "```" ,line)) (defun parse (in) "Parse gemtext from the stream IN." (loop with doc = nil for line = (read-line in nil) unless line return (nreverse doc) do (push (if (markerp line) (loop with label = (subseq line 3) with content = nil for line = (read-line in nil) when (or (not line) (markerp line)) return (make-instance 'verbatim :alt (unless (string-equal label "") label) :text (format nil "~{~A~%~^~}" (nreverse content))) do (push line content)) (parse-line line)) doc))) (defun parse-string (str) "Parse the string STR as gemtext." (with-input-from-string (s str) (parse s))) (defgeneric unparse (obj stream) (:documentation "Print a textual representation of OBJ onto STREAM.")) (defmethod unparse ((l list) stream) (dolist (item l) (unparse item stream))) (defmethod unparse ((title title) stream) (with-slots (text level) title (dotimes (_ level) (format stream "#")) (format stream " ~a~%" text))) (defmethod unparse ((link link) stream) (with-slots (url text) link (format stream "=> ~a ~a~%" url text))) (defmethod unparse ((item item) stream) (with-slots (text) item (format stream "* ~a~%" text))) (defmethod unparse ((p paragraph) stream) (with-slots (text) p (format stream "~a~%" text))) (defmethod unparse ((v verbatim) stream) (with-slots (alt text) v (format stream "```~a~%~a```~%" alt text))) (defmethod unparse ((b blockquote) stream) (with-slots (text) b (format stream "> ~a~%" text))) (defgeneric line-eq (a b) (:documentation "t if the lines A and B are equals.") (:method-combination and)) (defmethod line-eq and ((a element) (b element)) (and (eq (type-of a) (type-of b)) (equal (text a) (text b)))) (defmethod line-eq and ((a title) (b title)) (eq (level a) (level b))) (defmethod line-eq and ((a link) (b link)) (quri:uri-equal (url a) (url b))) (defmethod line-eq and ((a verbatim) (b verbatim)) (equal (alt a) (alt b)))