Dragon

Views Are HTML Templates

Hand-concatenating HTML strings gets old fast, and a str carries no guarantee that what is inside it is safe markup. Dragon's template literals give you first-class markup with interpolation and automatic escaping, and the HTML content type makes the type say so:

import ui
from html import HTML
from ui.desktop import Window

title: str = "Dashboard"
items: list[str] = ["Inbox", "Today", "Done"]

view: HTML = template[HTML] {
  <main style="font-family:system-ui;padding:1rem">
    <h1>!{title}</h1>
    <ul>
      !{ for item in items { :{ <li>!{item}</li> } } }
    </ul>
  </main>
}

win: Window = Window(title, 420, 320)
win.body = view
win.show()
ui.App.run()

A template[HTML] { ... } evaluates to an HTML value, and Window.body takes exactly that - so the view you build flows into the window with no str conversion in between. That is the point of the HTML type: it tracks, at compile time, that a value is markup intended for an HTML context.

Escaping is by type, not by discipline

Interpolations are HTML-escaped by default - a title of "<script>" renders as text, not a tag - so the markup you build from user data is safe without you remembering to escape it (the same guarantee the web chapter leans on):

user_input: str = "<script>alert('xss')</script>"
view: HTML = template[HTML] {
  <h1>!{user_input}</h1>
}
# renders: <h1>&lt;script&gt;alert(&#x27;xss&#x27;)&lt;/script&gt;</h1>

Because escaping keys off the type of the interpolated value, composition just works: a str spliced into an HTML template is escaped, but an HTML value is inserted raw - so components nest without double-escaping. This is exactly why a view-building function should return HTML, not str:

def badge(text: str) -> HTML {
    return template[HTML] { <span class="badge">!{text}</span> }
}

view: HTML = template[HTML] {
  <div class="card">
    !{badge("new")}          <!-- HTML value -> inserted raw -->
    <p>!{user_input}</p>     <!-- str -> escaped -->
  </div>
}

When you genuinely have trusted markup to insert verbatim, opt out explicitly with the raw filter: !{trusted | raw}. The unsafe path is the one you have to spell out. Block interpolation (!{ for ... { :{ ... } } }) loops markup in place. The full content-type story - CSS, XML, custom types - is in HTML, CSS, and XML.

Assigning an HTML view to win.body paints it once. To make the screen follow your data as it changes, you need state the UI can watch - that is the next page, Signals and Reactivity.