markout
markout copied to clipboard
Markout is an awesome Crystal DSL for HTML
Markout
Markout is an awesome Crystal DSL for HTML. It enables calling regular HTML tags as methods to generate HTML.
Markout ensures type-safe HTML with valid syntax, and automatically escapes attribute values. It supports HTML 4 and 5, and XHTML.
Examples:
p "A paragraph"
# => <p>A paragraph</p>
p do
text "A paragraph"
end
# => <p>A paragraph</p>
h1 "A first-level heading", class: "heading"
# => <h1 class='heading'>A first-level heading</h1>
h1 class: "heading" do
text "A first-level heading"
end
# => <h1 class='heading'>A first-level heading</h1>
ul id: "a-wrapper", class: "list-wrap" do
["aa", "bb", "cc"].each do |x|
li x, class: "list-item"
end
end
# => <ul id='a-wrapper' class='list-wrap'>
# <li class='list-item'>aa</li>
# <li class='list-item'>bb</li>
# <li class='list-item'>cc</li>
# </ul>
input type: "checkbox", checked: nil
# => HTML 4, 5: <input type='checkbox' checked>
# => XHTML: <input type='checkbox' checked='checked' />
Installation
Add this to your application's shard.yml:
dependencies:
markout:
github: GrottoPress/markout
Usage
Pages
With Markout, pages are created using regular Crystal structs and classes. Markout comes with a page mixin, which child pages can include, and override specific methods for their own use case:
require "markout"
# Create your own base page
abstract struct BasePage
# Include the page mixin
include Markout::Page
# Set HTML version
#
# Versions:
# `HtmlVersion::HTML_5` (default)
# `HtmlVersion::XHTML_1_1`
# `HtmlVersion::XHTML_1_0`
# `HtmlVersion::HTML_4_01`
#private def html_version : HtmlVersion
# HtmlVersion::XHTML_1_1
#end
private def body_tag_attr : NamedTuple
{class: "my-body-class"}
end
private def inside_head : Nil
meta charset: "UTF-8"
head_content
end
private def inside_body : Nil
header id: "header" do
h1 "My First Heading Level", class: "heading"
p "An awesome description", class: "description"
end
main id: main do
body_content
end
footer id: "footer" do
raw "<!-- I'm unescaped -->"
end
end
private def head_content : Nil
end
private def body_content : Nil
end
end
# Now, create a page
struct MyFirstPage < BasePage
private def head_content : Nil
title "My First Page"
end
private def body_content : Nil
p "Hello from Markout!"
end
end
# SEND OUTPUT TO CONSOLE
puts MyFirstPage.new
# => <!DOCTYPE html>\
# <html lang='en'>\
# <head profile='http://ab.c'>\
# <meta charset='UTF-8'>\
# <title>My First Page</title>\
# </head>\
# <body class='my-body-class'>\
# <header id='header'>\
# <h1 class='heading'>My First Heading Level</h1>\
# <p class='description'>An awesome description</p>\
# </header>\
# <main id='main'>\
# <p>Hello from Markout!</p>\
# </main>\
# <footer id='footer'>\
# <!-- I'm unescaped -->\
# </footer>\
# </body>\
# </html>
# OR, SERVE IT TO THE BROWSER
require "http/server"
server = HTTP::Server.new do |context|
context.response.content_type = "text/html"
context.response.print MyFirstPage.new
end
puts "Listening on http://#{server.bind_tcp(8080)}"
server.listen
# Visit 'http://localhost:8080' to see Markout in action
Components
You may extract out shared elements that do not exactly fit into the page inheritance structure as components, and mount them in your pages:
require "markout"
# Create your own base component
abstract struct BaseComponent
include Markout::Component
# Set HTML version
#
# Same as for pages.
#private def html_version : HtmlVersion
# HtmlVersion::XHTML_1_1
#end
end
# Create the component
struct MyFirstComponent < BaseComponent
def initialize(users : Array(String))
render(users)
end
private def render(users : Array(String)) : Nil
ul class: "users" do
users.each do |user|
li user, class: "user"
# Same as `li class: "user" do text(user) end`
end
end
end
end
# Mount the component
struct MySecondPage < BasePage
def initialize(@users : Array(String))
end
private def head_content : Nil
title "Component Test"
end
private def body_content : Nil
div class: "users-wrap" do
mount MyFirstComponent, @users # Or `mount MyFirstComponent.new(@users)`
end
end
end
#puts MySecondPage.new(["Kofi", "Ama", "Nana"])
A component may accept named arguments and blocks:
# Create the component
struct MyLinkComponent < BaseComponent
def initialize(label : String, url : String, **opts)
render(label, url, **opts)
end
def initialize(url : String, **opts, &b : Proc(Component, Nil))
render(label, url, **opts, &b)
end
private def render(label : String, url : String, **opts)
args = opts.merge({href: url})
args = {class: "link"}.merge(args)
a label, **args
end
private def render(url : String, **opts, &b : Proc(Component, Nil))
args = opts.merge({href: url})
args = {class: "link"}.merge(args)
a **args do b.call(self) end
end
end
# Mount the component
struct MyThirdPage < BasePage
private def body_content : Nil
div class: "link-wrap" do
mount MyLinkComponent, "Abc", "http://ab.c", "data-foo": "bar"
end
end
end
# OR mount with a block
struct MyThirdPage < BasePage
private def body_content : Nil
div class: "link-wrap" do
mount MyLinkComponent, "http://ab.c", "data-foo": "bar" do |html|
html.text("Abc")
end
end
end
end
puts MyThirdPage.new
# => ...
# <div class='link-wrap'>\
# <a data-foo='bar' href='http://ab.c'>Abc</a>\
# </div>
# ...
Custom Tags
You may define arbitrary tags with #tag. This is particularly useful for rendering JSX or similar:
tag :MyApp, title: "My Awesome App" do
p "My app is the best."
end
# => <MyApp title='My Awesome App'>\
# <p>My app is the best.</p>\
# </MyApp>
tag :MyApp, title: "My Awesome App"
# => <MyApp title='My Awesome App' />
tag :cuboid, width: 4, height: 3, length: 2 do
text "A cuboid"
end
# => <cuboid width='4' height='3' length='2'>
# A cuboid
# </cuboid>
Handy methods
Apart from calling regular HTML tags as methods, the following methods are available:
#text(text : String): Use this to render escaped text#raw(text : String): Use this render unescaped text
Alternatives
Check out the following, if Markout does not fit your needs:
- crystal-lang/html_builder
- Lucky framework comes with an in-built html builder.
Contributing
- Fork it
- Switch to the
masterbranch:git checkout master - Create your feature branch:
git checkout -b my-new-feature - Make your changes, updating changelog and documentation as appropriate.
- Commit your changes:
git commit - Push to the branch:
git push origin my-new-feature - Submit a new Pull Request against the
GrottoPress:masterbranch.