Designing and Implementing a DSL with Langium - Example from a Frontend Framework

Mon Mar 28 2022 by Guillaume Fontorbe, Claas Petersen, Emil Krebs

Designing and implementing a DSL may seem like a daunting task. Langium provides a powerful tooling to implement your own DSL and provide out-of-the-box language server support. In this article we guide you through the creation of a framework for creating single static web pages including HTML, CSS, and JavaScript. You will gain insight into the creative process of designing your DSL and how to implement it with Langium.

First we need to define who will be the target audience. Simple UI is aimed at non-experts in front-end development, hence we need to design a DSL that feels intuitive and non-complex for people with little to no experience in creating static web pages.

Designing the Grammar

To make sure that our grammar feels simple to the end-user we want to remove a level of complexity from the HTML syntax. Therefore, we allow only a subset of HTML tags to be used while getting rid of the tag syntax altogether. We also need to ensure that the syntax stays consistent between the different HTML elements, thus ensuring a lower entry level.

We identified two types of HTML elements:

  • Single HTML elements, such as headings, images, links, etc.
  • Nesting HTML elements, which are HTML elements which contain other nesting HTML elements or single HTML elements (e.g. div or section elements in HTML)

We also want the user to be able to define styling for HTML elements via a set of CSS classes defined in a separate file. Those CSS classes should also be able to be tweaked by allowing a subset of CSS properties to be set inline with the HTML element declaration.

The general structure of the syntax follows a consistent pattern and can be seen below for single elements:

and for nesting elements:

where "HTML Content" is other Nesting HTML elements or Simple HTML elements.

Implementing the Grammar

HTML elements are differentiated with the use of keywords. These are the keywords used in the grammar to parse simple HTML elements:

  • paragraph (<p>)
  • heading (<h1> to <h6>)
  • image (<img>)
  • link (<a>)
  • linebreak (<br>)
  • button (<button>)
  • textbox (<input type="text">)

And for nesting HTML elements:

  • div (<div>)
  • section (<section>)

Those keywords are followed by an optional name/id for the element, and keywords/properties that are specific to the given HTML element. For example, the grammar for a heading element is implemented in Langium as:

Heading:
    'heading' ElementName? 'level:'level=INT text=Expression;

and results in the declaration of a heading element by the end-user as:

heading <name> level:<1-6> <text> 

Each HTML element has its own specific set of properties.

CSS classes and CSS styles can be appended to the HTML declaration as:

classes[<className1>, <className2>,...,<classNameN>]
styles[<property1>:<value1>,...,<propertyN>:<valueN>]

The final grammar can be found here

Following is an short example of a file that will result in a HTML document containing a heading, followed by a paragraph and an image, all of those being wrapped in a div

div classes[flex-container, flex-column, center]{
    heading level: 1 "Example page" styles[text-color: "darkslategrey"]
    paragraph "Lorem ipsum"
    image "https://picsum.photos/200" alt:"a random image"
}

Special Elements

We also provide a handful of special elements, which are predefined elements of higher complexity such as a topbar containing navigation links or a footer. We also provide the ability to define reusable components.

Reusable components are defined by the user and can be reused several times and at any place in the file.

For example we can define a component 'card' which takes two arguments to modify its content:

component card (header:string, content:string){
    div classes[flex-container, center]{
        heading level:2 $[header]
        paragraph $[content]
    }
}

This component can then be reused:

usecomponent card("header text", "content text")

This will be equivalent to writing:

div classes[flex-container, center]{
    heading level:2 "header text"
    paragraph "content text"
}

This allows for avoiding repeating blocks of code and increasing modularity.

Language Server Features

Many aspects of the language server are provided by default by Langium. These include renaming, basic completion, hover requests, etc.

We improved the completion by implementing a custom CompletionProvider to provide completion for CSS classes.

The CompletionProvider retrieves all classes from a target CSS file and provides suggestions to the user when typing CSS classes. Any CSS class that is not defined in that file will provide the user with an error message.

To learn more about how to customize services please refer to the documentation.

Generating Files

To get usable files out of our DSL, we need to generate HTML, CSS, and JavaScript files from files written in the SimpleUI language. We use three separate generators, one for generating a HTML file, one for generating a CSS file, and one for generating a JavaScript file. These generators use the AST created by Langium, so we only need to handle the code to generate the output files.

Let's focus on the HTML generator. An HTML file has a basic structure consisting of a head part and a body part. The head contains metadata about the file, while the body contains the actual content. To write our file, we take advantage of Langium's CompositeGeneratorNode, which can generate a string with the expected structure and indentation.

const fileNode = new CompositeGeneratorNode();

To create a new instance of CompositeGeneratorNode.

fileNode.append('Text here!', NL);

To add a new line to the file, followed by a linebreak.

fileNode.indent(indent => {});

To create an indent in our file where we can then append new lines.

HTML elements are generated in the order they are defined in the Simple-UI file. The generator traverses the AST node by node, generates the corresponding HTML and appends it to the CompositeGeneratorNode. For each type of HTML element, we implemented a function to generate the final HTML. To decide which function needs to be run, we look at the type of the node since Langium generates a type for each individual node. With this type, we can now run a switch statement to run the matching function of the element. Once all elements have been processed, an HTML file is created with the string contained in the CompositeGeneratorNode.

For generating the CSS file, we extract all the classes used in our input file from a base file (by default named base.css). This way, only the classes that are actually used are exported to the final stylesheet.

Try It Out!

Head to the GitHub repository and modify or expand the DSL, or start generating static web pages! You can have a look at what a file using SimpleUI looks like here and at the generated static webpage below!