FLIN escapes all HTML by default. When you write {user_input} in a template, FLIN converts <, >, &, ", and ' to their HTML entity equivalents. This prevents Cross-Site Scripting (XSS) attacks by ensuring that user input is never interpreted as HTML.
But sometimes you need to inject real HTML. A markdown renderer produces HTML that must be rendered as HTML, not as escaped text. An SVG icon is an HTML string that must be injected into the DOM. A WYSIWYG editor produces rich content that contains <b>, <i>, <a>, and other tags that must be rendered correctly.
Session 258 added the <raw> tag -- FLIN's controlled escape hatch for injecting trusted HTML into the DOM.
The Problem: Escaped vs. Unescaped Output
flin// Default: HTML is escaped (safe)
content = "<b>Bold</b> and <i>italic</i>"
<div>{content}</div>
// Renders as: <b>Bold</b> and <i>italic</i>
// Displays as: <b>Bold</b> and <i>italic</i> (visible tags)
// With <raw>: HTML is rendered (trusted)
<div><raw>{content}</raw></div>
// Renders as: <b>Bold</b> and <i>italic</i>
// Displays as: **Bold** and *italic* (formatted text)Without the <raw> tag, the content displays as literal text including the HTML tags. With the <raw> tag, the content is injected as real HTML and the browser renders it with formatting.
The Syntax
flin<raw>{expression}</raw>The <raw> tag wraps an expression whose value is a string of HTML. The string is injected directly into the DOM without escaping. The tag itself does not produce any DOM element -- it is a compiler directive that tells the renderer "trust this content."
flin// Markdown rendering
markdown_content = "# Hello\n\nThis is **bold** and *italic*."
html = render_markdown(markdown_content)
<article class="prose">
<raw>{html}</raw>
</article>
// SVG icon injection
svg_path = '<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>'
<svg viewBox="0 0 24 24" width="24" height="24">
<raw>{svg_path}</raw>
</svg>
// Rich text from WYSIWYG editor
<div class="article-body">
<raw>{article.html_content}</raw>
</div>Why "raw" and Not "html"
We considered naming the tag <html>, following Svelte's {@html} directive. We chose <raw> instead for two reasons:
First, clarity of intent. <raw> communicates "this content is injected as-is, without any processing." It suggests that the developer is bypassing the safety system -- which is exactly what is happening. The name carries an appropriate level of caution.
Second, avoiding confusion with the <html> element. In a .flin file that contains a full-page template, <html> might be confused with the HTML document element. <raw> is unambiguous.
Security: The Developer's Responsibility
The <raw> tag is explicitly a security bypass. FLIN's default escaping prevents XSS attacks by ensuring that user input cannot contain executable HTML. The <raw> tag disables this protection for the wrapped expression.
The rule is simple: never use <raw> with user input.
flin// DANGEROUS: user input injected as HTML
user_comment = get_user_input()
<raw>{user_comment}</raw>
// If user_comment contains <script>alert('xss')</script>, it executes!
// SAFE: sanitize before injecting
user_comment = get_user_input()
safe_comment = sanitize_html(user_comment)
<raw>{safe_comment}</raw>
// sanitize_html removes <script> tags and other dangerous contentThe sanitize_html function (covered in article 079) removes dangerous tags and attributes while preserving safe formatting. This is the correct pattern for displaying user-generated rich content:
- Store the raw content
- Sanitize it (remove dangerous tags)
- Inject the sanitized HTML with
<raw>
FLIN's compiler does not warn when <raw> is used (that would be too noisy -- the tag exists to be used). But the documentation and the FLIN Component Guide (from Session 092) prominently warn about the security implications.
Use Cases
Markdown Rendering
The most common use of <raw> is rendering markdown content. FLIN includes a built-in markdown renderer:
flin// Blog post page
post = Article.find_by(slug: params.slug)
html = render_markdown(post.content_md)
<article class="prose">
<h1>{post.title}</h1>
<Text color="muted">{post.published_at.format("MMMM D, YYYY")}</Text>
<Divider />
<raw>{html}</raw>
</article>The render_markdown function converts markdown to HTML. The HTML includes <h1> through <h6>, <p>, <ul>, <ol>, <li>, <code>, <pre>, <blockquote>, <a>, <img>, <strong>, <em>, and <table> tags. All of these must be rendered as HTML, not as escaped text.
Code Syntax Highlighting
Code blocks in technical content need syntax highlighting. The highlighter produces HTML with <span> elements that have CSS classes for color:
flincode = 'fn hello() {\n print("Hello!")\n}'
highlighted = highlight_code(code, "flin")
// '<span class="keyword">fn</span> <span class="function">hello</span>() ...'
<pre class="code-block">
<raw>{highlighted}</raw>
</pre>Without <raw>, the <span> tags would be displayed as literal text. With <raw>, they are rendered as colored code.
SVG Icon Rendering
The Icon component (article 087) uses <raw> internally to inject SVG path data:
flin// Icon.flin (simplified)
path_data = icon_registry[props.name]
<svg viewBox="0 0 24 24" width={props.size} height={props.size}
fill="none" stroke={props.color} stroke-width={props.stroke_width}>
<raw>{path_data}</raw>
</svg>The SVG path data (<path d="..."/>, <circle .../>, <polyline .../>) is HTML that must be injected as-is. The <raw> tag makes this possible. Since the path data comes from the icon registry (a compile-time constant), there is no security risk.
Email Templates
Email HTML is notoriously different from web HTML. Email clients have limited CSS support, require inline styles, and use table-based layouts. FLIN applications that send emails often generate the HTML from templates:
flinfn render_welcome_email(user) {
html = '
<table width="600" cellpadding="0" cellspacing="0">
<tr>
<td style="padding: 20px; background: #007bff; color: white;">
<h1 style="margin: 0;">Welcome, {user.name}!</h1>
</td>
</tr>
<tr>
<td style="padding: 20px;">
<p>Your account has been created.</p>
<a href="https://app.example.com/verify?token={user.verify_token}"
style="background: #007bff; color: white; padding: 10px 20px; text-decoration: none;">
Verify Email
</a>
</td>
</tr>
</table>
'
return html
}
// Preview in the browser
email_html = render_welcome_email(current_user)
<div class="email-preview">
<raw>{email_html}</raw>
</div>Third-Party Widget Integration
Some third-party services provide HTML embed codes (analytics widgets, chat widgets, maps):
flin// Embed a map
map_embed = '<iframe src="https://maps.google.com/..." width="100%" height="400" frameborder="0"></iframe>'
<Card>
<CardHeader>Our Location</CardHeader>
<CardBody>
<raw>{map_embed}</raw>
</CardBody>
</Card>Implementation: How raw Bypasses Escaping
FLIN's template renderer has two output paths for expression values:
rustfn render_expression(expr: &Expr, vm: &mut Vm, raw_mode: bool) -> String {
let value = vm.eval(expr)?;
let text = value.to_string();
if raw_mode {
// Raw mode: inject as-is
text
} else {
// Normal mode: escape HTML entities
html_escape(&text)
}
}When the compiler encounters <raw>{expr}</raw>, it sets a flag on the expression node indicating raw mode. The renderer checks this flag and skips the html_escape call.
The html_escape function is simple but critical:
rustfn html_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}Five replacements that prevent XSS. The <raw> tag skips all five. That is the entire mechanism -- no magic, no special DOM APIs, just the presence or absence of HTML entity escaping.
The Design Decision: Explicit Over Implicit
Some frameworks make raw HTML the default and require explicit escaping. PHP, for example, outputs variables without escaping unless you use htmlspecialchars(). This is the wrong default -- it means every missed escaping call is a potential XSS vulnerability.
Other frameworks make escaping the default and provide a way to bypass it. React uses dangerouslySetInnerHTML. Svelte uses {@html}. Vue uses v-html. FLIN uses <raw>.
The naming convention varies in how much it communicates danger:
- React: dangerouslySetInnerHTML -- very explicit about the risk
- Svelte: {@html} -- neutral
- Vue: v-html -- neutral
- FLIN: <raw> -- suggests "unprocessed, use with care"
We chose <raw> because it is honest without being alarmist. The tag exists for legitimate use cases (markdown, icons, rich text). Naming it dangerouslyInjectHtml would discourage legitimate use. Naming it html would fail to communicate the security implications. raw is the middle ground.
Guidelines for Safe Usage
- Never use
<raw>with direct user input. Always sanitize first. - Use
<raw>freely with generated content (markdown rendering, syntax highlighting, icon paths). - Use
<raw>with sanitized user content after callingsanitize_html(). - Store both raw and sanitized versions of user content for maximum flexibility.
- Review
<raw>usage in code reviews -- every<raw>tag should have an obvious source of trusted HTML.
flin// Pattern: sanitize at write time, render with <raw> at read time
entity Article {
content_md: text // User's markdown input
content_html: text // Sanitized HTML (computed at save time)
}
fn save_article(markdown: text) {
html = render_markdown(markdown)
safe_html = sanitize_html(html)
Article.create(content_md: markdown, content_html: safe_html)
}
// Display: safe to use <raw> because content_html was sanitized at save time
<raw>{article.content_html}</raw>The <raw> tag is a power tool. Used correctly, it enables markdown blogs, icon libraries, rich text editors, and email previews. Used carelessly, it enables XSS attacks. The tag's existence is a trade-off between safety and capability. FLIN chooses to provide the capability with clear documentation about the risks.
Reactive Raw Content
The <raw> tag works with FLIN's reactivity system. When the expression inside <raw> changes, the rendered HTML updates:
flinmarkdown_source = "# Hello\n\nWorld"
html_output = render_markdown(markdown_source)
// Editor
<Textarea value={markdown_source} rows={10} />
// Live preview
<div class="preview">
<raw>{render_markdown(markdown_source)}</raw>
</div>As the user types in the textarea, markdown_source changes. The render_markdown call re-evaluates, producing new HTML. The <raw> tag replaces the old HTML with the new HTML. The result is a live markdown editor with preview -- a common feature in blogging platforms and documentation tools.
The reactive update replaces the entire content of the <raw> block. There is no diffing of the raw HTML -- FLIN treats the HTML string as an opaque blob. For small to medium content (a blog post, a README file), this is fast enough to feel instantaneous. For very large HTML documents (thousands of lines), the full replacement might cause a brief flicker. In practice, this is rarely a problem because <raw> is typically used for article-sized content, not page-sized documents.
Comparison: Raw HTML Across Frameworks
| Framework | Syntax | Default | Security |
|---|---|---|---|
| React | dangerouslySetInnerHTML={{ __html: html }} | Escaped | Explicit prop name warns developers |
| Vue | v-html="html" | Escaped | Documented warning |
| Svelte | {@html html} | Escaped | Minimal warning |
| Angular | [innerHTML]="html" | Sanitized | Angular sanitizes by default |
| FLIN | <raw>{html}</raw> | Escaped | Tag name suggests caution |
Angular takes a unique approach: [innerHTML] sanitizes the HTML by default, removing <script> tags and event handlers. FLIN does not sanitize inside <raw> -- the developer is expected to sanitize before passing content to <raw>. This is a deliberate choice: automatic sanitization can silently remove content that the developer intended to include (like style attributes or custom data attributes), leading to confusing bugs.
The FLIN philosophy: be explicit. If you want sanitized HTML, call sanitize_html() explicitly. If you want raw, unmodified HTML, use <raw> explicitly. No hidden behavior. No surprises.
When Not to Use Raw
Not every HTML injection requires <raw>. Some common patterns have better alternatives:
Dynamic attributes -- use interpolation instead:
``flin
// Instead of <raw>
<div><raw>{"<p class=\"" + class_name + "\">text</p>"}</raw></div>
BLANK
// Use interpolation
<p class={class_name}>text</p>
``
Conditional content -- use {if} blocks instead:
``flin
// Instead of <raw>
content = show_details ? "<div>Details...</div>" : ""
<raw>{content}</raw>
BLANK
// Use conditionals
{if show_details}
<div>Details...</div>
{/if}
``
Lists of elements -- use {for} loops instead:
``flin
// Instead of <raw>
html = items.map(i => "<li>{i.name}</li>").join("")
<ul><raw>{html}</raw></ul>
BLANK
// Use loops
<ul>
{for item in items}
<li>{item.name}</li>
{/for}
</ul>
``
<raw> should be a last resort, used only when the HTML content is genuinely dynamic and cannot be expressed through FLIN's template syntax. The three legitimate use cases -- markdown rendering, SVG icon injection, and sanitized rich text -- cover the vast majority of <raw> usage in practice.
This is Part 94 of the "How We Built FLIN" series, documenting how a CEO in Abidjan and an AI CTO built an HTML escape hatch into a template system without compromising default security.
Series Navigation: - [93] Theme Toggle and Dark Mode - [94] The Raw Tag: Escape Hatch for HTML (you are here) - [95] 151 FlinUI Components Built by AI Agents