HTML DSL in V

Published .. 31-07-2024
Type ....... article
Tags ....... v, dsl

V is expressive enough to support creating DSL (Domain Specific Language). It's not Lisp, but it will do. Here are the beginning of a DSL to express HTML in V.

Here is how you can write HTML using the DSL:

fn main() {
    // vfmt off
    x := html(atts(), 
            meta(atts("keywords","demo"))
            body(atts(),
                div(atts("class","card"), 
                    a(atts('href', '/demo/', 'target', '_blank'), 'Click me'), 
                    p(atts(), 'More text'))))
    // vfmt on
    println(x)
}

The HTML DSL. This is much nicer than the same written in Go because of the sum-type in V:

module main

import strings

type Element = Node | string

fn (e Element) str() string {
    return match e {
        Node { e.str() }
        string { e.str() }
    }
}

struct Node {
    tag          string
    tag_omission bool
    attributes   Attributes
    children     []Element
}

fn (n Node) str() string {
    mut b := strings.new_builder(1024)
    b.write_string('<${n.tag}')
    if n.attributes.len > 0 {
        b.write_string(' ')
        for i := 0; i < n.attributes.len; i++ {
            a := n.attributes[i]
            b.write_string('${a.name}="${a.value}"')
            if i < n.attributes.len - 1 {
                b.write_string(' ')
            }
        }
    }
    b.write_string('>')
    for c in n.children {
        b.write_string(c.str())
    }
    if !n.tag_omission {
        b.write_string('</${n.tag}>\n')
    }
    return b.str()
}

struct Attribute {
    name  string
    value string
}

type Attributes = []Attribute

// atts take a list of name and values and converts these
// to an array of Attribute elements.
fn atts(alist ...string) []Attribute {
    mut res := []Attribute{}
    for i := 0; i < alist.len; i += 2 {
        a := Attribute{
            name: alist[i]
            value: alist[i + 1]
        }
        res << a
    }
    return res
}

fn construct(tag string, tagomit bool, attribs Attributes, children ...Element) Node {
    n := Node{
        tag: tag
        tag_omission: tagomit
        attributes: attribs
        children: children
    }
    return n
}

fn meta(attribs Attributes, children ...Element) Node {
    return construct('meta', true, attribs, ...children)
}

fn html(attribs Attributes, children ...Element) Node {
    return construct('html', false, attribs, ...children)
}

fn body(attribs Attributes, children ...Element) Node {
    return construct('body', false, attribs, ...children)
}

fn p(attribs Attributes, children ...Element) Node {
    return construct('p', false, attribs, ...children)
}

fn a(attribs Attributes, children ...Element) Node {
    return construct('a', false, attribs, ...children)
}

fn div(attribs Attributes, children ...Element) Node {
    return construct('div', false, attribs, ...children)
}

// etc...