Back to flin
flin

La etiqueta raw: válvula de escape para HTML

Cómo la etiqueta <raw> de FLIN permite inyectar HTML confiable directamente en el DOM -- potenciando el renderizado de markdown, íconos SVG y visualización de contenido enriquecido manteniendo la seguridad.

Thales & Claude | March 30, 2026 10 min flin
EN/ FR/ ES
flinrust

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.

El problema: salida escapada vs. no escapada

flin// Default: HTML is escaped (safe)
content = "<b>Bold</b> and <i>italic</i>"
<div>{content}</div>
// Renders as: &lt;b&gt;Bold&lt;/b&gt; and &lt;i&gt;italic&lt;/i&gt;
// 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.

La sintaxis

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>

Por qué "raw" y no "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.

Seguridad: la responsabilidad del desarrollador

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 content

The 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:

  1. Store the raw content
  2. Sanitize it (remove dangerous tags)
  3. 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.

Casos de uso

Renderizado de markdown

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.

Resaltado de sintaxis de código

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.

Renderizado de íconos SVG

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.

Plantillas de email

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>

Integración de widgets de terceros

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('&', "&amp;")
     .replace('<', "&lt;")
     .replace('>', "&gt;")
     .replace('"', "&quot;")
     .replace('\'', "&#39;")
}

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.

La decisión de diseño: explícito sobre implícito

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.

Pautas para uso seguro

  1. Never use <raw> with direct user input. Always sanitize first.
  2. Use <raw> freely with generated content (markdown rendering, syntax highlighting, icon paths).
  3. Use <raw> with sanitized user content after calling sanitize_html().
  4. Store both raw and sanitized versions of user content for maximum flexibility.
  5. 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.

Contenido raw reactivo

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

FrameworkSyntaxDefaultSecurity
ReactdangerouslySetInnerHTML={{ __html: html }}EscapedExplicit prop name warns developers
Vuev-html="html"EscapedDocumented warning
Svelte{@html html}EscapedMinimal warning
Angular[innerHTML]="html"SanitizedAngular sanitizes by default
FLIN<raw>{html}</raw>EscapedTag 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.

Cuándo no usar 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.


Esta es la Parte 94 de la serie "Cómo construimos FLIN", que documenta cómo un CEO en Abiyán y un CTO de IA construyeron una válvula de escape HTML en un sistema de plantillas sin comprometer la seguridad por defecto.

Navegación de la serie: - [93] Alternancia de tema y modo oscuro - [94] La etiqueta raw: válvula de escape para HTML (estás aquí) - [95] 151 componentes FlinUI construidos por agentes de IA

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles