Bosca / BML BML Reference

The language

Markup with a type system

A BML file reads like HTML because it mostly is HTML — plus routable pages, components with typed Kotlin props, control flow as tags, and styles that scope themselves. Everything a page needs lives in the file that renders it.

Pages

A route is a file

Each page file declares exactly one <page route="…">. Path parameters like {id} are matched by the router and arrive in server code as in-scope values; the bare requireAuth attribute redirects signed-out visitors before anything renders.

  • <if> takes <else-if> and <else> as bare separators — one closing tag ends the whole conditional.
  • <for item in expr> iterates any Iterable; <for (i, item) in expr> adds the index.
  • Server data comes from <script server provides="…"> — the block's last expression binds into scope.
group.bml
<page route="/group/{id}" requireAuth>
  <script server provides="page">
    myapp.group(ctx)
  </script>

  <html lang="en">
    <head><title>Group</title></head>
    <body>
      <main class="wrap">
        <if page.error != null>
          <p class="error">{ page.error }</p>
        <else-if page.found>
          <group-head :name="page.groupName" :members="page.members"/>
        <else>
          <p>This group isn't available.</p>
        </if>
      </main>
    </body>
  </html>
</page>

Expressions

Six marks to learn

The expression syntax is small on purpose. Escaped by default, raw only when you say so.

{ expr }

A server-evaluated Kotlin expression, HTML-escaped by default.

{@ expr }

Raw HTML output — the explicit, visible trusted path.

:href="user.url"

A bound attribute: one server expression. false or null omits the attribute entirely.

class="card-{ size }"

Static attributes interpolate server values inline.

requireAuth

A bare attribute means true by being there — presence is the value.

{# note #}

A build-only comment: never emitted, unlike an HTML comment.

Components

Typed props, scoped styles

<component tag="…"> declares a reusable tag. Props are typed inputs — any Kotlin type, including your own classes and generics — and they're in scope in the body and its server script. Callers pass them with the same binding syntax: <prayer-card :prayer="p"/>.

  • A <style scoped> is isolated to the component: the compiler stamps a marker attribute and rewrites each selector to require it.
  • Scoped CSS is emitted once per page no matter how many times the component renders; a plain <style> stays global.
  • Slotted content is scoped to the caller — it's the caller's markup.
prayer-card.bml
<component tag="prayer-card">
  <prop name="prayer" type="myapp.PrayerRow" required/>

  <article class="prayer-card">
    <span class="author">{ prayer.profileName }</span>
    <if prayer.answered>
      <span class="status-pill">Answered</span>
    </if>
    <h3><a href="{ prayer.href }">{ prayer.title }</a></h3>
  </article>

  <style scoped>
    .prayer-card { border: 1px solid var(--divider); }
    .status-pill { color: var(--forest); }
  </style>
</component>

Slots & overrides

Reuse markup with slots

A component with a <slot/> renders the caller's children inside shared markup. Use it for repeated sections; each page still emits its own <html>, <head>, and <body>. <template> and layout= are reserved syntax and do not wrap a page.

  • Most built-in HTML tags are overrideable: <component tag="a"> replaces every anchor on the site.
  • The html: namespace always resolves to the native element — how an override emits the real tag without recursing.
  • Special tags (page, component, if, for, island, slot) are protected.
content-frame.bml
<component tag="content-frame">
  <main class="content-frame">
    <slot/>
  </main>
</component>

Keep exploring

More on BML