SkillAgentSearch skills...

Falco.Markup

An F# DSL for efficient markup generation, including HTML, SVG and XML.

Install / Use

/learn @falcoframework/Falco.Markup
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Falco.Markup

NuGet Version build

open Falco.Markup

let doc =
    _html [] [
        _body [ _class_ "100-vh" ] [
            _h1' "Hello world!" ] ]

renderHtml doc

Falco.Markup is an XML markup module that can be used to produce any form of angle-bracket markup (i.e. HTML, SVG, XML etc.).

Key Features

  • Use native F# to produce any form of angle-bracket markup.
  • Simple to create reusable blocks of code (i.e., partial views and components).
  • Easily extended by creating custom tags and attributes.
  • Compiled as part of your assembly, leading to improved performance and simpler deployments.
  • Provides strongly typed functions matching the full HTML spec.

Design Goals

  • Provide a tool to generate any form of angle-bracket markup.
  • Must be performant and memory efficient.
  • Should be simple, extensible and integrate with existing .NET libraries.
  • Can be easily learned.
  • Match HTML spec as closely as possible.
  • Support rendering full documents as well as fragments.

Overview

Falco.Markup is broken down into three primary modules, Elem, Attr and Text, which are used to generate elements, attributes and text nodes respectively. Each module contain a suite of functions mapping to the various element/attribute/node names. But can also be extended to create custom elements and attributes.

Primary elements are broken down into two types, ParentNode or SelfClosingNode.

ParentNode elements are those that can contain other elements. Represented as functions that receive two inputs: attributes and optionally elements.

Each of the primary modules can be access using the name directly, or using the "underscore syntax" seen below.

| Module | Syntax | |--------|--------| | Elem | _h1 [] [] | | Attr | _class_ "my-class" | | Text | _text "Hello world!" | | Text shortcuts | _h1' "Hello world" (note the trailing apostrophe) |

let markup =
    _div [ _class_ "heading" ] [
        _h1' "Hello world!" ]

SelfClosingNode elements are self-closing tags. Represented as functions that receive one input: attributes.

let markup =
    _div [ _class_ "divider" ] [
        _hr [] ]

Text is represented using the TextNode and created using one of the functions in the Text module.

let markup =
    _div [] [
        _p' "A paragraph"
        _p [] [ _textf "Hello %s" "Jim" ]
        _code [] [ _textEnc "<div>Hello</div>" ] // HTML encodes text before rendering
    ]

Attributes contain two subtypes as well, KeyValueAttr which represent key/value attributes or NonValueAttr which represent boolean attributes.

let markup =
    _input [ _type_ "text"; _required_ ]

Most JavaScript Events have also been mapped in the Attr module. All of these events are prefixed with the word "on" (i.e., _onclick_, _onfocus_ etc.)

let markup =
    _button [ _onclick_ "console.log(\"hello world\")" ] [ _text "Click me" ]

HTML

Though Falco.Markup can be used to produce any markup. It is first and foremost an HTML library.

Combining views to create complex output

open Falco.Markup

// Components
let divider =
    _hr [ _class_ "divider" ]

// Template
let master (title : string) (content : XmlNode list) =
    _html [ _lang_ "en" ] [
        _head [] [
            _title [] [ _text title ]
        ]
        _body [] content
    ]

// Views
let homeView =
    master "Homepage" [
        _h1' "Homepage"
        divider
        _p' "Lorem ipsum dolor sit amet, consectetur adipiscing."
    ]

let aboutView =
    master "About Us" [
        _h1' "About"
        divider
        _p' "Lorem ipsum dolor sit amet, consectetur adipiscing."
    ]

Strongly-typed views

open Falco.Markup

type Person =
    { FirstName : string
      LastName : string }

let doc (person : Person) =
    _html [ _lang_ "en" ] [
        _head [] [
            _title [] [ _text "Sample App" ]
        ]
        _body [] [
            _main [] [
                _h1' "Sample App"
                _p' $"{person.First} {person.Last}"
            ]
        ]
    ]

Forms

Forms are the lifeblood of HTML applications. A basic form using the markup module would like the following:

let dt = DateTime.Now

_form [ _methodPost_; _action_ "/submit" ] [
    _label [ _for_' "name" ] [ _text "Name" ]
    _input [ _id_ "name"; _name_ "name"; _typeText_ ]

    _label [ _for_' "birthdate" ] [ _text "Birthday" ]
    _input [ _id_ "birthdate"; _name_ "birthdate"; _typeDate_; _valueDate_ dt ]

    _input [ _typeSubmit_ ]
]

Expanding on this, we can create a more complex form involving multiple inputs and input types as follows:

_form [ _methodPost_; _action_ "/submit" ] [
    _label [ _for_' "name" ] [ _text "Name" ]
    _input [ _id_ "name"; _name_ "name" ]

    _label [ _for_' "bio" ] [ _text "Bio" ]
    _textarea [ _name_ "id"; _name_ "bio" ] []

    _label [ _for_' "hobbies" ] [ _text "Hobbies" ]
    _select [ _id_ "hobbies"; _name_ "hobbies"; _multiple_ ] [
        _option [ _value_ "programming" ] [ _text "Programming" ]
        _option [ _value_ "diy" ] [ _text "DIY" ]
        _option [ _value_ "basketball" ] [ _text "Basketball" ]
    ]

    _fieldset [] [
        _legend [] [ _text "Do you like chocolate?" ]
        _label [] [
            _text "Yes"
            _input [ _typeRadio_; _name_ "chocolate"; _value_ "yes" ] ]
        _label [] [
            _text "No"
            _input [ _typeRadio_; _name_ "chocolate"; _value_ "no" ] ]
    ]

    _fieldset [] [
        _legend [] [ _text "Subscribe to our newsletter" ]
        _label [] [
            _text "Receive updates about product"
            _input [ _typeCheckbox_; _name_ "newsletter"; _value_ "product" ] ]
        _label [] [
            _text "Receive updates about company"
            _input [ _typeCheckbox_; _name_ "newsletter"; _value_ "company" ] ]
    ]

    _input [ _typeSubmit_ ]
]

A simple but useful meta-element _control can reduce the verbosity required to create form outputs. The same form would look like:

_form [ _methodPost_; _action_ "/submit" ] [
    _control "name" [] [ _text "Name" ]

    _controlTextarea "bio" [] [ _text "Bio" ] []

    _controlSelect "hobbies" [ _multiple_ ] [ _text "Hobbies" ] [
        _option [ _value_ "programming" ] [ _text "Programming" ]
        _option [ _value_ "diy" ] [ _text "DIY" ]
        _option [ _value_ "basketball" ] [ _text "Basketball" ]
    ]

    _fieldset [] [
        _legend [] [ _text "Do you like chocolate?" ]
        _control "chocolate" [ _id_ "chocolate_yes"; _typeRadio_ ] [ _text "yes" ]
        _control "chocolate" [ _id_ "chocolate_no"; _typeRadio_ ] [ _text "no" ]
    ]

    _fieldset [] [
        _legend [] [ _text "Subscribe to our newsletter" ]
        _control "newsletter" [ _id_ "newsletter_product"; _typeCheckbox_ ] [ _text "Receive updates about product" ]
        _control "newsletter" [ _id_ "newsletter_company"; _typeCheckbox_ ] [ _text "Receive updates about company" ]
    ]

    _input [ _typeSubmit_ ]
]

Attribute Value

One of the more common places of sytanctic complexity is with _value_ which expects, like all Attr functions, string input. Some helpers exist to simplify this.

let dt = DateTime.Now

_input [ _typeDate_; _valueStringf_ "yyyy-MM-dd" dt ]

// you could also just use:
_input [ _typeDate_; _valueDate_ dt ] // formatted to ISO-8601 yyyy-MM-dd

// or,
_input [ _typeMonth_; _valueMonth_ dt ] // formatted to ISO-8601 yyyy-MM

// or,
_input [ _typeWeek_; _valueWeek_ dt ] // formatted to Gregorian yyyy-W#

// it works for TimeSpan too:
let ts = TimeSpan(12,12,0)
_input [ _typeTime_; _valueTime_ ts ] // formatted to hh:mm

// there is a helper for Option too:
let someTs = Some ts
_input [ _typeTime_; _valueOption_ _valueTime_ someTs ]

Merging Attributes

The markup module allows you to easily create components, an excellent way to reduce code repetition in your UI. To support runtime customization, it is advisable to ensure components (or reusable markup blocks) retain a similar function "shape" to standard elements. That being, XmlAttribute list -> XmlNode list -> XmlNode.

This means that you will inevitably end up needing to combine your predefined XmlAttribute list with a list provided at runtime. To facilitate this, the Attr.merge function will group attributes by key, and intelligently concatenate the values in the case of additive attributes (i.e., class, style and accept).

open Falco.Markup

// Components
let heading (attrs : XmlAttribute list) (content : XmlNode list) =
    // safely combine the default XmlAttribute list with those provided
    // at runtime
    let attrs' =
        Attr.merge [ _class_ "text-large" ] attrs

    _div [] [
        _h1 [ attrs' ] content
    ]

// Template
let master (title : string) (content : XmlNode list) =
    _html [ _lang_ "en" ] [
        _head [] [
            _title [] [ _text title ]
        ]
        _body [] content
    ]

// Views
let homepage =
    master "Homepage" [
        heading [ _class_ "red" ] [ _text "Welcome to the homepage" ]
        _p' "Lorem ipsum dolor sit amet, consectetur adipiscing."
    ]

let homepage =
    master "About Us" [
        heading [ _class_ "purple" ] [ _text "This is what we're all about" ]
        _p' "Lorem ipsum dolor 
View on GitHub
GitHub Stars42
CategoryDevelopment
Updated2mo ago
Forks3

Languages

F#

Security Score

95/100

Audited on Feb 3, 2026

No findings