/pp-html

A html template language in Emacs-Lisp

Primary LanguageEmacs LispGNU General Public License v3.0GPL-3.0

English Doc | 中文文档

Table of Contents

Introduction

pp-html is a HTML template library in emacs-lisp. The idea comes from Liquid template language which includes three main parts: object, tag and filter. It is convenient to generate simple HTML code or complex HTML page by writing elisp S expression in the form of pp-html syntax. It is worth mentioning that :include and :extend tag make it possible to build HTML pages by module and reuse HTML blocks.

Installation

Clone pp-html’s github repo to local directory:

$ git clone https://github.com/Kinneyzhang/pp-html.git <path-to-pp-html>

Install some dependencies: dash, s and web-mode.

Then, add the following two lines in your emacs configuration:

(add-to-list 'load-path "<path-to-pp-html>")
(require 'pp-html)

Usage

Basic

pp-html use elisp’s S expression to output html code and use pp-html function to evaluate the S expression. Following are some examples for readers’ well understanding of the usage of pp-html.

Single S expression

The syntax of single sexp is (element :attribute value :attribute value ... content) . In sexp, html element is necessary and others are optional. Pairs of attribute and value should be organized in the form of elisp plist. Specially, id and class attribute can be write in the style of css selector. The char ‘.’ represent ‘class’ and ‘@’ represent ‘id’ (without using ‘#’ because it’s a special char in elisp syntax). For some attribute without value, such as ‘async’ ‘sync’, there are two forms. One is (:attr nil) , another is (:attr) . The latter one has a condition: it must not be the last attribute.

Belows are some examples.

(pp-html '(a "content"))
(pp-html '(a @id .class))
(pp-html '(a :id "id" :class "class"))
(pp-html '(a .test :href "url" :target "_blank" "content"))
(pp-html '(link :async :rel "stylesheet" :href "url" :async nil))
<a>content</a>
<a id="id" class="class"></a>
<a id="id" class="class"></a>
<a class="test" href="url" target="_blank">content</a>
<link async rel="stylesheet" href "url" async/>

Paratactic S expression

Some paratactic sexps should be included by parentheses.

(pp-html
 '((div .div1 "div-content")
   (p "paragraph")
   (a :href "url" "a-content")
   (img :src "path")
   (ul .org-ul
       (li "1")
       (li "2")
       (li "3"))))
<div class="div1">div-content</div>
<p>paragraph</p>
<a href="url">a-content</a>
<img src="path"/>
<ul class="org-ul">
  <li>1</li>
  <li>2</li>
  <li>3</li>
</ul>

Nested S expression

Nested sexp represents for nested html element.

(pp-html
 '(div .container
       (div .row
            (div .col-8
                 (p "paragraph 1"))
            (div .col-4
                 (p "paragraph 2")))))
<div class="container">
  <div class="row">
    <div class="col-8">
      <p>paragraph 1</p>
    </div>
    <div class="col-4">
      <p>paragraph 2</p>
    </div>
  </div>
</div>

Objects

Objects tell pp-html where to show content in a page. It includes three types: variable evaluation, object’s attribute evaluation and function evaluation. Use function pp-html-eval to evaluate object.

Variable evaluation

Variables are denoted by char ‘$’: $var.

(let ((var1 "happy hacking emacs"))
  (pp-html-eval '$var1))
happy hacking emacs

Variables can be used in any part of sexp.

(let ((url "https://geekinney.com/")
      (name "Geekblog"))
  (pp-html '(a :href $url $name)))
<a href="https://geekinney.com/">Geekblog</a>

Object’s attribute evaluation

For objects in style of elisp plist, use dot to get value.

(let ((site '(:name "Geekblog" :domain "geekinney.com" :author "Geekinney")))
  (pp-html '(div .site-info
                 (p $site.name)
                 (p $site.domain)
                 (p $site.author))))
<div class="site-info">
  <p>Geekblog</p>
  <p>geekinney.com</p>
  <p>Geekinney</p>
</div>

Function evaluation

The form of function in pp-html sexp is ($ <function> <args...>) . Arguments can be write in style of variable.

(let ((var1 "happy")
      (var2 " hacking"))
  (pp-html-eval '($ concat $var1 $var2 " emacs")))
happy hacking emacs

The arguments can also be functions. There are two equal forms.

(let ((var1 "now")
      (var2 " is ")
      (now '(current-time)))
  (pp-html-eval '($ concat ($ upcase $var1) $var2 ($ format-time-string "%Y-%m-%d" $now)))
  (pp-html-eval '($ concat (upcase $var1) $var2 (format-time-string "%Y-%m-%d" $now))))
NOW is 2020-05-10
NOW is 2020-05-10

The same as ‘variable evaluation’, functions can be used in any part of sexp. Now, we can use abundant emacs-lisp functions in pp-html. Bravo!

Tags

Tags create the logic and control flow for templates. They are denoted by colon and should be placed in the first position of sexp: (:tag …). Tags can be categorized into five types:

  • Variable assign
  • Control flow
  • Iteration
  • Block

Variable assign

assign

:assign create new pp-html variables, the equivalent in elisp is ‘let’ or ‘setq’.

(pp-html
 '((:assign str1 "happy"
            str2 "hacking"
            str3 "emacs")
   (p ($ concat $str1 " " $str2 " " $str3))))
<p>happy hacking emacs</p>

Control flow

Control flow tags can change the information pp-html shows using programming logic.

ifequal

Executes a block of code only if two args are equal.

  (pp-html
   '((:assign str1 "emacs"
		str2 "emacs2")
     (:ifequal $str1 $str2 (p "equal")
		 (p "not equal"))))
<p>not equal</p>

ifnotequal

Executes a block of code only if two args are not equal.

  (pp-html
   '((:assign str1 "emacs"
		str2 "emacs2")
     (:ifnotequal $str1 $str2 (p "not equal")
		 (p "equal"))))
<p>not equal</p>

if

Executes a block of code only if a certain condition is true.

(pp-html
 '((:assign bool nil)
   (:if $bool (p "true")
	    (p "false"))))
<p>false</p>

unless

The opposite of if – executes a block of code only if a certain condition is not met.

(pp-html
 '((:assign bool nil)
   (:unless $bool (p "true")
	    (p "false"))))
<p>true</p>

case and when

When the value after :when is equal to the value after :case, executes the block following.

(pp-html
 '((:assign editor "emacs")
   (:case $editor
	      (:when "vim" (p "editor vim"))
	      (:when "emacs" (p "editor emacs"))
	      (:when "vscode" (p "editor vscode")))))
<p>editor emacs</p>

cond

Try each clause until one succeeds. Each clause looks like (CONDITION BODY…). Return the value of last one in body.

(pp-html
 '((:assign case "case3")
   (:cond
    ($ string= $case "case1") (p "case1 branch")
    ($ string= $case "case2") (p "case2 branch")
    ($ string= $case "case3") (p "case3 branch")
    t (p "default branch"))))
<p>case3 branch</p>

Interation

Iteration tags run blocks of code repeatedly.

for

Repeatedly executes a block of code.

(pp-html
 '((:assign editors ("vim" "emacs" "vscode"))
   (ul
    (:for editor in $editors
          (li :id $editor $editor)))))
<ul>
  <li id="vim">vim</li>
  <li id="emacs">emacs</li>
  <li id="vscode">vscode</li>
</ul>
  • else

    Specifies a fallback case for a for loop which will run if the loop has zero length.

(pp-html
 '((:assign editors ())
   (ul
    (:for editor in $editors
	  (li :id $editor $editor)
	  (:else (li "no editor"))))))
<ul>
  <li>no editor</li>
</ul>
  • break

    Causes the loop to stop iterating when it encounters the break tag.

(pp-html
 '((:assign editors ("vim" "emacs" "vscode" "atom" "sublime text"))
   (ul
    (:for editor in $editors
	  (:ifequal $editor "atom"
		    (:break)
		    (li :id $editor $editor))
	  (:else (li "no editor"))))))
<ul>
  <li id="vim">vim</li>
  <li id="emacs">emacs</li>
  <li id="vscode">vscode</li>
</ul>
  • continue

    Causes the loop to skip the current iteration when it encounters the continue tag.

(pp-html
 '((:assign editors ("vim" "emacs" "vscode" "atom" "sublime text"))
   (ul
    (:for editor in $editors
	  (:ifequal $editor "atom"
		    (:continue)
		    (li :id $editor $editor))
	  (:else (li "no editor"))))))
<ul>
  <li id="vim">vim</li>
  <li id="emacs">emacs</li>
  <li id="vscode">vscode</li>
  <li id="sublime text">sublime text</li>
</ul>

for with parameters

  • limit

    Limits the loop to the specified number of iterations.

(pp-html
 '((:assign editors ("vim" "emacs" "vscode" "atom" "sublime text"))
   (ul
    (:for editor in $editors :limit 3
	  (li :id $editor $editor)
	  (:else (li "no editor"))))))
<ul>
  <li id="vim">vim</li>
  <li id="emacs">emacs</li>
  <li id="vscode">vscode</li>
</ul>
  • offset

    Begins the loop at the specified index.

(pp-html
 '((:assign editors ("vim" "emacs" "vscode" "atom" "sublime text"))
   (ul
    (:for editor in $editors :offset 2
	  (li :id $editor $editor)
	  (:else (li "no editor"))))))
<ul>
  <li id="vscode">vscode</li>
  <li id="atom">atom</li>
  <li id="sublime text">sublime text</li>
</ul>
  • range

    Defines a range of numbers to loop through. The range can be defined by both literal and variable numbers.

(pp-html
 '((ul
    (:for it in (3..6)
	  (li :id $it $it)
	  (:else (li "no number"))))
   (:assign max 9)
   (ul
    (:for it in (6..$max)
	  (li :id $it $it)
	  (:else (li "no number"))))
   (ul
    (:for it in (2..$max by 2)
	  (li :id $it $it)
	  (:else (li "no number"))))))
<ul>
  <li id="3">3</li>
  <li id="4">4</li>
  <li id="5">5</li>
  <li id="6">6</li>
</ul>
<ul>
  <li id="6">6</li>
  <li id="7">7</li>
  <li id="8">8</li>
  <li id="9">9</li>
</ul>
<ul>
  <li id="2">2</li>
  <li id="4">4</li>
  <li id="6">6</li>
  <li id="8">8</li>
</ul>
  • reversed

    Reverses the order of the loop.

(pp-html
 '((:assign editors ("vim" "emacs" "vscode" "atom"))
   (ul
    (:for editor in $editors :reversed
	  (li :id $editor $editor)
	  (:else (li "no editor"))))))
<ul>
  <li id="atom">atom</li>
  <li id="vscode">vscode</li>
  <li id="emacs">emacs</li>
  <li id="vim">vim</li>
</ul>

NOTE: all types of parameters can be combined together, for example:

(pp-html
 '(ul
   (:for it in (1..15 by 2) :offset 2 :limit 3 :reversed
	 (li :id $it $it)
	 (:else (li "no number")))))
<ul>
  <li id="9">9</li>
  <li id="7">7</li>
  <li id="5">5</li>
</ul>

Block

include

Include other blocks in one block.

  (setq block1
	  '(p "block1 content"
	      (a :href "url" "content")))

  (setq block2
	  '(div .block2
		(p "block2 content")
		(:include $block1)))

  (pp-html block2)
   <div class="block2">
     <p>block2 content</p>
     <p>
	block1 content
	<a href="url">content</a>
     </p>
   </div>

extend and block

Extend a block, replace the block in :block tag if has new block, otherwise extend the default one.

 (setq base-block '(p .base
			 (:block block-name (span "base content")))
	  extend-block1 '(:extend $base-block
				  (:block block-name))
	  extend-block2 '(:extend $base-block
				  (:block block-name
					  (span "extended content"))))
 (pp-html
  '((div "extend the default"
	    (:include $extend-block1))
    (div "extend with new"
	    (:include $extend-block2))))
   <div>
     extend the default
     <p class="base">
	<span>base content</span>
     </p>
   </div>
   <div>
     extend with new
     <p class="base">
	<span>extended content</span>
     </p>
   </div>

Filters

Filters change the output of a pp-html object. The form of filter is (/ <value> <:filter args> ...) . Some filters have argument and others have none, it all depends.

Customize filters

pp-html support to customize filters by yourself using pp-html-define-filter function. The function has two arguments: the name of a filter and a filter function.

   (pp-html-define-filter :add 'pp-html-filter-add)
   (defun pp-html-filter-add (value arg)
     "Add a value to a number"
     (let ((arg (if (stringp arg)
		     (string-to-number arg)
		   arg)))
	(+ value arg)))

The code above defined a filter named ‘:add’, the function is ‘pp-html-filter-add’. The name of filter function is up to you.

Built-in filters

abs: returns the absolute value of a number

(pp-html-eval '(/ -5 :abs)) ;; => 5

append: appends a list to another one

 (let ((list1 '(1 2 3))
	  (list2 '(5 6 7)))
   (pp-html-eval '(/ $list1 :append $list2))) ;; => (1 2 3 5 6 7)

at_least: limits a number to a minimum value

(pp-html-eval '(/ 3 :at_least 5)) ;; => 5

at_most: limit a number to a maximum value

(pp-html-eval '(/ 3 :at_most 5)) ;; => 3

capitalize: makes the first character of a string capitalized

(pp-html-eval '(/ "happy hacking emacs!" :capitalize)) ;; => "Happy hacking emacs!"

compact: removes any nil values from an array

(let ((lst '(nil 1 2 nil 3 4 nil)))
  (pp-html-eval '(/ $lst :compact))) ;; => (1 2 3 4)

concat: concatenates two strings and returns the concatenated value

 (let ((str1 "happy hacking ")
	  (str2 "emacs"))
   (pp-html-eval '(/ $str1 :concat $str2))) ;; => "happy hacking emacs"

date: converts a timestamp into another date format

(pp-html-eval '(/ "now" :date "%Y-%m-%d %T")) ;; => "2020-06-17 22:25:11"

default: default will show its value if the left side is nil, false, or empty

 (let ((str1 "")
	  (str2 "new value")
	  (lst1 '(1 2 3))
	  (lst2 nil))
   (pp-html-eval '(/ $str1 :default "default value")) ;; => "default value"
   (pp-html-eval '(/ $str2 :default "default value")) ;; => "new value"
   (pp-html-eval '(/ $lst1 :default (4 5 6))) ;; => (1 2 3)
   (pp-html-eval '(/ $lst2 :default (4 5 6))) ;; => (4 5 6)
   )

divided_by: divides a number by another number

(pp-html-eval '(/ 5 :divided_by 3)) ;; => 1

downcase: convert all chars in string to lower case

(pp-html-eval '(/ "HAPPY Hacking Emacs!" :downcase)) ;; => "happy hacking emacs!"

first: returns the first item of an array

(pp-html-eval '(/ (2 3 4 5) :first)) ;; => 2

floor: rounds the input down to the nearest whole number

(pp-html-eval '(/ 23.6 :floor)) ;; => 23

join: combines the items in a list into a single string using the argument as a separator

(pp-html-eval '(/ ("happy" "hacking" "emacs") :join " ")) ;; => "hacking hacking emacs"

last: returns the last item of an array

(pp-html-eval '(/ (2 3 4 5) :last)) ;; => 5

lstrip: Removes all whitespace (tabs, spaces, and newlines) from the left side of a string. It does not affect spaces between words

(pp-html-eval '(/ "  happy hacking emacs!" :lstrip)) ;; => "happy hacking emacs!"

map: creates an array of values by extracting the values of a named property from another object

(let ((map-lst '((:title "t1" :category "c1" :author "a1")
		     (:title "t2" :category "c2" :author "a2")
		     (:title "t3" :category "c3" :author "a3"))))
  (pp-html-eval '(/ $map-lst :map "category"))) ;; => ("c1" "c2" "c3")

minus: subtracts a number from another number

(pp-html-eval '(/ 6 :minus 3)) ;; => 3

modulo: returns the remainder of a division operation

(pp-html-eval '(/ 5 :modulo 3)) ;; => 2

plus: adds a number to another number

(pp-html-eval '(/ 3 :plus 4)) ;; => 7

prepend: adds the specified string to the beginning of another string

(pp-html-eval '(/ "geekinney.com/" :prepend "https://")) ;; => "https://geekinney.com/"

replace: replaces every occurrence of the first argument in a string with the second argument

(let ((repl-str "emacs is a lifestyle and happy hacking emacs."))
  (pp-html-eval '(/ $repl-str :replace "emacs" "vim"))) ;; => "vim is a lifestyle and happy hacking vim."

replace_first: replaces only the first occurrence of the first argument in a string with the second argument

(let ((repl-str "emacs is a lifestyle and happy hacking emacs."))
  (pp-html-eval '(/ $repl-str :replace_first "emacs" "vim"))) ;; => "vim is a lifestyle and happy hacking emacs."

reverse: reverses the order of the items in an array

(pp-html-eval '(/ (1 2 3 4) :reverse)) ;; => (4 3 2 1)

round: rounds a number to the nearest integer

(pp-html-eval '(/ 3.6 :round)) ;; => 4

rstrip: Removes all whitespace (tabs, spaces, and newlines) from the right side of a string. It does not affect spaces between words.

(pp-html-eval '(/ "happy hacking emacs!   " :rstrip)) ;; => "happy hacking emacs!"

size: returns the number of characters in a string or the number of items in an array

(pp-html-eval '(/ "emacs" :size)) ;; => 5
(pp-html-eval '(/ (2 3 4 5) :size)) ;; => 4

slice: Return a new string whose contents are a substring of STRING. The returned string consists of the characters between index FROM (inclusive) and index TO (exclusive) of STRING. FROM and TO are zero-indexed: 0 means the first character of STRING. Negative values are counted from the end of STRING. If TO is nil, the substring runs to the end of STRING.

(pp-html-eval '(/ "happy hacking emacs!" :slice 6 -1)) ;; "hacking emacs"

sort: sorts items in an array in case-sensitive order

(pp-html-eval '(/ ("happy" "Happy" "vim" "hacking" "emacs") :sort)) ;; => ("Happy"  "emacs" "hacking" "happy" "vim")

sort_natural: sorts items in an array in none case-insensitive order

(pp-html-eval '(/ ("happy" "Happy" "vim" "hacking" "emacs") :sort_natural)) ;; => ("emacs" "hacking" "happy" "Happy" "vim")

split: Divides a string into an array using the argument as a separator. split is commonly used to convert comma-separated items from a string to an array.

(pp-html-eval '(/ "happy hacking emacs" :split " ")) ;; => ("happy" "hacking" "emacs")

strip: Removes all whitespace (tabs, spaces, and newlines) from both the left and right sides of a string. It does not affect spaces between words.

(pp-html-eval '(/ "  happy hacking emacs!   " :strip)) ;; => "happy hacing emacs!"

truncate: Shortens a string down to the number of characters passed as an argument. If the specified number of characters is less than the length of the string, an ellipsis (…) is appended to the string and is included in the character count.

(let ((trun-str "emacs is a lifestyle and happy hacing emacs"))
  (pp-html-eval '(/ $trun-str :truncate 27)) ;; => "emacs is a lifestyle and..."
  (pp-html-eval '(/ $trun-str :truncate 27 " :)")) ;; => "emacs is a lifestyle and :)"
  )

truncatewords: Shortens a string down to the number of words passed as an argument. If the specified number of words is less than the number of words in the string, an ellipsis (…) is appended to the string.

(let ((trunw-str "happy hacking emacs, cool!"))
  (pp-html-eval '(/ $trunw-str :truncatewords 3)) ;; => "happy hacking emacs..."
  (pp-html-eval '(/ $trunw-str :truncatewords 3 " :)")) ;; => "happy hacking emacs :)"
  )

uniq: removes any duplicate elements in an array

(pp-html-eval '(/ (2 3 4 3 5 5 2) :uniq)) ;; => (2 3 4 5)

upcase: convert all chars in string to upper case

(pp-html-eval '(/ "happy hacking emacs" :upcase)) ;; => "HAPPY HACKING EMACS"

Integration

Click to see an integration example.

Tips

Test and Preview

Use pp-html-test function to preview the formatted HTML generated by S expression. Use pp-html-parse function to see the S expression after processing all logic tags. The two functions are useful for test and debug.

XML support

pp-html also support print XML. Just set the second argument of pp-html to t is fine.

Not a html5 tag

If you try to print a no-html5 tag using pp-html, it will prompt an error. However, sometime, some packages have defined some no-html5 tags. How to handle it in pp-html? Just set the variable pp-html-other-html-elements which is a list.

Integrate with OrgMode

In Org file, we can use emacs-lisp source block with some parameters to generate html source code in Org or HTML, for example.

1.when export the Org file, it will generate a html page with a red background div.

#+BEGIN_SRC emacs-lisp :results value html :exports results
(pp-html '(div :style "background-color:red;" "content"))
#+END_SRC

#+RESULTS:
#+begin_export html
<div style="background-color:red;">content</div>
#+end_export

2.when export the Org file, it will generate a html page with html source code: <div style="background-color:red;">content</div>.

#+BEGIN_SRC emacs-lisp :wrap src html :exports results
(pp-html '(div :style "background-color:red;" "content"))
#+END_SRC

#+RESULTS:
#+begin_src html
<div style="background-color:red;">content</div>
#+end_src

See Working-with-Source-Code to learn more about org source block parameters.

Blog package

My personal blog site is built in the base of pp-html because it’s handy to build a blog. I will develop a blog site generator emacs package by using pp-html. Please keep watching my Github!

Plan

  • [X] Support more useful tags.
  • [X] Support more useful filters.
  • [ ] Write a function named pp-html-reverse which can parse HTML string into pp-html’s S expression form.

Acknowledgment

pp-html is the first emacs package developed by myself. During developing it, I have met many challenges. Thanks to emacs hacker in Emacs-China for your answering questions.

BTW, issues and prs are always welcome!