{"id":21939181,"url":"https://github.com/grottopress/markout","last_synced_at":"2025-04-22T14:49:55.730Z","repository":{"id":85788162,"uuid":"151182165","full_name":"GrottoPress/markout","owner":"GrottoPress","description":"Markout is an awesome Crystal DSL for HTML","archived":false,"fork":false,"pushed_at":"2024-08-06T13:47:26.000Z","size":132,"stargazers_count":22,"open_issues_count":0,"forks_count":1,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-03-29T16:11:11.560Z","etag":null,"topics":["crystal","dsl","html"],"latest_commit_sha":null,"homepage":"","language":"Crystal","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/GrottoPress.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-10-02T01:01:26.000Z","updated_at":"2024-08-06T13:47:30.000Z","dependencies_parsed_at":"2024-08-03T18:52:19.375Z","dependency_job_id":null,"html_url":"https://github.com/GrottoPress/markout","commit_stats":null,"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GrottoPress%2Fmarkout","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GrottoPress%2Fmarkout/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GrottoPress%2Fmarkout/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GrottoPress%2Fmarkout/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/GrottoPress","download_url":"https://codeload.github.com/GrottoPress/markout/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250263204,"owners_count":21401815,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["crystal","dsl","html"],"created_at":"2024-11-29T02:17:18.427Z","updated_at":"2025-04-22T14:49:55.692Z","avatar_url":"https://github.com/GrottoPress.png","language":"Crystal","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Markout\n\n**Markout** is an awesome Crystal DSL for HTML. It enables calling regular HTML tags as methods to generate HTML.\n\n*Markout* ensures type-safe HTML with valid syntax, and automatically escapes attribute values. It supports *HTML 4* and *5*, and *XHTML*.\n\n### Examples:\n\n```crystal\np \"A paragraph\"\n# =\u003e \u003cp\u003eA paragraph\u003c/p\u003e\n\np do\n  text \"A paragraph\"\nend\n# =\u003e \u003cp\u003eA paragraph\u003c/p\u003e\n\nh1 \"A first-level heading\", class: \"heading\"\n# =\u003e \u003ch1 class='heading'\u003eA first-level heading\u003c/h1\u003e\n\nh1 class: \"heading\" do\n  text \"A first-level heading\"\nend\n# =\u003e \u003ch1 class='heading'\u003eA first-level heading\u003c/h1\u003e\n\nul id: \"a-wrapper\", class: \"list-wrap\" do\n  [\"aa\", \"bb\", \"cc\"].each do |x|\n    li x, class: \"list-item\"\n  end\nend\n# =\u003e \u003cul id='a-wrapper' class='list-wrap'\u003e\n#      \u003cli class='list-item'\u003eaa\u003c/li\u003e\n#      \u003cli class='list-item'\u003ebb\u003c/li\u003e\n#      \u003cli class='list-item'\u003ecc\u003c/li\u003e\n#    \u003c/ul\u003e\n\ninput type: \"checkbox\", checked: nil\n# =\u003e HTML 4, 5: \u003cinput type='checkbox' checked\u003e\n# =\u003e XHTML: \u003cinput type='checkbox' checked='checked' /\u003e\n```\n\n## Installation\n\nAdd this to your application's `shard.yml`:\n\n```yaml\ndependencies:\n  markout:\n    github: GrottoPress/markout\n```\n\n## Usage\n\n### Pages\n\nWith *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:\n\n```crystal\nrequire \"markout\"\n\n# Create your own base page\nabstract struct BasePage\n  # Include the page mixin\n  include Markout::Page\n\n  # Set HTML version\n  #\n  # Versions:\n  #   `HtmlVersion::HTML_5` (default)\n  #   `HtmlVersion::XHTML_1_1`\n  #   `HtmlVersion::XHTML_1_0`\n  #   `HtmlVersion::HTML_4_01`\n  #private def html_version : HtmlVersion\n  #  HtmlVersion::XHTML_1_1\n  #end\n\n  private def body_tag_attr : NamedTuple\n    {class: \"my-body-class\"}\n  end\n\n  private def inside_head : Nil\n    meta charset: \"UTF-8\"\n    head_content\n  end\n\n  private def inside_body : Nil\n    header id: \"header\" do\n      h1 \"My First Heading Level\", class: \"heading\"\n      p \"An awesome description\", class: \"description\"\n    end\n\n    main id: main do\n      body_content\n    end\n\n    footer id: \"footer\" do\n      raw \"\u003c!-- I'm unescaped --\u003e\"\n    end\n  end\n\n  private def head_content : Nil\n  end\n\n  private def body_content : Nil\n  end\nend\n\n# Now, create a page\nstruct MyFirstPage \u003c BasePage\n  private def head_content : Nil\n    title \"My First Page\"\n  end\n\n  private def body_content : Nil\n    p \"Hello from Markout!\"\n  end\nend\n\n# SEND OUTPUT TO CONSOLE\n\nputs MyFirstPage.new\n# =\u003e \u003c!DOCTYPE html\u003e\\\n#    \u003chtml lang='en'\u003e\\\n#      \u003chead profile='http://ab.c'\u003e\\\n#        \u003cmeta charset='UTF-8'\u003e\\\n#        \u003ctitle\u003eMy First Page\u003c/title\u003e\\\n#      \u003c/head\u003e\\\n#      \u003cbody class='my-body-class'\u003e\\\n#        \u003cheader id='header'\u003e\\\n#          \u003ch1 class='heading'\u003eMy First Heading Level\u003c/h1\u003e\\\n#          \u003cp class='description'\u003eAn awesome description\u003c/p\u003e\\\n#        \u003c/header\u003e\\\n#        \u003cmain id='main'\u003e\\\n#          \u003cp\u003eHello from Markout!\u003c/p\u003e\\\n#        \u003c/main\u003e\\\n#        \u003cfooter id='footer'\u003e\\\n#          \u003c!-- I'm unescaped --\u003e\\\n#        \u003c/footer\u003e\\\n#      \u003c/body\u003e\\\n#    \u003c/html\u003e\n\n# OR, SERVE IT TO THE BROWSER\n\nrequire \"http/server\"\n\nserver = HTTP::Server.new do |context|\n  context.response.content_type = \"text/html\"\n  context.response.print MyFirstPage.new\nend\n\nputs \"Listening on http://#{server.bind_tcp(8080)}\"\n\nserver.listen\n# Visit 'http://localhost:8080' to see Markout in action\n```\n\n### Components\n\nYou may extract out shared elements that do not exactly fit into the page inheritance structure as components, and mount them in your pages:\n\n```crystal\nrequire \"markout\"\n\n# Create your own base component\nabstract struct BaseComponent\n  include Markout::Component\n\n  # Set HTML version\n  #\n  # Same as for pages.\n  #private def html_version : HtmlVersion\n  #  HtmlVersion::XHTML_1_1\n  #end\nend\n\n# Create the component\nstruct MyFirstComponent \u003c BaseComponent\n  def initialize(@users : Array(String))\n  end\n\n  private def render : Nil\n    ul class: \"users\" do\n      @users.each do |user|\n        li user, class: \"user\"\n        # Same as `li class: \"user\" do text(user) end`\n      end\n    end\n  end\nend\n\n# Mount the component\nstruct MySecondPage \u003c BasePage\n  def initialize(@users : Array(String))\n  end\n\n  private def head_content : Nil\n    title \"Component Test\"\n  end\n\n  private def body_content : Nil\n    div class: \"users-wrap\" do\n      mount MyFirstComponent, @users # Or `mount MyFirstComponent.new(@users)`\n    end\n  end\nend\n\n#puts MySecondPage.new([\"Kofi\", \"Ama\", \"Nana\"])\n```\n\nA component may accept a block:\n\n```crystal\n# Create the component\nstruct MyLinkComponent \u003c BaseComponent\n  def initialize(@url : String, \u0026@block : Proc(Component, Nil))\n  end\n\n  private def render : Nil\n    a href: @url, class: \"link\", \"data-foo\": \"bar\" do\n      @block.call(self)\n    end\n  end\nend\n\n# Mount the component\nstruct MyThirdPage \u003c BasePage\n  private def body_content : Nil\n    div class: \"link-wrap\" do\n      mount MyLinkComponent, \"http://ab.c\" do |html|\n        html.text(\"Abc\")\n      end\n    end\n  end\nend\n\nputs MyThirdPage.new\n# =\u003e ...\n#    \u003cdiv class='link-wrap'\u003e\\\n#      \u003ca href='http://ab.c' class='link' data-foo='bar'\u003eAbc\u003c/a\u003e\\\n#    \u003c/div\u003e\n#    ...\n```\n\nTo accept arbitrary arguments, you would have to do something different:\n\n```crystal\n# Create the component\nstruct MyLinkComponent \u003c BaseComponent\n  def initialize(@label : String, @url : String, **opts)\n    render_args(**opts)\n  end\n\n  private def render_args(**opts)\n    args = opts.merge({href: @url})\n    args = {class: \"link\"}.merge(args)\n\n    a @label, **args\n  end\nend\n\n# Mount the component\nstruct MyThirdPage \u003c BasePage\n  private def body_content : Nil\n    div class: \"link-wrap\" do\n      mount MyLinkComponent, \"Abc\", \"http://ab.c\", \"data-foo\": \"bar\"\n    end\n  end\nend\n\nputs MyThirdPage.new\n# =\u003e ...\n#    \u003cdiv class='link-wrap'\u003e\\\n#      \u003ca data-foo='bar' href='http://ab.c'\u003eAbc\u003c/a\u003e\\\n#    \u003c/div\u003e\n#    ...\n```\n\n### Custom Tags\n\nYou may define arbitrary tags with `#tag`. This is particularly useful for rendering JSX or similar:\n\n```crystal\ntag :MyApp, title: \"My Awesome App\" do\n  p \"My app is the best.\"\nend\n# =\u003e \u003cMyApp title='My Awesome App'\u003e\\\n#      \u003cp\u003eMy app is the best.\u003c/p\u003e\\\n#    \u003c/MyApp\u003e\n\ntag :MyApp, title: \"My Awesome App\"\n# =\u003e \u003cMyApp title='My Awesome App' /\u003e\n\ntag :cuboid, width: 4, height: 3, length: 2 do\n  text \"A cuboid\"\nend\n# =\u003e \u003ccuboid width='4' height='3' length='2'\u003e\n#      A cuboid\n#    \u003c/cuboid\u003e\n```\n\n### Handy methods\n\nApart from calling regular HTML tags as methods, the following methods are available:\n\n- `#raw(text : String)`: Use this render unescaped text\n- `#text(text : String)`: Use this to render escaped text\n\n## Contributing\n\n1. [Fork it](https://github.com/GrottoPress/markout/fork)\n1. Switch to the `master` branch: `git checkout master`\n1. Create your feature branch: `git checkout -b my-new-feature`\n1. Make your changes, updating changelog and documentation as appropriate.\n1. Commit your changes: `git commit`\n1. Push to the branch: `git push origin my-new-feature`\n1. Submit a new *Pull Request* against the `GrottoPress:master` branch.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgrottopress%2Fmarkout","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgrottopress%2Fmarkout","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgrottopress%2Fmarkout/lists"}