├── .gitattributes ├── cl-markdown.lisp ├── dynamic.lisp ├── .github └── workflows │ └── ci.yml ├── spinneret.lisp ├── LICENSE.txt ├── special.lisp ├── interpret.lisp ├── spinneret.asd ├── syntax.lisp ├── package.lisp ├── deftag.lisp ├── stream.lisp ├── ps.lisp ├── functions.lisp ├── compile.lisp ├── run.lisp ├── tags.lisp ├── README.md └── tests.lisp /.gitattributes: -------------------------------------------------------------------------------- 1 | # CRLF breaks the tilde-newline format directive. 2 | *.lisp text eol=lf 3 | -------------------------------------------------------------------------------- /cl-markdown.lisp: -------------------------------------------------------------------------------- 1 | ;; We define this package to make ASDF package-inferred 2 | ;; systems happy 3 | (defpackage #:spinneret/cl-markdown 4 | (:use #:cl)) 5 | (in-package #:spinneret/cl-markdown) 6 | 7 | ;; Here we redefine a function inside of the main 8 | ;; package, to change it's behavior 9 | (defun spinneret::parse-as-markdown (string) 10 | "Expand STRING as markdown only if it contains markdown." 11 | (declare (string string)) 12 | (let ((expansion 13 | (with-output-to-string (s) 14 | (let (markdown:*parse-active-functions* 15 | markdown:*render-active-functions*) 16 | (markdown:markdown string 17 | :stream s 18 | :format :html))))) 19 | (if (search string expansion) 20 | string 21 | (if (find #\Newline string) 22 | expansion 23 | (spinneret::trim-ends "
" expansion "
"))))) 24 | -------------------------------------------------------------------------------- /dynamic.lisp: -------------------------------------------------------------------------------- 1 | (in-package :spinneret) 2 | 3 | (defun expand-dynamic-tag (&rest args) 4 | `(dynamic-tag ,@args)) 5 | 6 | (deftag dynamic-tag (body attrs &key name) 7 | (unless name 8 | (error "No tag name")) 9 | (let ((empty? (null body)) 10 | (thunk (gensym (string 'dynamic-tag-thunk))) 11 | (attrs (escape-attrs nil attrs))) 12 | `(prog1 nil 13 | (flet ((,thunk () 14 | ,@(loop for expr in body 15 | collect `(catch-output ,expr)))) 16 | (declare (dynamic-extent #',thunk)) 17 | (dynamic-tag* ,name 18 | (list ,@attrs) 19 | #',thunk 20 | ,empty?))))) 21 | 22 | (defun expand-h* (&rest args) 23 | (if *interpret* 24 | (cons (heading-depth-heading) 25 | args) 26 | `(h* ,@args))) 27 | 28 | (deftag h* (body attrs &key) 29 | `(dynamic-tag 30 | :name (heading-depth-heading) 31 | ,@attrs 32 | ,@body)) 33 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | GITHUB_WORKSPACE: $HOME/common-lisp/spinneret 7 | 8 | jobs: 9 | test: 10 | name: ${{ matrix.lisp }} on ${{ matrix.os }} 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | lisp: [sbcl-bin, ccl-bin/1.12.1] 15 | os: [ubuntu-latest, macOS-13] # macos-latest when Clozure supports arm64? 16 | 17 | steps: 18 | - uses: actions/checkout@v1 19 | - name: Install Roswell 20 | env: 21 | LISP: ${{ matrix.lisp }} 22 | run: | 23 | curl -L https://raw.githubusercontent.com/roswell/roswell/master/scripts/install-for-ci.sh | sh 24 | - name: Install ci-utils 25 | run: ros install ci-utils 26 | - name: Run tests 27 | run: | 28 | PATH="~/.roswell/bin:$PATH" 29 | run-fiveam -l spinneret/tests 'spinneret.tests::run-tests' 30 | - name: Run compile-bundle-op 31 | run: | 32 | ros run 33 | -------------------------------------------------------------------------------- /spinneret.lisp: -------------------------------------------------------------------------------- 1 | ;;;; spinneret.lisp 2 | 3 | (in-package #:spinneret) 4 | 5 | (define-condition spinneret-error (error) 6 | ()) 7 | 8 | (define-condition no-such-tag (spinneret-error) 9 | ((name :initarg :name)) 10 | (:report (lambda (c s) 11 | (with-slots (name) c 12 | (format s "No such HTML tag: ~a" name))))) 13 | 14 | ;;;; The exported macros. 15 | 16 | (defun get-html-path () 17 | "Return a copy of *HTML-PATH*. 18 | This is necessary because *HTML-PATH* itself is stack-allocated." 19 | (copy-list *html-path*)) 20 | 21 | (defmacro with-html (&body body &environment env) 22 | "Interpret BODY as HTML. Consult README.txt for the syntax." 23 | `(let ((*html* (ensure-html-stream *html*))) 24 | ,(if (and (null (cdr body)) (atom (car body))) 25 | (car body) 26 | `(progn ,@(parse-html body env))))) 27 | 28 | (defmacro with-html-string (&body body) 29 | "Like WITH-HTML, but capture the output as a string." 30 | `(with-output-to-string (*html*) 31 | (with-html ,@body))) 32 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Paul M. Rodriguez 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /special.lisp: -------------------------------------------------------------------------------- 1 | (in-package #:spinneret) 2 | 3 | (declaim (stream *html*)) 4 | 5 | (defparameter *html* (make-synonym-stream '*standard-output*) 6 | "Output stream for HTML generation.") 7 | 8 | (declaim (string *html-lang* *html-charset*)) 9 | 10 | (defparameter *html-lang* "en") 11 | 12 | (defparameter *html-charset* "UTF-8") 13 | 14 | (declaim (type (integer -1 #.(1- most-positive-fixnum)) *depth*)) 15 | 16 | (defvar *depth* -1 17 | "Depth of the tag being output.") 18 | 19 | (defvar *indent*) 20 | 21 | (defun get-indent () 22 | (or (bound-value '*indent*) 23 | *depth*)) 24 | 25 | (defvar *pre* nil) 26 | 27 | (defparameter *fill-column* 80 28 | "Column at which to wrap text. 29 | This is always measured from the start of the tag.") 30 | 31 | (declaim (boolean *pending-space* *suppress-inserted-spaces*)) 32 | 33 | (defvar *pending-space* nil) 34 | 35 | (defvar *suppress-inserted-spaces* nil 36 | "When set to non-nil, spaces will never be inserted automatically.") 37 | 38 | (defvar *html-path* nil 39 | "List (in ascending order) of parent nodes.") 40 | (assert (null *html-path*)) 41 | 42 | (defvar *html-style* :human 43 | "How should we pretty-print HTML?") 44 | (declaim (type (member :human :tree) *html-style*)) 45 | 46 | (defvar *always-quote* nil 47 | "Add quotes to all attributes.") 48 | (declaim (type boolean *always-quote*)) 49 | -------------------------------------------------------------------------------- /interpret.lisp: -------------------------------------------------------------------------------- 1 | (in-package :spinneret) 2 | 3 | (defun interpret-html-tree (tree &key 4 | (stream *html*) 5 | ((:style *html-style*) :tree)) 6 | "Interpet TREE as HTML. 7 | The syntax used is roughly that of Spinneret. 8 | " 9 | (let ((*html* stream)) 10 | (labels ((interpret-html-tree (tree &optional expanded) 11 | (match tree 12 | ;; Handle the (:tag :name "mytag") syntax for dynamic tags. 13 | ((list* (and _ (eql :tag)) attrs-and-body) 14 | (multiple-value-bind (attrs body) 15 | (parse-leading-keywords attrs-and-body) 16 | (let ((name (getf attrs :name))) 17 | (unless name 18 | (error "No name for dynamic tag: ~a" tree)) 19 | (interpret-html-tree 20 | `(,name ,@(remove-from-plist attrs :name) 21 | ,@body))))) 22 | ((list* (and tag (type keyword)) attrs-and-body) 23 | (if-let (expander 24 | (and (not expanded) 25 | (pseudotag-expander tag))) 26 | ;; Handle interpreting a pseudotag. 27 | (interpret-html-tree 28 | (let ((*interpret* t)) 29 | (apply expander attrs-and-body)) 30 | t) 31 | (receive (tag attrs body) 32 | (tag-parts tree) 33 | (dynamic-tag :name tag :attrs attrs 34 | (mapc #'interpret-html-tree body) 35 | nil)))) 36 | (otherwise 37 | (html tree))))) 38 | (interpret-html-tree tree)))) 39 | -------------------------------------------------------------------------------- /spinneret.asd: -------------------------------------------------------------------------------- 1 | (defsystem "spinneret" 2 | :description "Common Lisp HTML5 generator." 3 | :version "3.0" 4 | :author "Paul M. RodriguezText link text more text 131 |
139 | Text 140 | 141 | link text 142 | 143 | more text 144 |
145 |Text link text more text
293 | ``` 294 | 295 | Similarly, the pseudo-tag :TAG allows you to select a tag at run time. 296 | 297 | ```common-lisp 298 | (:tag :name "div" 299 | (:tag :name "p" 300 | (:tag :name "span" 301 | "Hello."))) 302 | ≡ (:div (:p (:span "Hello"))) 303 | ``` 304 | 305 | Note that :TAG only allows you to *select* a tag, not *create* one. 306 | The tag must still be one that is known to Spinneret to be valid. (That is, either defined as part of HTML or matching the requirements for a custom element.) 307 | 308 | For maximum dynamicity, you can combine :TAG and :ATTRS: 309 | 310 | ```common-lisp 311 | (:tag :name "div" :attrs (list :id "dynamic!")) 312 | =>
313 | ``` 314 | 315 | ### Interpreting trees 316 | 317 | For the *ne plus ultra* of flexibility, you can interpret trees at runtime using a subset of Spinneret syntax: 318 | 319 | ```common-lisp 320 | (interpret-html-tree `(:div :id "dynamic!")) 321 | => 322 | ``` 323 | 324 | The interpreter is still under development; it supports most but not yet all Spinneret syntax. 325 | 326 | ### Markdown 327 | 328 | If the additional system `spinneret/cl-markdown` is loaded, then a 329 | string in function position is first compiled as Markdown (using 330 | [CL-MARKDOWN][]), then passed to `format` as a control string and 331 | applied to its arguments. 332 | 333 | This is useful for inline formatting, like links, where sexps would be 334 | clumsy: 335 | 336 | ```common-lisp 337 | (with-html 338 | ("Here is some copy, with [a link](~a)" link)) 339 | 340 | (with-html 341 | (:span "Here is some copy, with " 342 | (:a :href link "a link."))) 343 | ``` 344 | 345 | ## `get-html-path` 346 | 347 | Sometimes it is useful for a piece of HTML-generating code to know 348 | where in the document it appears. You might, for example, want to 349 | define a `tabulate` function that prints list-of-lists as rows of 350 | cells, but only prints the surrounding `" 56 | (with-html-string 57 | (:p :dataset (:duck (dolomphious) :fish 'fizzgigious 58 | :spoon "runcible")))))))) 59 | 60 | (test attrs 61 | (without-pretty-printing 62 | (is (equal 63 | "
bar" 64 | (let ((attrs '(:foo "bar" :baz "quux"))) 65 | (with-html-string (:p :attrs attrs "bar"))))))) 66 | 67 | (defun bigtable (&optional (*html* *html*)) 68 | (with-html 69 | (:table 70 | (dotimes (i 1000) 71 | (:tr (dotimes (i 10) 72 | (:td (1+ i)))))))) 73 | 74 | (test bigtable 75 | (flet ((bt (msg) 76 | (let ((start (get-internal-run-time))) 77 | (with-output-to-string (*html*) 78 | (finishes (bigtable))) 79 | (let* ((end (get-internal-run-time)) 80 | (duration (- end start)) 81 | (seconds (/ duration (float internal-time-units-per-second)))) 82 | (format t "~&Bigtable benchmark ~a: ~d second~:p~%" msg seconds))))) 83 | (let ((*print-pretty* t) 84 | (*html-style* :human)) 85 | (bt "with pretty printing")) 86 | (let ((*print-pretty* t) 87 | (*html-style* :tree)) 88 | (bt "with pretty printing (tree style)")) 89 | (let ((*print-pretty* nil) 90 | (*html-style* :human)) 91 | (bt "without pretty printing")) 92 | (let ((*print-pretty* nil) 93 | (*html-style* :tree)) 94 | (bt "without pretty printing (tree style)")))) 95 | 96 | (defun readme-example () 97 | (with-pretty-printing 98 | (let* ((user-name "John Q. Lisper") 99 | (last-login "12th Never") 100 | (shopping-list 101 | '("Atmospheric ponds" 102 | "Electric gumption socks" 103 | "Mrs. Leland's embyronic television combustion" 104 | "Savage gymnatic aggressors" 105 | "Pharmaceutical pianos" 106 | "Intravenous retribution champions")) 107 | (amounts '(10 6 4 9 6 9))) 108 | (with-html 109 | (:doctype) 110 | (:html 111 | (:head 112 | (:title "Home page")) 113 | (:body 114 | (:header 115 | (:h1 "Home page")) 116 | (:section 117 | ("~A, here is *your* shopping list: " user-name) 118 | (:ol (loop for item in shopping-list 119 | for amount in amounts 120 | do (:li amount item)))) 121 | (:footer ("Last login: ~A" last-login)))))))) 122 | 123 | (defun readme-example-string () 124 | (with-output-to-string (*html*) 125 | (readme-example))) 126 | 127 | (test readme-example 128 | (with-pretty-printing 129 | (let* ((expected-string 130 | (format nil "~ 131 | 132 | 133 |
134 | 135 |hi there") 204 | (let ((*print-pretty* t)) 205 | (spinneret:with-html-string 206 | (:p "hi " (:span "there")))))))) 207 | 208 | (test null-attr 209 | (without-pretty-printing 210 | (is (equal (with-html-string (:li :class nil "Hello")) 211 | "
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor 254 | incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis 255 | nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 256 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu 257 | fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in 258 | culpa qui officia deserunt mollit anim id est laborum.Hello Lorem ipsum dolor sit amet, consectetur adipiscing 260 | elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim 261 | ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea 262 | commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit 263 | esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat 264 | non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 265 |
Hello 297 | world 298 |
372 | 373 | Forgot?") 374 | (with-html-string 375 | (:p 376 | (:button "Log in") 377 | (:a :href "#" "Forgot?"))))))) 378 | 379 | (test empty-tags-on-same-line 380 | (with-pretty-printing 381 | (is (visually-equal 382 | (format nil "~ 383 |
" 412 | (with-html-string 413 | (:p :class :foo)))) 414 | (is (equal "" 415 | (with-html-string 416 | (:link :rel :stylesheet)))))) 417 | 418 | (test dynamic-tags 419 | (with-pretty-printing 420 | (is (visually-equal 421 | (with-html-string 422 | (:div 423 | (:section 424 | (:h2 425 | (:p "hello"))))) 426 | (with-html-string 427 | (:div 428 | (:section 429 | (:tag :name :h2 430 | (:p "hello"))))))))) 431 | 432 | (test h* 433 | (with-pretty-printing 434 | (is (visually-equal 435 | (format nil "~ 436 |
437 |...
439 | ...
441 | ...
443 | ...
445 | ...
448 | This is a second-level heading
442 | This is another second-level heading
444 | This is a third-level heading
447 |
... 471 |
... 473 | ") 474 | (with-html-string 475 | (:body 476 | (:h* "This is a top level heading") 477 | (:p "...") 478 | (let ((*html-path* (append *html-path* '(:section)))) 479 | (:h* "This is a second-level tricked by *HTML-PATH*") 480 | (:p "..."))))))) 481 | 482 | 483 | (test print-tree 484 | (with-pretty-printing 485 | (is (visually-equal 486 | (format nil "~ 487 |
Text link text more text 489 |
499 | Text 500 | 501 | link text 502 | 503 | more text 504 |
505 |hello
world" 535 | (let ((spinneret:*html-style* :tree) 536 | (*print-pretty* nil)) 537 | (spinneret:with-html-string 538 | (:p "hello") 539 | (:span "world")))))) 540 | 541 | 542 | (test raw-shouldnt-pretty-print-its-content 543 | (is (visually-equal 544 | "Very very very very very very very very very very very very very very very very very very very very very very very very long line" 545 | (with-html-string 546 | (:raw "Very very very very very very very very very very very very very very very very very very very very very very very very long line"))))) 547 | 548 | (test valid-custom-element-names 549 | (is (not (valid-custom-element-name? :x))) 550 | (is (not (valid-custom-element-name? :-))) 551 | (is (not (valid-custom-element-name? :-a))) 552 | (is (valid-custom-element-name? :a-)) 553 | (is (not (valid-custom-element-name? (make-keyword "a"))))) 554 | 555 | (test literal-custom-element-names 556 | (signals error 557 | (eval 558 | '(with-html-string 559 | (:xy "Hello")))) 560 | 561 | (finishes 562 | (eval 563 | '(with-html-string 564 | (:x-y "Hello"))))) 565 | 566 | (test dynamic-custom-element-names 567 | (signals error 568 | (eval 569 | '(with-html-string 570 | (:tag :name "xy" "Hello")))) 571 | (finishes 572 | (eval 573 | '(with-html-string 574 | (:tag :name "x-y" "Hello"))))) 575 | 576 | (test function-space-discrepancy 577 | (let ((*html-style* :human) 578 | (*print-pretty* t)) 579 | (is (equal 580 | (with-html-string 581 | (:p "foo" 582 | "bar" 583 | (values "baz"))) 584 | (with-html-string 585 | (:p (values "foo") 586 | "bar" 587 | "baz")))) 588 | (is (visually-equal 589 | (with-html-string 590 | (:p "foo" 591 | "bar" 592 | "baz")) 593 | (with-html-string 594 | (:p (values "foo") 595 | (values "bar") 596 | (values "baz"))))))) 597 | 598 | (test literal-pathnames 599 | (finishes 600 | (with-html-string 601 | (:html 602 | (:head 603 | (:link #p"styles.css" :type "text/css")))))) 604 | 605 | (test pre-closing-tag 606 | (is 607 | (visually-equal 608 | "verbatim line one 610 | verbatim line two611 |
Some following stuff 612 |
foo 624 | bar" 625 | (with-html-string (:pre "foo" #\Newline "b" "a" "r"))))) 626 | 627 | (test pre-no-spaces-format 628 | (is 629 | (visually-equal 630 | "
foo 631 | bar" 632 | (with-html-string (:pre ("foo ~A~A~A~A" #\Newline "b" "a" "r")))))) 633 | 634 | (test pre-code 635 | (let ((*print-pretty* t)) 636 | (is 637 | (visually-equal 638 | #.(format nil "~ 639 |
(defun blah ()
640 | (+ 1 2))")
641 | (spinneret:with-html-string
642 | (:pre
643 | (:code "(defun blah ()
644 | (+ 1 2))")))))))
645 |
646 | (test heading-depth
647 | (let ((*print-pretty* nil))
648 | (is (search "h1" (spinneret:with-html-string (:h*))))
649 | (is (search "h2" (spinneret:with-html-string (:section (:h*)))))))
650 |
651 | (test ps-attributes
652 | (is (not (search "classvar()"
653 | (ps:ps
654 | (let ((classvar "myclass"))
655 | (spinneret:with-html
656 | (:div#myid :class classvar
657 | (:p "lorem ipsum")))))))))
658 |
659 | (test double-cdata-close
660 | (is (equal (with-html-string
661 | (:html
662 | (:head
663 | (:script
664 | (:CDATA "foo")))))
665 | "
666 |
667 |
668 |
669 |
670 | ")))
671 |
672 | (test double-comment-close
673 | (is (equal
674 | (let (*print-pretty*)
675 | (with-html-string
676 | (:html
677 | (:!-- "something"))))
678 | "")))
679 |
680 | (test interpret-tree
681 | (is (visually-equal
682 | (with-output-to-string (*html*)
683 | (interpret-html-tree
684 | `(:ul :class "shuffle" (:li "Item1") (:li "Item2"))
685 | :stream *html*))
686 | (fmt
687 | "~
688 | She said, "'Hello', she said."")))
701 |
702 | (test escape-single-quotes-in-attributes
703 | (is (equal
704 | (let (*print-pretty*)
705 | (with-html-string
706 | (:button :onclick "window.alert('Hello, world.')" "My button")))
707 | ;; Interestingly this still works.
708 | "")))
709 |
710 | (test raw-metatag
711 | (is (equal "ahahaha"
712 | (with-output-to-string (*html*)
713 | (interpret-html-tree '(:raw "ahahaha")))))
714 | (is (search "lang=en"
715 | (with-output-to-string (*html*)
716 | (interpret-html-tree '(:html (:p "Hello"))))))
717 | (is (search "!DOCTYPE"
718 | (with-output-to-string (*html*)
719 | (interpret-html-tree '(:html (:doctype))))))
720 | (is (search "ahahaha -->"
721 | (with-output-to-string (*html*)
722 | (interpret-html-tree '(:comment "ahahaha")))))
723 | (is (search "charset=UTF-8"
724 | (with-output-to-string (*html*)
725 | (interpret-html-tree '(:html (:head))))))
726 | (is (equal " "
734 | (with-output-to-string (*html*)
735 | (interpret-html-tree '(:tag :name :p))))))
736 |
737 | (test dissect-interpreted-tag
738 | (let ((spinneret:*html-style* :tree))
739 | (is (equal
740 | (with-html-string
741 | (:div.my-class))
742 | (remove #\Newline
743 | (with-output-to-string (*html*)
744 | (interpret-html-tree '(:div.my-class))))))))
745 |
746 | (test dissect-dynamic-tag
747 | (let ((spinneret:*html-style* :tree))
748 | (is (equal
749 | (with-html-string
750 | (:tag :name :div.my-class))
751 | (remove #\Newline
752 | (with-output-to-string (*html*)
753 | (interpret-html-tree '(:div.my-class))))))))
754 |
755 | (test override-lang-defaults
756 | (let ((string
757 | (with-html-string
758 | (:html
759 | (:head)
760 | (:p)))))
761 | (is (search "lang=en" string))
762 | (is (search "charset=UTF-8" string)))
763 | (let ((string
764 | (with-html-string
765 | (:html :lang "foo"
766 | (:head
767 | (:meta :charset "some-other"))
768 | (:p)))))
769 | (is (search "lang=foo" string))
770 | (is (search "charset=some-other" string))))
771 |
772 | (test allow-allowfullscreen
773 | (finishes
774 | (let ((*html* (make-broadcast-stream)))
775 | (with-html (:iframe :width 560 :height 315 :src "example.com" :title "YouTube video player" :frameborder 0 :allow "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" :allowfullscreen t)))))
776 |
777 | (test always-quote-attributes
778 | (let ((spinneret:*html-style* :tree)
779 | (spinneret:*always-quote* t))
780 | (is (equal
781 | (with-html-string
782 | (:img :attrs (list :alt "some alt text" :src "https://test.com/image.png")))
783 | " Hello"
872 | (with-html-string
873 | (:p
874 | (:disable-html
875 | (with-thing-printing (:p "Hello")))))))
876 | (is (equal " Hello"
877 | (with-html-string
878 | (:p
879 | (with-thing-printing
880 | (:disable-html (:p "Hello"))))))))
881 |
--------------------------------------------------------------------------------
"))))
785 |
786 | (test raw-attributes
787 | (is (equal
788 | " with every form (except
817 |
")))
823 |
824 | (deftag :selfref (body attrs &key href &allow-other-keys)
825 | `(:a.selfref :href ,href ,@attrs ,@body))
826 |
827 | (test deftag-selector-syntax
828 | ;; The tag is not bound as a macro.
829 | (is (not (fboundp :selfref)))
830 | ;; The tag works.
831 | (is (equal
832 | "Example website"
833 | (with-html-string
834 | (:selfref#id :href "https://example.com" "Example website")))))
835 |
836 | (test inline-tag-leading-spaces
837 | (flet ((f (url)
838 | (with-html
839 | (:h4 (:raw " ") (:a :href url "Some Text")))))
840 | (is (equal
841 | " Some Text
"
842 | (with-html-string
843 | (f "http://short.com/"))))
844 | (is (search ">Some"
845 | (with-html-string
846 | (f "http://thisisreallyreallylonglonglonglongonwegoxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.com"))))))
847 |
848 | (test inline-tag-trailing-spaces
849 | (is (equal
850 | (spinneret:with-html-string
851 | (:span "Click the " (:a :href "https://google.com" ) "."))
852 | "Click the .")))
853 |
854 | (test html-tag-empty-string
855 | (finishes
856 | (spinneret:with-html-string (:style ""))))
857 |
858 | (test test-dataset-property-within-hyphens
859 | "Hyphenated data properties should be translated to bracketed strings
860 | with underscores."
861 | (is (search
862 | "dataset['x_y']"
863 | (ps:ps (with-html (:div :data-x-y "z"))))))
864 |
865 | (defmacro with-thing-printing ((&key p) &body body)
866 | (declare (ignore body))
867 | `(progn (princ ,p *html*)
868 | nil))
869 |
870 | (test test-disable-html
871 | (is (equal "