8.4. Implementing a library for safe html construction¶
We want to build a library which we can use to programmatically build a html website in Haskell and then render it.
Note: this exercise is intended to be solved using both a Haskell source file and ghci.
My recommendation is to implement the code in a file (Something.hs
) then open ghci
and load the file with the :load FileName.hs
(this has autocompletion for the file name as well) command.
After that the types and functions you defined in the file will be in scope and you can play around with them.
Note: In ghci bindings must be created with let binding = expr
.
8.4.1. A base type¶
First we need a basic Html
type.
For now this is just going to be a wrapper around a String
containing the actiual html.
Define the Html
type as a wrapper around String
. [1]
Don’t expose your constructor to the user of the library [2] so that they cannot unsafely create Html
values from String
.
Also create a function render
or renderHtml
which takes a Html
value and returns it in rendered String
.
In this case that’s simply the String
contained in the Html
value.
You’ll then be able to use this function in the subsequent tasks to look at the Html
values and verify you have implemented your manipulation functions correctly.
8.4.2. Creating html from strings¶
Now we need the user to be able to create Html
values from strings, but we want that to be safe.
First we will enable them to create just html text nodes.
Html text nodes may not contain any of the special html characters like &
, <
, >
.
Write a function mkTextNode
which takes a String
as input and verifies that none of the above mentioned characters are in it. [3]
If one of the characters is found raise an error
and if not return a Html
value containing the string.
8.4.3. Concatenating html¶
Html elements can also be consecutive.
Like <div>...</div><span>...</span>
.
Write a function which takes as input two Html
values and returns a Html
value which is the concatenation of the two input Html
values. [4]
8.4.4. Html containers¶
Now we want to be able to use things like html div
and span
.
Write at least two functions which implement one of the html containers like i
, div
or span
.
I recommend calling the mkDiv
and mkSpan
etc.
For now we will not add any attributes to these containers.
They should accept a Html
value as input and return a Html
value.
And what they should do is add the respective opening and closing tags around the html value they have received as input. [5]
8.4.5. Html documents¶
Now we want to model a whole html document. First we will need to model the doctype.
Create a
Doctype
type with constructors for some of the most common html versions:Html
(for html4)Html5
(for html5) andXHtml
.- For the document itself we will create a
Document
type. This type should have three fields.
doctype :: Doctype
headSection :: Html
bodySection :: Html
Implement this type using record syntax. This allows us to manipulate the fields later.
- For the document itself we will create a
Lastly we need a way to render it.
Create a
renderDocument
function which returns a string that is the concatenation of:- The correct doctype string for the
Doctype
- The head html wrapped in
<head></head>
- The body html wrapped in
<body></body>
- The correct doctype string for the
8.4.6. Making the html editable¶
Until now we have only used String
for the internal html.
However we can do better.
We want to be able to edit our html safely after we have created it.
Also we want support for attributes.
8.4.7. Doing some inspection¶
Now that we have this fancier Html tree we can do interesting things.
Implement the following queries as functions (they all return Bool
).
- Is a supplied
Html
value a text node - Does the node have a specific tag (hint: the type signature should be
:: String -> Html -> Bool
) - How many attributes does the node have? (assuming no attribute occurs twice in the attribute list) [11]
- Does the node have a specific attribute (hint: the type signature should be
:: String -> Html -> Bool
) [12]
8.4.8. Implement a monoid¶
Implement the Data.Monoid.Monoid
typeclass for Html
.
mappend
stands for append. The m
is only added so it does not clash names.
Think about which of the functions we have already implemented that appends.
mempty
stands for empty. Think about what an empty element for Html
would be.
Hint: The empty element should be such that appending an empty element to anything it should no change the original thing.
8.4.9. A typeclass for rendering¶
Define a typeclass for our render function.
I propose to call the typeclass Renderable r
, with one member function render :: r -> String
.
Implement the Renderable
typeclass for Html
, Doctype
and Document
. [14]
8.4.10. Escaping (advanced)¶
Change the text node creation so it doesn’t fail when illegal characters are found but instead replaces them with the xml escape sequences. The important thing to keep in mind here is that you need to replace single characters by strings of characters. [13]
Character | Escape |
---|---|
& |
& |
< |
< |
> |
> |
footnotes
[1] | You can use a data declaration, however since we only have one field in it you should use a newtype . |
[2] | Use the export list in your module to only export the type, not the constructor. |
[3] | Remember that the Haskell String type is just a list of characters.
Look at the Data.List module in the base library documentation and find the function that allows you to test whether a certain character is in the string.
(Hint: its the same function that tests whether a certain value is an element of the list.) |
[4] | You’ll have to unwrap the input Html values to get acces to the strings within.
Look for an operator in Data.List which appends two lists together.
You can use this operator to combine the strings as well.
Finally wrap it all back up into a new Html value. |
[5] | You’ll again have to unwrap the Html , prepend the start tag and append the end tag to it.
Finally wrap it all back up into a new Html value |
[6] | Pairs are the same a tuples. Both attribute and its value should be of type String . |
[7] | Children are again Html values. |
[8] | You can implement the different types of html by making it an algebraic datatype (data ) with one constructor for the text node and one for the container node.
Use record syntax for the latter. |
[9] | Some things that may come in handy here is the Haskell supports calling functions recursively.
Meaning you can for instance call |
[10] | This can be nicely done using a partially applied Container constructor. |
[11] | This is the same as the length of the attribute list. |
[12] | To see if an element of a list satisfies a predicate there are two ways.
Either using map and any or using find .
I leave you to find out how to use these ;) |
[13] | I’d recommend either to use concatMap or foldr . |
[14] | You can use the render function for other types or nested data inside the definition of render .
For instance when rendering Document you can use render on the Doctype or a Html value to render it. |