Spinneret

repository·master·Indexed 19 days ago

https://github.com/ruricolist/spinneret

A modern, composable Common Lisp HTML5 generator designed to produce human-readable output. It provides a bilingual experience between Lisp and Parenscript, utilizing macros like `with-html` and `with-html-string` to generate HTML5 documents. Features include a selector-like syntax for IDs and classes, pseudo-tags for special structures, custom tag definition via `deftag`, and optional Markdown support via `spinneret/cl-markdown`.

Tokens
2.7K
Snippets
13
Records
14
Agent score
15%

What's inside Spinneret

  1. Manipulate HTML nesting depth using `*html-path*`

    master

    The variable *html-path* is the underlying storage for get-html-path. You can use let to bind *html-path* to a new list to manually simulate or preserve HTML nesting depth when splitting document generation across multiple functions.

    WARNING: Spinneret binds *html-path* with dynamic extent. If you need to inspect or store the path value safely, use get-html-path instead of accessing the variable directly.

    Example of preserving structure in sub-functions:

    (defun inner-section ()
      """Binds *HTML-PATH* to replicate the depth the output is used in."""
      (with-html-string
        (let ((*html-path* (append *html-path* '(:section :section))))
          (:h* "Heading three levels deep"))))
    
    (defun outer-section (html)
      """Uses HTML from elsewhere and embed it into a section"""
      (with-html-string
        (:section
         (:h* "Heading two levels deep")
         (:section
          (:raw html)))))
    
    (outer-section (inner-section))
    ;; <section>
    ;; <h2>Heading two levels deep</h2>
    ;; <section><h3>Heading three levels deep</h3>
    ;; </section>
    ;;</section>
    (let ((*html-path* (append *html-path* '(:section :section))))
      (:h* "Heading three levels deep"))
  2. Generate HTML with Spinneret

    master

    Spinneret allows you to generate HTML5 documents using Common Lisp macros. The primary entry point is with-html, which writes to the *html* stream, and with-html-string, which returns the generated HTML as a string.

    Tags are represented by keywords in function position. You can use a selector-like syntax for IDs and classes (e.g., :div#id :div.class). Attributes are provided as keyword-value pairs following the tag.

    (in-package #:spinneret)
    
    (defun shopping-list ()
      (with-html-string
        (:header
         (:h1 "Home page"))
        (:section
         (:p "Hello World")))) ; => <header><h1>Home page</h1></header><section><p>Hello World</p></section>
  3. Use Markdown for inline formatting

    master

    If the spinneret/cl-markdown system is loaded, strings in a function position are treated as Markdown. The Markdown is compiled and then passed to format as a control string. This is ideal for quick inline formatting like links.

    (with-html
      ("Here is some copy, with [a link](~a)" link))
  4. Use Spinneret with Parenscript

    master

    To use Spinneret in a Parenscript environment, load the spinneret/ps system.

    Behavioral Differences in Parenscript:

    • with-html returns a DocumentFragment instead of a string.
    • with-html-string is not available.
    • get-html-path is not implemented.
    • :ATTRS and :TAG are not available.
    • If Markdown support is enabled, strings in function position are parsed as Markdown, but supplying arguments triggers an error (as Parenscript lacks format).

    To embed Parenscript code inside Spinneret HTML, you must wrap the ps macro with :raw to prevent the generated JavaScript from being escaped.

    Example of embedding script:

    (with-html-string
      (:script
        (:raw (ps
                (defun greeting ()
                  (alert "Hello"))))))
    (load "spinneret/ps")
  5. Configure HTML printing style with *html-style*

    master

    You can control how Spinneret formats its output by binding the *html-style* variable. This is useful for controlling indentation and predictability.

    • :human (default): Attempts to produce human-readable, idiomatic HTML.
    • :tree: Prints every element as a block element and every run of text on a new line. This ensures all tags are explicitly closed and is highly predictable.
    (let ((*html-style* :tree) (*print-pretty* nil))
      (with-html-string
        (:div
          (:p "Text " (:a "link text") " more text"))))
    ;; => "<div><p>Text <a>link text</a> more text</p></div>"
  6. Configure attribute validation in Spinneret

    master

    Spinneret performs compile-time validation of tags and attributes.

    • Valid Tags: Standard HTML5 tags or custom elements (names starting with an ASCII alphabetic character and containing a hyphen).
    • Custom Elements: Attributes are not validated for custom elements.

    Managing Validation Warnings:

    1. Disable validation for specific prefixes (e.g., for frameworks like Angular using ng-):

      (pushnew "ng-" *unvalidated-attribute-prefixes* :test #'equal)
    2. Disable attribute validation entirely:

      (setf *unvalidated-attribute-prefixes* '(""))
    (pushnew "ng-" *unvalidated-attribute-prefixes* :test #'equal)
  7. Embed Parenscript code in Spinneret

    master

    When using the ps macro to generate JavaScript within a Spinneret HTML block, always wrap the ps call in (:raw ...) to ensure the generated code is not escaped.

    Example:

    (with-html-string
      (:div :onclick (:raw (ps (alert "Hello")))))
    ;; <div onclick="alert('Hello');"></div>
    (with-html-string
      (:script
        (:raw (ps
                (defun greeting ()
                  (alert "Hello"))))))
  8. Customize line wrapping with html-length

    master

    If Spinneret makes poor line-breaking decisions for your custom types, you can specialize the html-length generic function. This tells the pretty-printer how long the object will be when rendered.

    (defmethod html-length ((uri puri:uri))
      (length (puri:render-uri uri nil)))
  9. Create custom HTML tags with `deftag`

    master

    The deftag macro-writing macro allows you to create reusable HTML abstractions that behave like native tags. Unlike standard macros, deftag re-arranges arguments so that the body of the tag is passed as the first argument, and matching attributes are bound to keywords.

    Key Features:

    • Argument Re-ordering: The first argument is the tag body; subsequent keyword arguments are attributes.
    • Attribute Passthrough: By including an attrs argument and splicing it (e.g., ,@attrs), any unhandled attributes passed to the macro are automatically passed through to the underlying HTML element.
    • Idiomatic Syntax: It allows building "subclasses" of HTML elements that feel native to the Spinneret DSL.

    Note: If the name deftag is used as a keyword, no macro is defined; it can only be used within a with-html form.

    Example of a custom input tag:

    (deftag input (default attrs &key name label (type "text"))
      (once-only (name)
        `(progn
           (:label :for ,name ,label)
           (:input :name ,name :id ,name :type ,type
             ,@attrs
             :value (progn ,@default)))))
    
    ;; Usage:
    (input :name "why" :label "Reason" :required t :class "special" "Default")
    ;; => <label for="why">Reason</label>
    ;;    <input class="special" name="why" id="why" type="text" required value="Default">
    (deftag field (control attrs)
      `(:p ,@attrs ,@control))
  10. Check the current HTML nesting level with `get-html-path`

    master

    The get-html-path function returns a list of currently open HTML tags, ordered from the most recent (innermost) to the earliest (outermost). This is useful for conditional rendering, such as deciding whether to wrap content in a <table> tag based on whether a table is already open.

    Note that get-html-path returns a freshly-consed list every time it is called.

    Example of conditional wrapping:

    (defun tabulate (&rest rows)
      (with-html
        (flet ((tabulate ()
                 (loop for row in rows do
                   (:tr (loop for cell in row do
                     (:td cell))))))
          (if (find :table (get-html-path))
              (tabulate)
              (:table (:tbody (tabulate)))))))
    (get-html-path) ;-> '(:table :section :body :html)
  11. Interpret HTML trees at runtime

    master

    You can use interpret-html-tree to parse a Lisp s-expression representing HTML structure into actual HTML output. Note that this interpreter is still under development and supports a subset of Spinneret syntax.

    (interpret-html-tree `(:div :id "dynamic!"))
    ;; => <div id="dynamic!"></div>