HyML MiNiMaL

Minimal markup language generator in Hy

HyML (acronym for Hy Markup Language) is a set of macros to generate XML, XHTML, and HTML code in Hy.

HyML MiNiMaL (ml) macro is departed from the more extensive document and validation oriented full version of HyML.

HyML MiNiMaL is meant to be used as a minimal codebase to generate XML (Extensible Markup Language) with the next features:

  1. closely resembling syntax with XML
  2. ability to mix Hy / Python program code within markup
  3. processing lists and external templates
  4. using custom variables and functions

You can use HyML MiNiMaL for:

  • static XML / XHTML / HTML content and file generation
  • render html code for the Jupyter Notebook for example
  • attach it to a server for dynamic html output generation (sHyte 0.2)
  • practice and study DSL and macro (Lisp) programming
  • challenge your imagination

To compare with full version HyML XML / HTML macros, MiNiMaL means that there is no tag name validation and no tag and attribute minimize techniques utilized. If you need them, you should see full HyML documentation.

HyML thus refers to XML, althought markup language term itself is more generic term.

Ready, steady, go!

Project is hosted at: https://github.com/markomanninen/hyml

Install

For easy install, use pip Python repository installer:

$ pip install hyml

This will install only the necessary source files for HyML, no example templates nor Jupyter Notebook files.

There are no other dependencies except Hy language upon Python of cource. If Hy does not exist on your computer, it will be installed (or updated to the version 0.12.1 or greater) at the same time.

Import

Then import MiNiMaL macros:

(require (hyml.minimal (*)))

This will load the next macros for usage

  • main markup macro: ml
  • defvar and deffun macros for custom variable and function setter
  • include macro for using templates
  • list-comp* list comprehension helper macro

Optionally ml> render macro will be loaded, if code is executed on Jupyter Notebook / IPython environment with display.HTML function available.

If you intend to use xml code indent function, you should also import it:

(import (hyml.minimal (indent)))

Run

And run the simple example:

(ml (tag :attr "value" (sub "Content")))

That should output:

<tag attr="value"><sub>Content</sub></tag>

Tests

To run basic tests, you can use Jupyter Notebook document for now.

Jupyter Notebook

If you want to play with HyML Notebook documents, you should download the whole HyML repository (or clone it with $ git clone https://github.com/markomanninen/hyml.git) to your computer. It contains all necessary templates to get everything running as presented in the HyML MiNiMaL Notebook document.

HyML MiNiMaL codebase

Because codebase for HyML MiNiMaL implementation is roughly 60 lines only, it will be provided here with structural comments for introspection. More detailed comment are available in the minimal.hy source file.


In [1]:
; eval and compile variables, constants and functions for ml, defvar, deffun, and include macros
(eval-and-compile
  ; global registry for variables and functions
  (setv variables-and-functions {})
  ; internal constants
  (def **keyword** "keyword") (def **unquote** "unquote")
  (def **splice** "unquote_splice") (def **unquote-splice** (, **unquote** **splice**))
  (def **quote** "quote") (def **quasi** "quasiquote")
  (def **quasi-quote** (, **quote** **quasi**))
  ; given two dictionaries, merge them into a new dictionary as a shallow copy
  (defn merge-two-dicts [x y]
    (if-not (or (empty? x) (empty? y))
            ; tiny optimization, if there are values in both x and y
            (do (setv z (.copy x)) (.update z y) z)
            ; else one more check to return the one that has values
            ; or maybe all were empty? then either one should be fine
            (if (empty? x) y x)))
  ; detach keywords and content from code expression
  (defn get-content-attributes [code &optional [vars-and-funcs {}]]
    (setv content [] attributes [] kwd None)
    (for [item code]
         (do (if (iterable? item)
                 (if (= (first item) **unquote**)
                     (setv item (eval (second item)
                                      (merge-two-dicts variables-and-functions vars-and-funcs)))
                     (in (first item) **quasi-quote**) (setv item (name (eval item)))))
             (if-not (keyword? item)
               (if (none? kwd)
                   (.append content (parse-mnml item vars-and-funcs))
                   (.append attributes (, kwd (parse-mnml item vars-and-funcs)))))
             (if (and (keyword? kwd) (keyword? item))
                 (.append attributes (, kwd (name kwd))))
             (if (keyword? item) (setv kwd item) (setv kwd None))))
    (if (keyword? kwd)
        (.append attributes (, kwd (name kwd))))
    (, content attributes))
  ; recursively parse expression
  (defn parse-mnml [code &optional [vars-and-funcs {}]] 
    (if (coll? code)
        (do (setv tag (catch-tag (first code)))
            (if (in tag **unquote-splice**)
                (if (= tag **unquote**)
                    (str (eval (second code) (merge-two-dicts variables-and-functions vars-and-funcs)))
                    (.join "" (map (if (empty? vars-and-funcs) parse-mnml (fn [item] (parse-mnml item vars-and-funcs)))
                                   (eval (second code) (merge-two-dicts variables-and-functions vars-and-funcs)))))
                (do (setv (, content attributes) (get-content-attributes (drop 1 code) vars-and-funcs))
                    (+ (tag-start tag attributes (empty? content))
                       (if (empty? content) ""
                           (+ (.join "" (map str content)) (+ "</" tag ">")))))))
        (if (none? code) "" (str code))))
  ; detach tag from expression
  (defn catch-tag [code]
    (if (and (iterable? code) (= (first code) **unquote**))
        (eval (second code))
        (try (name (eval code))
             (except (e Exception) (str code)))))
  ; concat attributes
  (defn tag-attributes [attr]
    (if (empty? attr) ""
        (+ " " (.join " " (list-comp
          (% "%s=\"%s\"" (, (name kwd) (name value))) [[kwd value] attr])))))
  ; create start tag
  (defn tag-start [tag-name attr short]
    (+ "<" tag-name (tag-attributes attr) (if short "/>" ">"))))
; global variable registry handler
(defmacro defvar [&rest args]
  (setv l (len args) i 0)
  (while (< i l) (do
    (assoc variables-and-functions (get args i) (get args (inc i)))
    (setv i (+ 2 i)))))
; global function registry handler
(defmacro deffun [name func]
  (assoc variables-and-functions name (eval func)))
; include functionality for template engine
(defmacro include [template]
  `(do (import [hy.importer [tokenize]])
       (with [f (open ~template)]
         (tokenize (+ "~@`(" (f.read) ")")))))
; main MiNiMaL macro to be used. passes code to parse-mnml
(defmacro ml [&rest code]
  (.join "" (map parse-mnml code)))
; 
(defmacro macro [name params &rest body]
  `(do
    (defmacro ~name ~params
      `(quote ~~@body)) None))


Out[1]:
<function <lambda> at 0x0000016BD41D58C8>

Features

Basic syntax

MiNiMaL macro syntax is simple and practically follows the rules of Hy syntax. MiNiMaL macro expression is made of the following four components:

  1. tag name
  2. tag attribute-value pairs
  3. tag text / literal content
  4. possible sub expression

Syntax of the expression consists of:

  • parentheses to define hierarchical (nested) structure of the document
  • all opened parentheses ( must have closing parentheses pair )
  • the first item of the expression is the tag name
  • next items in the expression are either:

    • tag attribute-value pairs
    • tag content wrapped with double quotes
    • sub tag expression
    • nothing at all
  • between keywords, keyword values, and content there must a whitespace separator OR expression components must be wrapped with double quotes when suitable

  • whitespace is not needed when a new expression starts or ends (opening and closing parentheses)

Only the tag name is mandatory on the expression. There is no limit on the depth of the nested levels. There is no limit on how many attribute-value pairs you want to use. Also it doesn't matter in what order you define tag content and keywords, althougt it might be easier to read for others, if the keywords are introduced first and then the content. However, all keywords are rendered in the same order they have been presented in the markup. Also content and sub nodes are rendered similarly in the given order.

Main differences to XML syntax are:

  • instead of < and > delimiters, parentheses ( and ) are used
  • there can't be a separate end tag
  • in ml macro, given expressions does not need to have be inside a single root node
  • see other possible differences by comparing HyML to wiki/XML

Special characters

In addition to basic syntax, there are three other symbols for advanced code generation. They are:

  • quasiquote `
  • unquote ~
  • unquote splice ~@

These all are symbols used in Hy macro notation, so they should be self explanatory. But to make everything clear, in the MiNiMaL macro they may look they work other way around.

Unquote (~) and unquote-splice (~@) gets you back to the Hy code evaluation mode. And quasiquote (`) sets you back to MiNiMaL macro mode. This is natural when you think that expression passed to MiNiMaL macro is a quoted code in the first place. So if you want to evaluate Hy code inside it, you need to do it inside unquote.

But let us start from the simple example first.

Simple example

Observe the simple example utilizing above features and all four components:

(tag :attr "value" (sub "Content"))

tag is the first element of the expression, so it regarded as a tag name. :attr "value" is the keyword-value (attribute-value) -pair. (sub starts a new expression. So there is no other content (or keywords) in the tag element. Sub node instead has the titlecase content "Content" given.

Running this on a console or on the Jupyter Notebook, output would be:

<tag attr="value"><sub>Content</sub></tag>

Process components with unquote syntax (~)

Any component (tag name, tag attribute / value, and tag content) can be generated instead of hardcoded to the expression.

Tag name

You can generate a tag name with Hy code by using ~ symbol:


In [2]:
(ml (~(+ "t" "a" "g"))) ; output: tag


Out[2]:
'<tag/>'

This is useful if tag names collide with Hy internal symbols and datatypes. For example, the symbol J is reserved for the complex number type. Instead of writing: (ml (J)) which produces <1j/>, you should use: (ml ("J")) or maybe (ml (~"J")).

Attribute name and value

You can generate an attribute name or a value with Hy by using ~ symbol. Generated attribute name must be a keyword however:


In [3]:
(ml (tag ~(keyword (.join "" ['a 't 't 'r])) "value")) ; output: <tag attr="value"/>


Out[3]:
'<tag attr="value"/>'

In [4]:
(ml (tag :attr ~(+ 'v 'a 'l 'u 'e))) ; output: <tag attr="value"/>


Out[4]:
'<tag attr="value"/>'

Content

You can generate content with Hy by using ~ symbol:


In [5]:
(ml (tag ~(.upper "content"))) ; output: <tag>CONTENT</tag>


Out[5]:
'<tag>CONTENT</tag>'

Using custom variables and functions

You can define custom variables and functions for the MiNiMaL macro. Variables and functions are stored on the common registry and avialble on the macro expansion. You can access predefined symbols when quoting (~) the expression.


In [6]:
; define variables with defvar macro
(defvar firstname "Dennis"
        lastname "McDonald")

; define functions with deffun macro
(deffun wholename (fn [x y] (+ y ", " x)))

; use variables and functions with unquote / unquote splice
(ml (tag ~(wholename firstname lastname)))


Out[6]:
'<tag>McDonald, Dennis</tag>'

Process lists with unquote splice syntax (~@)

Unquote-splice is a special symbol to be used with the list and the template processing. It is perhaps the most powerful feature in the MiNiMaL macro.

Generate list of items

You can use list comprehension function to generate a list of xml elements. Hy code, sub expressions, and variables / functions work inside unquote spliced expression. You need to quote a line, if it contains a sub MiNiMaL expression.


In [7]:
; generate 5 sub tags and use enumerated numeric value as a content
(ml (tag ~@(list-comp `(sub ~(str item)) [item (range 5)])))


Out[7]:
'<tag><sub>0</sub><sub>1</sub><sub>2</sub><sub>3</sub><sub>4</sub></tag>'

Using templates

Let us first show the template content existing in the external file:


In [8]:
(with [f (open "templates/note.hy")] (print (f.read)))


(note :src "https://www.w3schools.com/xml/note.xml"
  (to ~to)
  (from ~from)
  (heading ~heading)
  (body ~body))

Then we will define variables and a function to be used inside MiNiMaL macro:


In [9]:
(defvar to "Tove"
        from "Jani"
        heading "Reminder"
        body "Don't forget me this weekend!")

And finally include and render the template. Indent function is imported and used to pretty print xml:


In [10]:
(import (hyml.minimal (indent)))
(print (indent (ml ~@(include "templates/note.hy"))))


<note src="https://www.w3schools.com/xml/note.xml">
	<to>Tove</to>
	<from>Jani</from>
	<heading>Reminder</heading>
	<body>Don't forget me this weekend!</body>
</note>

Templates extra

On HyML package there is also the render-template function and the extend-template macro available via HyML.template module.

HyML.template is especially useful when embedding HyML MiNiMaL to webserver, Flask for example. Here just the basic use case is shown, more examples you can find from sHyte 0.2 HyML Edition codebase.

In practice, render-template function is a shortcut to call parse-mnml with parameters and include in sequence. The first argument in render-template is the template name, and the rest of the arguments are dictionaries to be used on the template. So this is also an alternative way of using (bypassing the usage of) defvar and deffun macros.


In [11]:
; extend-template macro
(require [hyml.template [*]])
; render-template function
(import [hyml.template [*]])
; prepare template variables and functions
(setv template-variables-and-functions {"var" "Variable 1" "func" (fn[]"Function 1")})
; render template
(render-template "render.hyml" template-variables-and-functions)


Out[11]:
'<root><var>Variable 1</var><func>Function 1</func></root>'

On template engines it is a common practice to extend sub template with main template. Say we have a layout template, that is used as a wapper for many other templates. We can refactor layout xml code to another file and keep the changing sub content on other files.

In HyML MiNiMaL, extend-template macro is used for that.

Lets show an example again. First we have the content of the layout.hyml template file:


In [12]:
(with [f (open "templates/layout.hyml")] (print (f.read)))


(html
	(head (title ~title))
	(body ~body))

And the content of the extend.hyml sub template file:


In [13]:
(with [f (open "templates/extend.hyml")] (print (f.read)))


~(extend-template "layout.hyml" 
    {"body" `(p "Page content")})

We have decided to set the title as a "global" variable but define the body on the sub template. The render process goes like this:


In [14]:
(setv locvar {"title" "Page title"})
(render-template "extend.hyml" locvar)


Out[14]:
'<html><head><title>Page title</title></head><body><p>Page content</p></body></html>'

Note that extension name .hyml was used here even though it doesn't really matter what file extension is used.

At first it may look overly complicated and verbose to handle templates this way. Major advantage is found when processing multiple nested templates. Difference to simply including template files ~@(include "templates/template.hy") is that on include you pass variables (could be evaluated HyML code) to template, but on extend-template you pass unevaluated HyML code to another template. This will add one more dynamic level on using HyML MiNiMaL for XML content generation.

Template directory

Default template directory is set to "template/". You can change directory by changing template-dir variable in the HyML.template module:

(import hyml.template)
(def hyml.template.template-dir "templates-extra/")

Macro -macro

One more related feature to templates is a macro -macro that can be used inside template files to factorize code for local purposes. If our template file would look like this:

~(macro custom [attr]
  `(p :class ~attr))

~(custom ~class)

Then rendering it, would yield:


In [15]:
(render-template "macro.hyml" {"class" "main"}) ; output: <p class="main"/>


Out[15]:
'<p class="main"/>'

Directly calling the parse-mnml function

You are not forced to use ml macro to generate XML. You can pass quoted code directly to parse-mnml function. This can actually be a good idea, for example if you want to generate tags based on a dictionary. First lets see the simple example:


In [16]:
(parse-mnml '(tag)) ; output: <tag/>


Out[16]:
'<tag/>'

Then let us make it a bit more complicated:


In [17]:
; define contacts dictionary
(defvar contacts [
    {:firstname "Eric"
     :lastname "Johnson"
     :telephone "+1-202-555-0170"}
    {:firstname "Mary"
     :lastname "Johnson"
     :telephone "+1-202-555-0185"}])
; pretty print
(print (indent
(ml
  ; root contacts node
  (contacs
    ~@(do
      ; import parse-mnml function at the highest level of unquoted code
      (import (hyml.minimal (parse-mnml)))
      ; contact node
      (list-comp `(contact
        ; last contact detail node
        ~@(list-comp (parse-mnml `(~tag ~val))
            [[tag val] (.items contact)]))
      [contact contacts]))))))


<contacs>
	<contact>
		<firstname>Eric</firstname>
		<lastname>Johnson</lastname>
		<telephone>+1-202-555-0170</telephone>
	</contact>
	<contact>
		<firstname>Mary</firstname>
		<lastname>Johnson</lastname>
		<telephone>+1-202-555-0185</telephone>
	</contact>
</contacs>

With parse-mnml it is also possible to pass an optional dictionary to be used for custom variables and functions on evaluation process. This is NOT possible with ml -macro.


In [18]:
(parse-mnml '(tag :attr ~val) {"val" "val"}) ; output: <tag attr="val"/>


Out[18]:
'<tag attr="val"/>'

With template -macro you can actually see a very similar behaviour. In cases where variables can be hard coded, you might want to use this option:


In [19]:
(template {"val" "val"} `(tag :attr ~val)) ; output: <tag attr="val"/>


Out[19]:
'<tag attr="val"/>'

It doesn't really matter in which order you pass expression and dictionary to the template -macro. It is also ok to leave dictionary out if expression does not contain any variables. For template -macro, expression needs to be quasiquoted, if it contains HyML code.

Wrapping up everything

So all features of the MiNiMaL macro has now been introduced. Let us wrap everything and create XHTML document that occupies the most of the feature set. Additional comments will be given between the code lines.


In [20]:
; define variables
(defvar topic "How do you make XHTML 1.0 Transitional document with HyML?"
        tags ['html 'xhtml 'hyml]
        postedBy "Hege Refsnes"
        contactEmail "hege.refsnes@example.com")

; define function. note that we want to return quasiquoted HyML code here
(deffun valid (fn []
  `(p (a :href "http://validator.w3.org/check?uri=referer"
      (img :src "http://www.w3.org/Icons/valid-xhtml10" 
           :alt "Valid XHTML 1.0 Transitional" 
           :height "31" :width "88")))))

; let just arficially create a body for the post
; and save it to the external template file
(with [f (open "templates/body.hy" "w")]
  (f.write "(div :class \"body\"
    \"I've been wondering if it is possible to create XHTML 1.0 Transitional 
      document by using a brand new HyML?\")"))

; print indented document as a pretty raw html
(print (indent
  ; start up the MiNiMaL macro
  (ml
    ; xml document declaration
    "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
    "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" 
    \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">"
    ; create html tag with xml namespace and language attributes
    (html :xmlns "http://www.w3.org/1999/xhtml" :lang "en"
      (head
        ; title of the page
        (title "Conforming XHTML 1.0 Transitional Template")
        (meta :http-equiv "Content-Type" :content "text/html; charset=utf-8"))
      (body
        ; wrap everything inside the post div
        (div :class "post"
          ; first is the header of the post
          (div :class "header" ~topic)
          ; then body of the post from external template file
          ~@(include "templates/body.hy")
          ; then the tags in spans
          (div :class "tags"
            ~@(list-comp `(span ~tag) [tag tags]))
          ; finally the footer
          (div :id "footer"
            (p "Posted by: " ~postedBy)
            (p "Email: " 
              (a :href ~(+ "mailto:" contactEmail) ~contactEmail) ".")))
         ; proceed valid stamp by a defined function
         ~(valid))))))


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html
  PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN'
  'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
	<head>
		<title>Conforming XHTML 1.0 Transitional Template</title>
		<meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
	</head>
	<body>
		<div class="post">
			<div class="header">How do you make XHTML 1.0 Transitional document with HyML?</div>
			<div class="body">I've been wondering if it is possible to create XHTML 1.0 Transitional 
      document by using a brand new HyML?</div>
			<div class="tags">
				<span>html</span>
				<span>xhtml</span>
				<span>hyml</span>
			</div>
			<div id="footer">
				<p>Posted by: Hege Refsnes</p>
				<p>
					Email: 
					<a href="mailto:hege.refsnes@example.com">hege.refsnes@example.com</a>
					.
				</p>
			</div>
		</div>
		<p>
			<a href="http://validator.w3.org/check?uri=referer">
				<img alt="Valid XHTML 1.0 Transitional" height="31" src="http://www.w3.org/Icons/valid-xhtml10" width="88"/>
			</a>
		</p>
	</body>
</html>

Special features

These are not deliberately implemented features, but a consequence of the HyML MiNiMaL implementation and how Hy works.

Nested MiNiMaL macros

It is possible to call MiNiMaL macro again inside unquoted code:


In [21]:
(ml (tag ~(+ "Generator inside: " (ml (sub "content")))))


Out[21]:
'<tag>Generator inside: <sub>content</sub></tag>'

Unrecognized symbols

Unrecognized symbols (that is they are not specified as literals with double quotas and have no whitespace) are regarded as string literals, unless they are unquoted and they are not colliding with internal Hy symbols.


In [22]:
(ml (tag :alfred J. Kwak))


Out[22]:
'<tag alfred="J.">Kwak</tag>'

Quote and quasiquote

Tag names, attribute values, and tag content can be also single pre-quoted strings. It doesn't matter because in the final process of evaluating the component the string representation of the symbol is retrieved.


In [23]:
[(ml ('tag)) (ml (`tag)) (ml (tag)) (ml ("tag"))]


Out[23]:
['<tag/>', '<tag/>', '<tag/>', '<tag/>']

With keywords, however, single pre-queted strings will get parsed as a content.


In [24]:
[(ml (tag ':attr)) (ml (tag `:attr))]


Out[24]:
['<tag>attr</tag>', '<tag>attr</tag>']

Keyword specialties

Also if keyword marker is followed by a string literal, keyword will be empty, thus not a correctly wormed keyword value pair.


In [25]:
(ml (tag :"attr")) ; output: <tag ="attr"/>


Out[25]:
'<tag ="attr"/>'

So only working version of keyword notation is :{symbol} or unquoted ~(keyword {expression}).

Note: keywords without value are interpreted as a keyword having the same value as the keyword name (called boolean attributes in HTML).


In [26]:
[(ml (tag :disabled)) (ml (tag ~(keyword "disabled"))) (ml (tag :disabled "disabled"))]


Out[26]:
['<tag disabled="disabled"/>', '<tag disabled="disabled"/>', '<tag disabled="disabled"/>']

If you wish to define multiple boolean attributes together with content, you can collect them at the end of the expression. Note that in XML boolean attributes cannot be minimized similar to HTML. Attributes always needs to have a value pair.


In [27]:
(ml (tag "Content" :disabled :enabled))


Out[27]:
'<tag disabled="disabled" enabled="enabled">Content</tag>'

One more thing with keywords is that if the same keyword value pair is given multiple times, it will show up in the mark up in the same order, as multiple. Depending on the markup parser, the last attribute might be valuated OR parser might give an error, because by XML Standard attibute names should be unique and not repeated under the same element.


In [28]:
(ml (tag :attr :attr "attr2"))


Out[28]:
'<tag attr="attr" attr="attr2"/>'

Test main features

Assert tests for all main features presented above. There should be no output after running these. If there is, then Houston, we have a problem!


In [29]:
;;;;;;;;;
; basic ;
;;;;;;;;;
; empty things
(assert (= (ml) ""))
(assert (= (ml"") ""))
(assert (= (ml "") ""))
(assert (= (ml ("")) "</>"))
; tag names
(assert (= (ml (tag)) "<tag/>"))
(assert (= (ml (TAG)) "<TAG/>"))
(assert (= (ml (~(.upper "tag"))) "<TAG/>"))
(assert (= (ml (tag "")) "<tag></tag>"))
; content cases
(assert (= (ml (tag "content")) "<tag>content</tag>"))
(assert (= (ml (tag "CONTENT")) "<tag>CONTENT</tag>"))
(assert (= (ml (tag ~(.upper "content"))) "<tag>CONTENT</tag>"))
; attribute names and values
(assert (= (ml (tag :attr "val")) "<tag attr=\"val\"/>"))
(assert (= (ml (tag ~(keyword "attr") "val")) "<tag attr=\"val\"/>"))
(assert (= (ml (tag :attr "val" "")) "<tag attr=\"val\"></tag>"))
(assert (= (ml (tag :attr "val" "content")) "<tag attr=\"val\">content</tag>"))
(assert (= (ml (tag :ATTR "val")) "<tag ATTR=\"val\"/>"))
(assert (= (ml (tag ~(keyword (.upper "attr")) "val")) "<tag ATTR=\"val\"/>"))
(assert (= (ml (tag :attr "VAL")) "<tag attr=\"VAL\"/>"))
(assert (= (ml (tag :attr ~(.upper "val"))) "<tag attr=\"VAL\"/>"))
; nested tags
(assert (= (ml (tag (sub))) "<tag><sub/></tag>"))
; unquote splice
(assert (= (ml (tag ~@(list-comp `(sub ~(str item)) [item [1 2 3]])))
           "<tag><sub>1</sub><sub>2</sub><sub>3</sub></tag>"))
; variables
(defvar x "variable")
(assert (= (ml (tag ~x)) "<tag>variable</tag>"))
; functions
(deffun f (fn [x] x))
(assert (= (ml (tag ~(f "function"))) "<tag>function</tag>"))
; templates
(with [f (open "test/templates/test.hy" "w")] (f.write "(tag)"))
(assert (= (ml ~@(include "test/templates/test.hy")) "<tag/>"))
; set up custom template directory
(import hyml.template)
(def hyml.template.template-dir "test/templates/")
; render template function
(import [hyml.template [*]])
(with [f (open "test/templates/test2.hy" "w")] (f.write "(tag ~tag)"))
(assert (= (render-template "test2.hy" {"tag" "content"}) "<tag>content</tag>"))
; extend template
(require [hyml.template [*]])
(with [f (open "test/templates/test3.1.hy" "w")] (f.write "(tags :attr ~val ~tag)"))
(with [f (open "test/templates/test3.2.hy" "w")] (f.write "~(extend-template \"test3.1.hy\" {\"tag\" `(tag)})"))
(assert (= (render-template "test3.2.hy" {"val" "val"}) "<tags attr=\"val\"><tag/></tags>"))
; macro -macro
(with [f (open "test/templates/test4.hy" "w")] (f.write "~(macro custom [content] `(tag ~content))~(custom ~content)"))
(assert (= (render-template "test4.hy" {"content" "content"}) "<tag>content</tag>"))
; revert path
(def hyml.template.template-dir "templates/")
; template macro
(assert (= (template {"val" "val"} `(tag :attr ~val)) (template `(tag :attr ~val) {"val" "val"})))
;;;;;;;;;;;
; special ;
;;;;;;;;;;;
; tag names
(assert (= (ml (J)) "<1j/>"))
(assert (= (ml (~"J")) "<J/>"))
(assert (= [(ml ('tag)) (ml (`tag)) (ml (tag)) (ml ("tag"))] (* ["<tag/>"] 4)))
; attribute values
(assert (= [(ml (tag :attr 'val)) (ml (tag :attr `val)) (ml (tag :attr val)) (ml (tag :attr "val"))]
           (* ["<tag attr=\"val\"/>"] 4)))
; content
(assert (= [(ml (tag 'val)) (ml (tag `val)) (ml (tag val)) (ml (tag "val"))]
           (* ["<tag>val</tag>"] 4)))
; keyword processing
(assert (= [(ml (tag ':attr)) (ml (tag `:attr))] ["<tag>attr</tag>" "<tag>attr</tag>"]))
(assert (= (ml (tag :"attr")) "<tag =\"attr\"/>"))
; boolean attributes
(assert (= [(ml (tag :attr "attr")) (ml (tag :attr)) (ml (tag ~(keyword "attr")))]
           ["<tag attr=\"attr\"/>" "<tag attr=\"attr\"/>" "<tag attr=\"attr\"/>"]))
(assert (= (ml (tag :attr1 :attr2)) "<tag attr1=\"attr1\" attr2=\"attr2\"/>"))
(assert (= (ml (tag Content :attr1 :attr2)) "<tag attr1=\"attr1\" attr2=\"attr2\">Content</tag>"))
(assert (= (ml (tag :attr1 :attr2 Content)) "<tag attr1=\"attr1\" attr2=\"Content\"/>"))
; no space between attribute name and value as a string literal
(assert (= (ml (tag :attr"val")) "<tag attr=\"val\"/>"))
; no space between tag, keywords, keyword value, and content string literals
(assert (= (ml (tag"content":attr"val")) "<tag attr=\"val\">content</tag>"))
;;;;;;;;;
; weird ;
;;;;;;;;;
; quote should not be unquoted or surpressed
(assert (= (ml (quote :quote "quote" "quote")) "<quote quote=\"quote\">quote</quote>"))
; tag name, keyword name, value and content can be same
(assert (= (ml (tag :tag "tag" "tag")) "<tag tag=\"tag\">tag</tag>"))
; multiple same attribute names stays in the markup in the reserved order
(assert (= (ml (tag :attr "attr1" :attr "attr2")) "<tag attr=\"attr1\" attr=\"attr2\"/>"))
; parse-mnlm with variable and function dictionary
(assert (= (parse-mnml '(tag :attr ~var ~(func)) 
                       {"var" "val" "func" (fn[]"Content")}) "<tag attr=\"val\">Content</tag>"))

The MIT License

Copyright (c) 2017 Marko Manninen