Developer
News and Updates
Get Support
Sign in
Get Support
Sign in
DOCUMENTATION
Cloud
Data Center
Resources
Sign in
Sign in
DOCUMENTATION
Cloud
Data Center
Resources
Sign in
Last updated Apr 22, 2026

Soy Templates

A practical, use-case focused guide to Atlassian's Soy templates (based on Google's Closure Templates).

Key Concepts

  • Templates are reusable: Define once, call from many places
  • Auto-escaping is on by default: User input is safe from XSS automatically
  • Variables are immutable: Once set with let, they can't change
  • Parameters are required by default: Use ? to make them optional

Calling templates from JavaScript

Compiled templates are exposed as functions on the namespace object. Call one with a record of its parameters:

1
2
Example.Namespace.templateName({ paramA: 'value', paramB: 42 });
// → "rendered output"

The full function signature is templateFn(optional_data, opt_stringBuilder, opt_injectedData). You can use undefined for the string builder. You can pass undefined or an object for injected data (see Injected Data).

Throughout the rest of this document, JavaScript call examples are only included when they show something extra — e.g. an unusual signature ($ij), an interesting input/output pair (XSS escaping, contextual escape, kind="html" pass-through), or a runtime gotcha. For ordinary "feed in a record, get a string back" cases, use the convention above.

Atlassian Plugin System

Calling templates from Java

Import com.atlassian.soy.renderer package. Import the SoyTemplateRenderer service.

1
2
String html = soyTemplateRenderer.render('com.example.plugin.key:soy-module-key', 'Example.Namespace.templateName', Map.of());

Registering a Soy template web-resource

By default every web-resource of Soy templates is isolated, to use a template from another web-resource you need to add it as a web-resource dependency (note: This will also make their dependencies' templates available too, these could clash with yours).

1
2
<web-resource key="soy-module-key">
  <!-- Available to the server-side only -->
  <resource type="soy" name="links" location="templates/example.soy"/>

  <!-- Make available to the client-side in JS as well... -->
  <transformation extension="soy">
      <transformer key="soyTransformer"/>
  </transformation>
  <resource type="download" name="example.soy.js" location="templates/example.soy"/>
  <!-- Required for templates used on the client-side -->
  <dependency>com.atlassian.soy.soy-template-plugin:soy-deps</dependency>

  <!-- Makes AUI Soy templates usable https://aui.atlassian.com/aui/latest/docs/soy.html -->
  <dependency>com.atlassian.auiplugin:soy</dependency>
</web-resource>

Atlassian provided soy functions

concat

concat(listOrDictionaryA, listOrDictionaryB) will do a shallow, reference-based, join of "B" onto "A" overwriting any conflicting values on "A". Both values must be a list or a dictionary, not a mix. When used on the server-side an exception will be thrown if this is not the case. The client-side doesn't, instead it will silently produce the text [object Object].

contextPath

contextPath() will return just the relative context path (e.g. /jira) which would be the same as applicationProperties.getBaseUrl(UrlMode.RELATIVE) on the server and WRM.contextPath() on the clientside.

getText

Limitation: Only works with a maximum of 20 arguments

I18n transformer required?

Works the same as WRM.I18n / SAL's getText

getTextAsHtml

WARNING Only use for trusted HTML (not UGC)

getTextAsHtml('example.i18n.key', 'exampleSubstitutedText') works the same as getText, but will return HTML kind and bless it so it is not escaped by Soy. Arguments cannot contain HTML themselves, they are escaped to avoid attacks from UGC.

isList

isList($exampleSoyData) returns a boolean

isMap

isMap($exampleSoyData) returns a boolean

toString

toString($exampleSoyData) returns a string of basically String#valueOf

helpUrl

helpUrl is the equivalent of using HelpPathResolver#getHelpPath

Loading web-resources (Java-only)

On the client-side, instead use WRM.require/require('wrm/require')(

webResourceManager_includeResources, webResourceManager_requireResource, webResourceManager_requireResourcesForContext are the equivalents of using the WebResourceManager Java API

How-to

Defining a template

Every Soy template file starts with a namespace declaration and contains one or more templates. Each template is a reusable unit of HTML/text generation.

Namespace and template naming conventions

  • Template files must have a namespace (e.g., Demo.MyFeature, Confluence.Templates.Feature)
    • By convention, they are dot-separated, with each segment in UpperCamelCase
    • It's best to use something that won't clash with other templates
  • Template names must begin with a . character (e.g., .greet, .renderCard)
    • By convention, they are lowerCamelCase
  • All templates must have a Soy doc comment (similar to JavaDoc)
  • Templates can be marked private to indicate they should only be called by other templates in the same file
    • Note: On the server-side (Java), private templates are enforced at runtime. In JavaScript, they're annotated with @private but the browser doesn't prevent calls.

Example: Basic template structure

1
2
{namespace Demo.Example}

/**
 * Displays a greeting message
 */
{template .greet}
  <div>Hello, World!</div>
{/template}

/**
 * Internal helper template - only for use within this file
 */
{template .internalHelper private="true"}
  <!-- implementation -->
{/template}

Declaring template parameters

Template parameters define the data that a template needs to render. Soy supports required parameters, optional parameters, and typed parameters.

Required parameters

By default, all parameters are required. The template will fail to render if a required parameter is not provided.

1
2
{namespace demo.params}

/**
 * @param name string The person's name
 */
{template .greet}
  Hello {$name}!
{/template}

Optional parameters

Mark parameters with ? to make them optional. Inside the template, optional parameters have type <TYPE>|undefined.

1
2
/**
 * @param? greeting string Optional greeting message
 */
{template .optionalGreet}
  {if $greeting}
    {$greeting}
  {else}
    Hi
  {/if}!
{/template}

Multiple parameters

Templates can have multiple parameters of different kinds.

1
2
/**
 * @param first string First value
 * @param second string Second value
 */
{template .combine}
  First: {$first}, Second: {$second}
{/template}

Typed parameters

You can specify parameter "types" in SoyDoc for clarity:

1
2
/**
 * @param count number Number of items
 * @param name string String identifier
 */
{template .typedExample}
  {$name}: {$count}
{/template}

However, in this repo's compiler, SoyDoc "types" are not enforced for type checking (they are not validated, and callers are not type-checked against them).

If you need compiler-enforced types, use block-style parameter declarations inside the template body:

1
2
{namespace demo.paramBlockTyped}

/**
 * Minimal SoyDoc (required by this compiler)
 */
{template .t}
  {@param name: string}
  Hello {$name}!
{/template}

Block-style {@param ...} declarations are enforced by the compiler at {call} sites (when you compile the caller and callee together).

Calling other templates

Templates can call other templates using the {call} command. The output of the called template is inserted at the call site.

Basic template call

When calling another template, you pass its parameters explicitly using {param} blocks:

1
2
{namespace demo.calls}

/**
 * Simple greeting template
 * @param name string
 */
{template .greet}
  Hello {$name}!
{/template}

/**
 * Card that calls the greeting template
 * @param personName string
 */
{template .card}
  <div class="card">
    {call .greet}
      {param name: $personName /}
    {/call}
  </div>
{/template}

Calling with data spreading

You can pass all parameters from the current template to the called template using data="all":

1
2
/**
 * Inner template that displays data
 * @param first string
 * @param second string
 */
{template .display}
  First: {$first}, Second: {$second}
{/template}

/**
 * Outer template that spreads its parameters
 * @param first string
 * @param second string
 */
{template .wrapper}
  <div>
    {call .display data="all" /}
  </div>
{/template}

Block-form {param name}...{/param}

{param} has two forms inside a {call}:

  • Self-closing: {param name: $expr /} — pass a single expression value. Use this for primitives or anything that already lives in a variable.
  • Block form: {param name}...{/param} — render a fragment of template content and pass that as the parameter value. Use this when the value is itself a small chunk of markup or computed text and you don't want to extract it into a separate {let} first.
1
2
{namespace demo.paramBlock autoescape="contextual"}

/**
 * @param title
 * @param body
 */
{template .card}
  <div class="card">
    <h2>{$title}</h2>
    <div class="body">{$body |noAutoescape}</div>
  </div>
{/template}

/**
 * @param title
 * @param who
 */
{template .blockForm}
  {call .card}
    {param title: $title /}
    {param body}
      Hello, <strong>{$who}</strong>!
      Welcome aboard.
    {/param}
  {/call}
{/template}

A block-form {param} may also declare its own kind, just like a {let} or {template}. The kind controls how the block contents are escaped before being handed to the callee — for example, kind="html" says "this fragment is already HTML; don't re-escape it on the way in". kind on {param} requires the enclosing namespace (or template) to use autoescape="contextual" or "strict".

1
2
{template .blockKind}
  {call .card}
    {param title kind="text"}Greeting{/param}
    {param body kind="html"}
      <em>Hi, {$who}!</em>
    {/param}
  {/call}
{/template}

Namespace aliases ({alias})

For repeated calls into another namespace, an {alias} declaration lets you shorten the call path inside one file. After {alias demo.types}, every {call ...} in that file can refer to demo.types.simpleRenderer as just types.simpleRenderer. The alias is the last dotted segment of the aliased namespace.

1
2
{namespace demo.aliased}
{alias demo.calls}
{alias demo.types}

/** @param name */
{template .greet}
  {call calls.simple}
    {param name: $name /}
  {/call}
{/template}

/** @param text */
{template .greetAndRender}
  {call calls.simple}{param name: $text /}{/call}
  {call types.simpleRenderer}{param text: $text /}{/call}
{/template}

Constraints worth knowing about in this fork:

  • {alias} tags must appear immediately after {namespace}, each on its own line, and before any {template}. The compiler rejects them in any other position with Tag 'alias' not at start of line.
  • Aliases never affect compilation of the called template itself; they're a per-file shorthand for the caller. {call} and {delcall} both honour them.
  • The aliased namespace must actually be loaded at runtime (e.g. by importing its .soy file from the test or page that uses your template).

Debugging

If you encounter compilation errors, look for a SoySyntaxException or TofuException in your build logs. These typically indicate:

  • Missing parameter declarations in the SoyDoc comment
  • References to undefined variables
  • Type mismatches
  • Syntax errors in Soy tags

Example error message:

1
2
[INFO] com.google.template.soy.base.SoySyntaxException: In file example.soy:72, 
template MyTemplate.example: Found references to data keys that are not declared 
in SoyDoc: [exampleParameter]

Solution: Check that all variables used in the template are declared in the @param comments.

{debugger}

{debugger} compiles to a literal JavaScript debugger; statement in the generated function body. It produces no rendered output.

1
2
/** @param name */
{template .greet}
  {debugger}
  Hello, {$name}!
{/template}

In a browser with devtools open, execution pauses at this point so you can inspect the local variables of the template function. In Node and in production (where no debugger is attached), it is a no-op. Treat it as a development-only aid — leaving {debugger} in shipped templates is harmless to users but easy to forget about.

When the template is rendered on the server, attach a debug point at: com.google.template.soy.sharedpasses.render.RenderVisitor#visitDebuggerNode

{css className}

This is only useful if you use Google Closure Compiler and the templates only on the client-side. Otherwise, it's best to manually prefix CSS classes to avoid unintentional collisions.

{css ...} emits a CSS class name. It compiles to goog.getCssName('className'), which is the hook that Closure Stylesheets uses to rewrite class names (for minification, namespacing, etc.). With no rename map wired up — the common case here — it returns the name unchanged.

The point of using {css} instead of writing the class name as plain text is that every class reference in your templates becomes a single thing the rename pipeline can find and rewrite. If you later turn renaming on, you don't have to chase down every string literal.

1
2
{namespace demo.cssCmd}

{template .single}
  <div class="{css single}"></div>
{/template}

{template .multiple}
  <div class="{css btn} {css btn-primary}"></div>
{/template}

/** @param extra */
{template .mixed}
  <div class="{css btn} {$extra}"></div>
{/template}

Each class needs its own {css ...} call — there is no list form. {css} works inside class attributes alongside dynamic content; the result is just a string the auto-escaper treats like any other.

{xid identifier}

This is only useful if you use Google Closure Compiler and the templates only on the client-side. Otherwise, it's best to manually prefix identifiers to avoid unintentional collisions.

{xid ...} is the sibling of {css} for non-CSS identifiers (DOM ids, generated event names, anything you want to be reachable to a renaming/obfuscation pass). It compiles to a runtime xid('identifier') call. With no rename map registered — the default in this fork — the helper simply returns the identifier verbatim, so {xid header_logo} prints header_logo.

1
2
{namespace demo.xidCmd}

{template .single}
  <div id="{xid header}"></div>
{/template}

{template .dotted}
  <span data-key="{xid app.user.id}"></span>
{/template}

{template .multiple}
  <div id="{xid main}" data-target="{xid sidebar}"></div>
{/template}

The argument is a bare identifier (with optional dots), not a string or expression. There is no list form — call {xid} once per identifier you need rewritten. Note that the test harness in this repository stubs xid() to the identity function; in a real Atlassian product, registering a rename map will cause {xid} (and {css}) sites to emit the renamed values.

{log}{/log}

{log}{/log} evaluates its body as if it were an ordinary template fragment, then writes the resulting string to window.console.log — i.e. the info / "log" level, not warn, error, or debug. There is no level= attribute in this fork; everything goes through console.log. The body produces no rendered output. It is intended for transient debugging from inside a template.

1
2
{namespace demo.logCmd}

/** @param name */
{template .basic}
  before
  {log}rendering for: {$name}{/log}
  after
{/template}

/** @param items list<string> */
{template .withLoop}
  {foreach $item in $items}
    {log}item index {index($item)} = {$item}{/log}
    <li>{$item}</li>
  {/foreach}
{/template}

{log} is unconditional — it always logs, in every environment — so it is a debugging aid and not an alternative to a real logger. Remove {log} blocks before shipping; they will spam the console of every user that renders the template otherwise. There is no {log level=...} variant in this fork.

Comments

Soy supports several comment styles. No type of comment is ever rendered into the template output.

Single-line comments (//)

Use // to comment out the rest of a line in a .soy file, including inside template bodies.

1
2
{namespace demo.comments}

/**
 * @param name string
 */
{template .singleLineComment}
  // This is a single-line comment; it is not rendered
  Hello {$name}!
{/template}

Multi-line comments (/* ... */)

Use /* ... */ for multi-line comments.

1
2
/**
 * @param name string
 */
{template .multiLineComment}
  /* This is a multi-line comment; it is not rendered */
  Hello {$name}!
{/template}

Inline block comments and whitespace gotcha

Soy's line-joining algorithm collapses whitespace between tokens on different lines — newlines plus their leading indentation are stripped. Spaces written explicitly on the same line are preserved verbatim. The two rules together produce one specific footgun: an inline /* ... */ block comment is removed, but the spaces you wrote either side of it are not.

SourceOutput
Hello/* hidden */ {$name}!Hello Ada! (one space)
Hello /* hidden */{$name}!Hello Ada! (one space)
Hello /* hidden */ {$name}!Hello  Ada! (two spaces)
1
2
/**
 * @param name string
 */
{template .inlineBlockComment}
  Hello/* this inline block comment is not rendered */ {$name}!
{/template}

Whitespace handling does not depend on whether the surrounding context is plain text or HTML — both go through the same line-joining pass. A few examples illustrate the rule:

SourceOutput
Hello {$a}\n Goodbye {$b}Hello XGoodbye Y (newline+indent stripped, no space inserted)
<p>{$a}</p>\n <p>{$b}</p><p>X</p><p>Y</p> (no whitespace between sibling tags)
<span class="\n a\n b\n "><span class=" a b "> (leading-line spaces collapsed to one each, trailing/leading kept)

If you actually want a space between two pieces on different lines, write {sp} explicitly, or put both pieces on the same line with a space between them.

SoyDoc comments (/** ... */)

SoyDoc comments are the /** ... */ blocks you place immediately above a {template ...} declaration.

In this repo's compiler, SoyDoc is required (at least a minimal /** ... */ block above each template). Two rules apply to @param lines:

  • Unused parameters cause a compile error, not a warning. Declaring @param name and never referencing $name in the body fails parsing with:
    1
    2
    SoySyntaxException: ... Found params declared in SoyDoc but not used in template: [name]
    
    Either use the param or delete the @param line. There is no @suppress-style escape hatch.
  • The "type" written after the parameter name is treated as a free-text description, not a type. It is not enforced at {call} sites and the compiler emits no runtime type assertion for SoyDoc-declared params. (Block-form {@param x: type} is type-asserted at runtime — see "Block-form parameter declarations".)
1
2
/**
 * Says hello to a name
 * @param name The name to greet
 */
{template .greet}
  {@param name: string}
  Hello {$name}!
{/template}

Unsupported here: {/* ... */} style comments

Some newer Soy docs show "Soy comments" written as {/* ... */}. That syntax does not compile in this repo's compiler—use // or /* ... */ instead.

Special Characters

Soy provides special character commands for inserting whitespace and other characters that would otherwise be stripped or hard to represent. These are especially useful when dealing with Soy's line-joining algorithm.

Space ({sp})

Use {sp} to insert an explicit space character. This is essential when Soy's line-joining algorithm would otherwise remove the space (e.g., between text and an HTML tag).

1
2
{namespace demo.specialchars}

/**
 * @param first string
 * @param second string
 */
{template .explicitSpace}
  {$first}{sp}{$second}
{/template}

When to use {sp}

Use {sp} when the join location borders a Soy command or HTML tag:

1
2
/**
 * Without {sp}, "Hello" and "<strong>" would join without a space
 */
{template .spaceBeforeTag}
  Hello{sp}<strong>World</strong>
{/template}

Empty string ({nil})

Use {nil} to prevent unwanted spaces. While {nil} itself produces an empty string, it creates a Soy command at the line-joining location, which prevents the default space insertion.

1
2
/** */
{template .nilPreventSpace}
  <span class="{nil}
    my-class{nil}
  ">text</span>
{/template}

Without {nil}, the output would have extra spaces: <span class=" my-class ">text</span>

Non-breaking space ({nbsp})

{nbsp} inserts a non-breaking space (Unicode \u00A0).

1
2
/** */
{template .nonBreakingSpace}
  100{nbsp}USD
{/template}

Note: Don't use   in Soy templates — auto-escaping turns it into &nbsp;. Use {nbsp} (in this Atlassian environment) or the literal Unicode character (anywhere) instead.

Newline ({\n})

Use {\n} to insert a literal newline character in the output:

1
2
/**
 * Inserts a newline between lines
 */
{template .newline}
  Line 1{\n}Line 2
{/template}

Tab ({\t})

Use {\t} to insert a tab character:

1
2
/** */
{template .tabCharacter}
  Name{\t}Value
{/template}

Literal braces ({lb} and {rb})

Use {lb} (left brace) and {rb} (right brace) to output literal { and } characters without Soy interpreting them as commands:

1
2
/**
 * Outputs literal braces for JSON-like syntax
 */
{template .literalBraces}
  JSON: {lb}"key": "value"{rb}
{/template}

Special Characters Reference

CommandOutputUse Case
{sp}spaceAdd space at line joins near HTML/Soy tags
{nil}empty stringPrevent unwanted space at line joins
{nbsp}non-breaking spaceKeep words together, prevent wrapping (Atlassian-only — loader-injected, not a real Soy command; see warning above)
{\n}newlineInsert line break in output
{\t}tabInsert tab character
{\r}carriage returnInsert carriage return (rarely needed)
{lb}{Output literal left brace
{rb}}Output literal right brace

Literal Blocks

Use {literal}...{/literal} to include raw text that should not be parsed as Soy. Everything inside a literal block is output exactly as written—no HTML escaping, no line joining, no template command parsing, and no comment processing.

Use sparingly. Because {literal} disables HTML escaping, anything inside it that interpolates user data is an XSS vector waiting to happen — and {literal} does not allow Soy expressions inside it, so you cannot interpolate "safely" anyway. The only legitimate cases are static, pre-vetted code where Soy's brace syntax would otherwise collide with the host language's brace syntax:

When to use {literal}

In practice this means only inside <script> and <style> tags, and only for content you control end-to-end (no template parameters interpolated):

  • Static JavaScript or JSON inside a <script>...</script> block where braces would otherwise be parsed as Soy commands.
  • Static CSS inside a <style>...</style> block, for the same reason.

If you find yourself reaching for {literal} to dump a chunk of HTML, stop and write a regular template instead — you'll get auto-escaping, line-joining, and reusable parameters for free. Likewise, prefer keeping JavaScript and CSS in their own .js / .css files referenced from a <script src> / <link> — inline <script>/<style> blocks bypass the build pipeline (no minification, no source maps, no CSP nonces unless you wire them up by hand) and are usually a sign that something belongs elsewhere.

Other historical use cases — "documenting Soy syntax in templates", "including text that looks like Soy commands" — are real but rare. When you do hit one, prefer escaping the offending characters with {lb}/{rb} ({ and }) so auto-escaping still applies.

Basic usage

1
2
{namespace demo.literal}

/**
 * Outputs JSON with braces as raw text
 */
{template .jsonExample}
  {literal}{"key": "value", "count": 42}{/literal}
{/template}

Preserving Soy-like syntax

Use {literal} to output text that looks like Soy variables or commands:

1
2
/**
 * Shows Soy syntax as documentation
 */
{template .soyLikeSyntax}
  {literal}Use {$variable} to print values{/literal}
{/template}

JavaScript code snippets

Perfect for including code examples:

1
2
/**
 * Outputs JavaScript code with braces preserved
 */
{template .javascriptCode}
  {literal}
function greet(name) {
  return "Hello, " + name + "!";
}
  {/literal}
{/template}

Mixing literal and dynamic content

You can combine {literal} blocks with regular Soy expressions:

1
2
/**
 * @param title
 */
{template .mixedContent}
  <h1>{$title}</h1>
  <pre>{literal}<div class="example">{$notAVariable}</div>{/literal}</pre>
{/template}

The {$title} is rendered normally, but {$notAVariable} inside the literal block is output as raw text.

Important notes

  • No escaping: Content inside {literal} is NOT HTML-escaped. Never include un-vetted user input — there is no way to make it safe inside the block, because Soy expressions don't run there.
  • No processing: Whitespace, newlines, and indentation are preserved exactly as written.
  • No nesting: You cannot nest {literal} blocks or use Soy commands inside them — including no way to interpolate a parameter. If you need dynamic values, the block has to be split: render the dynamic parts outside {literal} and the static braces inside.

Print Statements

Soy has two equivalent ways to render a value into the output: the implicit short form {$x} and the explicit {print $x} command. The implicit form is what almost every example in this document uses. The two forms compile to identical code; everything that's true of one is true of the other.

Implicit vs. explicit print

The short {$x} form is preferred. Use {print ...} when you want the visual emphasis. e.g. calling out a particularly long expression that would otherwise blend into surrounding HTML, or e.g. when you need to grep for "all the print sites" ({ followed by $ is too noisy a pattern).

1
2
/**
 * @param x
 */
{template .implicit}
  {$x}
{/template}

/**
 * @param x
 */
{template .explicit}
  {print $x}
{/template}
  • Print directives still attach the same way: {print $x |escapeHtml} works exactly like {$x|escapeHtml}.

  • Inside HTML attributes: both forms work in any print position, including attribute values.

    1
    2
    /**
     * @param greeting
     * @param name
     */
    {template .insideAttribute}
      <span title="{print $greeting}, {$name}">hi</span>
    {/template}
    

Print Directives

Print directives are post-processing operations applied to the output of a {$variable} expression. They are specified using the pipe (|) character followed by the directive name.

|noAutoescape

Disables automatic HTML escaping for a value. Use with extreme caution—only for content you trust completely.

1
2
{namespace demo.printdirectives}

/**
 * WARNING: Only use with trusted content!
 * @param html Trusted HTML content
 */
{template .noAutoescapeExample}
  <div>{$html|noAutoescape}</div>
{/template}

The directive disables HTML-escaping at this print site, so a <script> payload in $html is rendered as-is. Warning: only use |noAutoescape when the content comes from a trusted source (like another Soy template's output). Never use it with user-provided input.

|escapeUri

Encodes a value for safe inclusion in a URL. Use this for query parameters and URL segments.

1
2
/**
 * @param query A query string value
 */
{template .escapeUriExample}
  <a href="/search?q={$query|escapeUri}">Search</a>
{/template}

A query of hello world becomes hello%20world; a&b=c becomes a%26b%3Dc.

|truncate

Truncates a string to a maximum length, optionally adding an ellipsis.

It's better to instead use AUI expanders for both usability and accessibility. It provides Soy templates for this component.

Basic truncation (with ellipsis)

1
2
/**
 * @param text Text to truncate
 */
{template .truncateExample}
  {$text|truncate:15}
{/template}

'This is a very long text' becomes 'This is a ve...'; strings shorter than the limit pass through unchanged.

Truncation without ellipsis

Pass false as the second argument to omit the ellipsis:

1
2
/**
 * @param text Text to truncate
 */
{template .truncateNoEllipsisExample}
  {$text|truncate:10,false}
{/template}

Note: Consider using CSS text-overflow: ellipsis for visual truncation, as |truncate is not Unicode-aware and may corrupt multi-byte characters like emojis.

|changeNewlineToBr

Converts newline characters (\n, \r, \r\n) to HTML <br> tags.

1
2
/**
 * @param text Text with newlines
 */
{template .changeNewlineToBrExample}
  <p>{$text|changeNewlineToBr}</p>
{/template}

|id

Marks a value as an identifier (HTML id attribute, CSS class name, etc.) and cancels autoescape at that print site. Functionally similar to |noAutoescape, but the intent is documentation: writing |id says "this string is an identifier, not arbitrary text".

The source (IdDirective.java) describes it as:

"A directive that marks an identifier such as an HTML id or CSS class name. This directive turns off autoescape for the 'print' tag (if it's on for the template)."

1
2
/**
 * @param value A pre-validated identifier
 */
{template .idExample}
  <div id="{$value|id}"></div>
{/template}

Warning: Same security concerns as |noAutoescape apply — the directive does not validate that the value is actually a safe identifier. Prefer leaving autoescape on and tolerating the (harmless) escape pass unless you have a measured reason to skip it.

Practical difference vs. |noAutoescape: None at runtime in this fork — both compile to identical JS. The difference is intent signalling for human readers and grep-ability ("show me every place we print an identifier"). If you have a renaming pipeline that rewrites identifiers, |id is the hook to find them.

|cleanHtml

Strips disallowed HTML tags from a string and returns the result as SanitizedContent of kind HTML. The allowlist is fixed and small: b, br, em, i, s, strong, sub, sup, u. Everything else (including <script>, <style>, <a>, <img> and any attribute on the kept tags) is removed. Text content from stripped tags is preserved.

1
2
{namespace demo.newDirectives autoescape="false"}

/**
 * |cleanHtml strips all HTML tags except a small allowlist.
 * Used with autoescape="false" so the input is not pre-escaped before
 * the directive sees it (cleanHtml is itself a sanitiser).
 * @param html
 */
{template .cleanHtmlBasic}
  {$html|cleanHtml}
{/template}

Use it when accepting HTML fragments from a partially trusted source — comments, rich-text fields — and you want to allow basic emphasis but nothing structural or interactive. Since it returns sanitised content, downstream auto-escaping is a no-op (the value is already vetted). For arbitrary user input you do not want to render any tags at all, prefer plain auto-escaping.

|filterImageDataUri

Validates a value as a safe data:image/... URI for use in <img src> (or similar). If the value is not a recognised image data URI, the directive returns the literal string about:invalid#zSoyz instead — a deliberately useless URL that browsers treat as broken.

1
2
/** @param uri */
{template .img}
  <img src="{$uri|filterImageDataUri}">
{/template}

This is the right directive for data:image/png;base64,... style inline images. It is not a general URL filter — passing https://example.com/foo.png will be rejected. For arbitrary URIs, use |escapeUri (or trust the value via |noAutoescape after validating it yourself).

|formatNum

Formats a number using locale-aware separators. With no arguments, it picks a sensible default; passing one argument selects a format style:

1
2
/** @param n number */
{template .plain}
  {$n|formatNum}                        // 1234567 → "1,234,567"
{/template}

/** @param n number */
{template .decimal}
  {$n|formatNum:'decimal'}              // 1234.5 → "1,234.5"
{/template}

/** @param n number */
{template .percent}
  {$n|formatNum:'percent'}              // 0.42 → "42%"
{/template}

Recognised style strings are 'decimal', 'percent', 'currency', 'scientific', and 'compact' (subject to runtime availability). The exact output depends on the active locale in the runtime; in the bare JS-only harness used by this repo, expect the en-US convention. Numbers passed through |formatNum should not be combined with manual round() for display — let the directive do the rounding so locale-specific decimal handling stays consistent.

|bidiUnicodeWrap

Wraps a string with Unicode bidi control characters (LRE/PDF or RLE/PDF) so it renders correctly when interpolated into text of the opposite direction. Use it on plain text (the variant for HTML output is |bidiSpanWrap):

1
2
/** @param text */
{template .userName}
  Hello, {$text|bidiUnicodeWrap}
{/template}

In an LTR-only application this is a no-op pass-through; reach for it only when you genuinely render mixed-direction text. See "Bidi Functions" below for the runtime helpers it pairs with.

Internal-only directives

A few directives the compiler defines are reserved for internal use by the contextual auto-escaper and are rejected by the CheckEscapingSanityVisitor if you try to write them yourself. The most commonly seen one is |text, which the auto-escaper inserts when it needs to render a typed value as plain text. There is no user-facing reason to write |text (or any of the |escapeHtmlAttribute*, |normalizeHtml*, |filterCss* family) in templates — leave them to the compiler.

Combining with auto-escaping

Print directives are applied after auto-escaping by default. This means |truncate receives already-escaped text:

1
2
/**
 * @param text
 */
{template .truncateEscaped}
  {$text|truncate:10}
{/template}

If $text is "<script>", it becomes "<script>" after escaping (13 chars), then truncated to "<script...".

Expressions and Operators

String concatenation

Strings can be concatenated using the + operator:

1
2
{namespace demo.expressions}

/**
 * Concatenates first and second with a space
 * @param first string
 * @param second string
 */
{template .greetingName}
  {let $fullGreeting: 'Hello, ' + $first + ' ' + $second /}
  {$fullGreeting}!
{/template}

Arithmetic operators

Basic math operations are supported:

1
2
/**
 * Performs arithmetic operations
 * @param a number
 * @param b number
 */
{template .calculate}
  Sum: {$a + $b}, Difference: {$a - $b}, Product: {$a * $b}
{/template}

In addition to +, -, *, /, the modulo operator % returns the remainder of integer division. It is most often used for "every Nth" patterns and even/odd checks:

1
2
{namespace demo.newOperators}

/**
 * Modulo: 17 % 5 = 2.
 * @param a number
 * @param b number
 */
{template .modulo}
  {$a % $b}
{/template}

/** @param n number */
{template .evenOdd}
  {if $n % 2 == 0}even{else}odd{/if}
{/template}

/** @param items list<string> */
{template .stripe}
  {foreach $item in $items}
    <li class="{if index($item) % 2 == 0}even{else}odd{/if}">{$item}</li>
  {/foreach}
{/template}

% follows JavaScript semantics: the sign of the result follows the sign of the left operand (so -7 % 3 is -1, not 2). It binds tighter than + and - but at the same level as * and /.

Comparison operators

Compare values using >, <, >=, <=, ==, !=:

1
2
/**
 * Compares a value
 * @param value
 */
{template .describe}
  {if $value > 10}
    Greater than 10
  {elseif $value == 10}
    Exactly 10
  {else}
    Less than 10
  {/if}
{/template}

Logical operators

Combine boolean conditions with and, or, and not:

1
2
/**
 * Demonstrates logical operators
 * @param x
 * @param y
 */
{template .logicalOps}
  {if $x and $y}
    Both are true
  {elseif $x or $y}
    At least one is true
  {else}
    Both are false
  {/if}
{/template}

Ternary operator (conditional expression)

For simple true/false cases, use the ternary operator:

1
2
/**
 * Uses ternary operator
 * @param isActive
 */
{template .status}
  Status: {$isActive ? 'Active' : 'Inactive'}
{/template}

Null-coalescing operator (?:)

The ?: operator returns its left operand unless that operand is null or undefined, in which case it returns the right operand. It is the Soy equivalent of JavaScript's ?? operator.

1
2
{namespace demo.nullCoalescing}

/** @param? value */
{template .basic}
  {$value ?: 'fallback'}
{/template}
1
2
basic({ value: 'hello' });  // → "hello"
basic({ value: null });     // → "fallback"
basic({});                  // → "fallback"  ($value is undefined)
basic({ value: '' });       // → ""          (empty string is NOT null!)
basic({ value: 0 });        // → "0"         (zero is NOT null!)

The operator is right-associative, so a chain reads left-to-right and returns the first non-null value:

1
2
{template .chain}
  {$a ?: $b ?: $c ?: 'last-resort'}
{/template}

It composes naturally with null-safe access to provide a default for a possibly-missing chain:

1
2
/** @param? user */
{template .greet}
  Hello, {$user?.name ?: 'stranger'}!
{/template}

?: vs the ternary ? : — same characters, different operator

These look superficially similar but they are two different operators in this fork:

FormBehaviorReads left/right of ? and : as
cond ? a : b (with whitespace)classic ternary — cond is a boolean testthree operands
expr ?: fallback (no operand between ? and :)null-coalescing — expr is the value to test for nulltwo operands

The parser distinguishes them by whether a middle operand is present. $x ? $x : $y and $x ?: $y are roughly equivalent in result, but the latter only evaluates $x once and only considers null/undefined as the "fall-back" trigger (not falsy values like '' or 0).

?? is NOT supported in this fork

Newer Closure Templates and other Soy dialects accept JavaScript's ?? syntax for the same operation. This fork does not. Operator.java defines NULL_COALESCING with the literal token ?:, and the parser (ExpressionParser.jj) raises a parse error on ??. If you copy an example from upstream Soy docs that uses $x ?? 'default', rewrite it as $x ?: 'default' before it will compile here.

List and record/map literals

You can build collections inline inside any expression position (typically the right-hand side of a {let ... /} or a {param ... /}).

List literals: [a, b, c]

1
2
{template .constList}
  {let $items: ['red', 'green', 'blue'] /}
  {foreach $c in $items}{$c}{if not isLast($c)}, {/if}{/foreach}
{/template}
1
2
constList(); // → "red, green, blue"

You can mix template parameters and constants in the same literal, and the result of the literal can be passed straight into another template:

1
2
/**
 * @param first
 * @param second
 */
{template .listAsParam}
  {call .joinList}
    {param items: [$first, $second, 'fixed'] /}
  {/call}
{/template}

The empty literal [] is also valid — useful as a default for an optional parameter:

1
2
/**
 * @param? items
 */
{template .emptyDefault}
  {let $list: $items != null ? $items : [] /}
  Count: {length($list)}
{/template}

Record vs. map literals: [k: v, k: v]

The same [key: value, ...] syntax produces two different types depending on whether all keys are constant strings:

All keys are constant strings?Inferred typeHow to read it
Yesrecord [primary: string, secondary: string]$colors.primary (dotted)
No (any key is dynamic)map$m[$k] (bracket)

This is enforced by the type checker: try to bracket-access a record and you get Type [...] does not support bracket-access.

Record literal (constant keys)

1
2
{template .constRecord}
  {let $colors: ['primary': 'blue', 'secondary': 'green'] /}
  {$colors.primary}/{$colors.secondary}
{/template}
1
2
constRecord(); // → "blue/green"

Map literal (at least one dynamic key)

1
2
/**
 * @param k
 * @param v
 */
{template .interpolatedMap}
  {let $m: [$k: $v, 'other': 'fixed'] /}
  {$m[$k]}|{$m['other']}
{/template}
1
2
interpolatedMap({ k: 'foo', v: 'bar' }); // → "bar|fixed"

Even though only one key ($k) is dynamic, the entire literal is typed as a map and every lookup must use bracket access. If you need to mix, build a record literal first and then index named keys with dotted access.

Gotchas

  • Don't try to bracket-access a constant-key literal. {$colors['primary']} will fail to compile with Type does not support bracket-access even though the key looks like an obvious string.
  • Don't try to dotted-access a dynamic-key literal. Once any key is non-constant, the type degrades to map and the dotted form is rejected.
  • The map runtime uses soy.$$checkMapKey for dynamic key lookups; if you port these patterns to a different runtime make sure that helper is available.

Conditional Logic

If/Elseif/Else statements

Use if, elseif, and else to conditionally render content based on boolean expressions.

1
2
{namespace demo.controlflow}

/**
 * Displays a message based on a numeric value
 * @param value
 */
{template .describeNumber}
  {if $value > 10}
    Greater than 10
  {elseif $value > 5}
    Greater than 5
  {else}
    5 or less
  {/if}
{/template}

Truthiness vs. presence

{if $x} follows JavaScript truthiness, not "is $x defined". The condition compiles directly to (opt_data.x) ? … : …, so the falsy values are exactly the JS ones:

$x{if $x}
undefined (missing key)falsy
nullfalsy
falsefalsy
0falsy
'' (empty string)falsy
[] (empty list)truthy (JS arrays are objects)
{} (empty record)truthy
any non-empty string, non-zero numbertruthy

Two consequences worth pinning down:

  • {if $count} is the wrong test for "the user passed count" when 0 is a legal value. {if isNonnull($count)} returns true for 0 and false only for null/undefined, which is usually what you actually want.
  • An empty list is truthy. {if $items} cannot distinguish "no items" from "some items"; use {if length($items) > 0} (or, equivalently, {if length($items)} since 0 is falsy) when you mean "non-empty".

For reference: isNonnull($x) is true for everything except null and undefined; the null-coalescing operator ?: ({$x ?: 'default'}) likewise tests only for null/undefined, not falsiness.

Switch/Case statements

1
2
/**
 * Returns a traffic light status message
 * @param color
 */
{template .trafficLight}
  {switch $color}
    {case 'red'}
      Red light - Stop!
    {case 'green'}
      Green light - Go!
    {default}
      Unknown color
  {/switch}
{/template}

Multiple case values

A single case can match multiple values (comma-separated):

1
2
{switch $numMarbles}
  {case 0}
    You have no marbles.
  {case 1, 2, 3}
    You have a normal number of marbles.
  {case 4, 5, 6}
    You have quite a few marbles.
  {default}
    You have a lot of marbles!
{/switch}

Intermediate Values and Local Variables

Let bindings

Use let to define intermediate values that can be reused within a template. Let bindings are immutable - once defined, they cannot be changed.

Basic let binding

1
2
{namespace demo.letbindings}

/**
 * @param x number
 * @param y number
 */
{template .binding}
  {let $sum: $x + $y /}
  The sum is {$sum}
{/template}

Use cases for let

  • Reusable values: Define a value once and use it multiple times instead of repeating the calculation
  • Readability: Give complex expressions meaningful names
  • Conditional logic: Store boolean results to use in multiple conditional branches

Block-form let ({let $x}...{/let})

The forms above use the expression variant: {let $sum: $a + $b /}. There is also a block variant where the body between {let $name} and {/let} is rendered like template content and the result is assigned to $name:

1
2
/**
 * @param name
 */
{template .blockForm}
  {let $greeting}Hello, {$name}!{/let}
  {$greeting}
{/template}

Block form is the right tool when the right-hand side needs template commands{if}, {foreach}, {call}, etc. None of those fit inside the expression form.

Block-form let with conditional content

1
2
/**
 * @param name
 * @param greet
 */
{template .blockFormConditional}
  {let $message}
    {if $greet}Hello, {$name}!{else}Goodbye, {$name}.{/if}
  {/let}
  {$message}
{/template}
1
2
blockFormConditional({ name: 'Ada', greet: true });  // → "Hello, Ada!"
blockFormConditional({ name: 'Ada', greet: false }); // → "Goodbye, Ada."

Block-form let collecting loop output

A common pattern is to render a list once and reuse the result — for example to pass it to another template, or to count whether it was empty:

1
2
/**
 * @param items list<string>
 */
{template .blockFormLoop}
  {let $rendered}
    {foreach $item in $items}
      {if not isFirst($item)}, {/if}{$item}
    {/foreach}
  {/let}
  Items: {$rendered}
{/template}
1
2
blockFormLoop({ items: ['a', 'b', 'c'] }); // → "Items: a, b, c"

Choosing between the two forms

Both are immutable bindings scoped to the enclosing template; the only difference is what you can put on the right-hand side.

FormSyntaxUse when
Expression{let $x: expr /}The right-hand side is an arithmetic, string, or list expression.
Block{let $x}...{/let}The right-hand side needs {if}, {foreach}, {call}, plain HTML, or any mix of those.

Parameter Types

Soy supports a rich type system for template parameters. Parameters can be typed to enable compile-time type checking and safer code. Here are the main types you'll encounter:

Primitive Types

string

Text values. All string parameters are auto-escaped when printed to prevent XSS.

1
2
{namespace demo.types}

/**
 * @param text string The text to display
 */
{template .stringExample}
  Text: {$text}
{/template}

'A & B' is rendered as 'A & B' because of auto-escaping (covered in Automatic HTML escaping).

number

Numeric values, including integers and floating-point numbers. In JavaScript, there's no distinction between int and float.

1
2
/**
 * @param value number A numeric value
 * @param price number A price in dollars
 */
{template .numberExample}
  Value: {$value}, Price: {$price}
{/template}

bool

Boolean values representing true or false conditions.

1
2
/**
 * @param isActive Whether the feature is active
 */
{template .boolExample}
  Status: {if $isActive}Active{else}Inactive{/if}
{/template}

Collection Types

list

An ordered collection of values. Lists can contain any type of values (strings, numbers, objects, etc.) and can even be mixed types.

1
2
/**
 * @param items A list of items
 */
{template .listExample}
  Count: {length($items)} items
{/template}

A list ['text', 42, true] happily mixes types — lists are not parameterised in this fork.

record (object)

A record is a collection of key-value pairs (similar to a JavaScript object). Records allow you to pass complex data structures to templates.

1
2
/**
 * @param data An object with properties
 */
{template .recordExample}
  Data received: {$data}
{/template}

Printing a whole record bare (as above) just calls JavaScript's toString() on it — you get "[object Object]". To render fields, drill in with {$data.name} or iterate.

Nullable Types (and the three different ? meanings)

In this repo's Soy dialect, ? appears in three different roles:

  1. @param? name TYPE in SoyDoc means the parameter is optional (it may be omitted).
  2. TYPE? means the type is nullable (for example string? allows null).
  3. ? by itself is the unknown type (it disables type checking for that value).

These are different syntaxes with different meanings.

Nullable String

1
2
/**
 * @param? message string Optional message (may be omitted / undefined)
 */
{template .nullableStringExample}
  {if $message != null}
    Message: {$message}
  {else}
    No message provided
  {/if}
{/template}

Nullable Number

1
2
/**
 * @param? count number Optional count (may be omitted / undefined)
 */
{template .nullableNumberExample}
  Count: {if $count != null}{$count}{else}Not provided{/if}
{/template}

Null-safe access (?. and ?[])

When you reach into a nullable value, Soy's null-safe access operators let you skip the manual if $x != null guard. They work the same way as JavaScript's optional chaining: as soon as a segment in the chain sees a null/undefined receiver, the whole expression becomes null and prints as the empty string.

There are two forms:

  • $obj?.field — null-safe dotted access.
  • $obj?[$key] — null-safe bracket access (for dynamic or non-identifier keys).

Basic usage

1
2
/**
 * @param? user
 */
{template .nameOrEmpty}
  {$user?.name}
{/template}
1
2
nameOrEmpty({ user: { name: 'Ada' } }); // → "Ada"
nameOrEmpty({ user: null });            // → ""        (no NPE)
nameOrEmpty({});                        // → ""        ($user is undefined)
1
2
/**
 * @param? user
 */
{template .deepNameOrEmpty}
  {$user?.profile?.displayName}
{/template}

A null at any step ($user, $user.profile, $user.profile.displayName) produces "". You only need a ?. between the segment that might be null and the next access; there is no penalty for repeating it on every link, and doing so is a useful in-line signal that the value above might be missing.

Dynamic keys with ?[]

1
2
/**
 * @param? data
 * @param key
 */
{template .lookup}
  {$data?[$key]}
{/template}

Combining with a fallback

?. returns null on a missing chain, so you can pair it with the ternary operator to substitute a default:

1
2
/**
 * @param? user
 */
{template .nameOrAnonymous}
  {$user?.name ? $user.name : 'Anonymous'}
{/template}

Inside an {if} condition

A null-safe expression evaluates to falsy when any link is null, so you can drop the explicit $user != null and ... guard:

1
2
/**
 * @param? user
 */
{template .greetIfPresent}
  {if $user?.name}Hello, {$user.name}!{else}Hello, stranger.{/if}
{/template}

Gotchas

  • ?. short-circuits on null and undefined, but not on the empty string or 0. {$user?.name} will still print "" if $user.name is the empty string — the ? only catches missing links.
  • The result of $x?.y is null (or whatever the chain produced); it is not auto-converted to the empty string until it reaches a print context. In an arithmetic expression a null result will still propagate as null and may surprise you — wrap with the ternary form above when you need a typed fallback.
  • The non-null counterpart still applies: once you have proven the chain is present (e.g. inside the {if $user?.name} branch above), you can use plain $user.name without the ?. — that's faster to read and makes intent explicit.

Safe String Types

Soy has special string types for different contexts to prevent security vulnerabilities:

html - HTML-safe content

Use this for content that should NOT be escaped because it's already safe HTML (typically from another template).

1
2
/**
 * @param content Safe HTML content
 */
{template .htmlExample}
  <div>{$content}</div>
{/template}

Note: In standard Soy, content is auto-escaped by default. The html type is used for pre-sanitized content. All user input should remain as plain strings and will be auto-escaped.

uri - Safe URL content

Use this for URLs that come from trusted sources.

1
2
/**
 * @param url A safe URL
 */
{template .uriExample}
  <a href="{$url}">Link</a>
{/template}

Type Checking in Templates

When you declare parameter types, Soy performs compile-time type checking. This means:

  • Type mismatches are caught at compile time, not runtime
  • Better IDE support with autocompletion
  • Clearer intentions - other developers see what types are expected
  • Smaller code generation - the compiler can optimize better
1
2
/**
 * Template with explicit types
 * @param name A required string
 * @param? count An optional number
 */
{template .strictlyTyped}
  {$name}: {if $count}{$count}{else}N/A{/if}
{/template}

This is better than:

1
2
/**
 * Less clear - any type accepted
 * @param name
 * @param? count
 */
{template .untyped}
  {$name}: {if $count}{$count}{else}N/A{/if}
{/template}

HTML Output and Safety

Automatic HTML escaping

By default, all variables printed in templates are automatically HTML-escaped. This prevents XSS (cross-site scripting) vulnerabilities by converting potentially dangerous HTML characters into safe entities.

How auto-escaping works

1
2
{namespace demo.html}

/**
 * Demonstrates HTML auto-escaping
 * @param userInput
 */
{template .autoEscape}
  <div>{$userInput}</div>
{/template}

Usage:

1
2
3
HtmlNS.autoEscape({ userInput: '<script>alert("xss")</script>' });
// Output: <div><script>alert("xss")</script></div>

What gets escaped

  • < becomes <
  • > becomes >
  • & becomes &
  • " becomes "
  • ' becomes &#39;

This keeps user-provided content safe while still rendering legitimate HTML markup that you author in the template.

Choosing an autoescape mode

Auto-escaping is configurable via the autoescape attribute on {namespace} (sets the default for every template in the file) and on {template} (overrides the namespace setting for one template). Four values are accepted in this fork:

ValueBehaviorWhen to use
"true" (default)HTML-escape every print siteThe right default for almost every template; pick the same escape no matter where the value lands.
"false"Never escapePre-rendered HTML produced by trusted code paths (and double-checked for XSS).
"contextual"Pick HTML / attribute / URI / JS / CSS escape based on the surrounding HTML context the compiler infersTemplates that mix HTML body, attribute, URL, <script>, and <style> content.
"strict"Like contextual, but also tracks a "content kind" for the template's own outputRequired for {template ... kind="text"} and {let ... kind="..."}...{/let}.

Newer Soy dialects renamed several of these (e.g. deprecated-noncontextual, deprecated-contextual, plus a stricter modern strict). Those names are not accepted by this fork — the parser rejects autoescape="deprecated-noncontextual" outright. Stick to the four values above.

Default: autoescape="true"

Equivalent to omitting the attribute. Every print site gets HTML-escaped:

1
2
{namespace demo.escapeTrue autoescape="true"}

/**
 * @param x
 */
{template .render}
  <p>{$x}</p>
{/template}
1
2
render({ x: '<b>' }); // → "<p><b></p>"

Disabled: autoescape="false"

Print sites pass straight through. Do not use this — this is a frequent XSS vector.

1
2
{namespace demo.escapeFalse autoescape="false"}

/**
 * @param x
 */
{template .render}
  <p>{$x}</p>
{/template}
1
2
render({ x: '<b>' }); // → "<p><b></p>"   (raw, unescaped)
Per-template override

Do not use this: Instead only disable auto-escape at individual print directives.

You can flip the mode on a single template with the same attribute on {template}. Inside an autoescape="false" namespace, this template re-enables escaping just for itself:

1
2
/**
 * @param x
 */
{template .renderEscaped autoescape="true"}
  <p>{$x}</p>
{/template}
1
2
renderEscaped({ x: '<b>' }); // → "<p><b></p>"

The override works in the other direction too: a single autoescape="false" template can opt out of an autoescape="true" namespace.

Smartest: autoescape="contextual"

The compiler walks the parser context as it reads the template and chooses the right escape for each print site. The same {$x} produces different output depending on whether it sits in HTML body, an attribute value, a URL attribute, a <script> block, or a <style> block.

1
2
{namespace demo.escapeContextual autoescape="contextual"}

/**
 * @param x
 */
{template .render}
  <a href="/user?id={$x}" title="{$x}">{$x}</a>
{/template}
1
2
3
4
render({ x: '<x>' });
// → '<a href="/user?id=%3Cx%3E" title="<x>"><x></a>'
//        ^^^ URI-escaped       ^^^ attr-escaped   ^^^ HTML-escaped

For mixed templates this is meaningfully safer than autoescape="true", but the difference is not where most people guess. Both modes already escape " and < everywhere — including inside HTML attribute values — so a string like a"><script>x</script> injected into id="{$x}" is rendered as id="a"><script>x</script>" under either setting and cannot break out of the attribute.

Where the modes diverge is at non-HTML-text print sites — places where HTML escaping is the wrong answer:

Print siteautoescape="true"autoescape="contextual"
HTML body (<p>{$x}</p>)HTML escapeHTML escape
HTML attribute (id="{$x}")HTML escape (works fine)HTML-attribute escape (slightly stricter)
URI attribute (href="{$x}")HTML escape onlyjavascript:alert(1) passes throughURI filter rejects unsafe schemes (about:invalid#zSoyz)
<script>...{$x}...</script>HTML escape (still emits literal < etc., XSS-prone)JS-string escape
<style>...{$x}...</style>HTML escape (wrong)CSS-value escape

So the practical reason to choose contextual over true is javascript: URIs and embedded <script>/<style> content. If your template only renders plain HTML body text and ordinary attributes (id, class, data-*), the two modes produce identical safety guarantees and true is fine.

Strict: autoescape="strict"

Builds on contextual mode and additionally lets each template (and each {let ...} block) declare a kind="..." attribute that describes what kind of content it produces. This is what unlocks the {template .x kind="text"} and {let $x kind="html"}...{/let} forms covered in their own sections later in this document.

If you don't need kind="..." attributes, prefer contextual — it imposes fewer restrictions on what templates may contain.

Content kinds (kind="...")

Once a namespace is in autoescape="strict", two related forms become available: a kind="..." attribute on {template} and on block-form {let}. Both attach a content-kind label to the value the template (or block) produces, which the contextual escape system then uses to decide what to do when that value is later printed somewhere.

Common kinds:

kind="..."What the value represents
"html"HTML body content (the implicit default for templates)
"text"Plain text — not HTML, not escaped
"uri"A URL fragment or whole URL
"attributes"One or more HTML attribute name/value pairs
"js"A JavaScript expression or statement
"css"A CSS declaration or rule

In this fork's stripped-down JS runtime the practical difference is most visible for "html" (skips re-escaping when printed) and "text" (carries the no-markup intent through to the print site).

Plain-text templates: {template ... kind="text"}

Use this for emails, JSON snippets, plain-text files — anything where HTML escaping would be wrong. Inside a kind="text" template, the literal characters in the body are emitted verbatim (< stays <), and parameters are escaped for plain-text context (which usually means no escaping at all).

1
2
{namespace demo.kindBlocks autoescape="strict"}

/**
 * @param name
 */
{template .plainTextEmail kind="text"}
Hello {$name},

Thanks for signing up.
{/template}

Note that Soy still applies whitespace normalisation (collapsing leading/trailing whitespace per line) inside the template body. The point of kind="text" is "this output is not HTML", not "preserve every space and newline". For literal whitespace, use {sp} and {\n} (covered in the Special Characters section).

HTML templates: {template ... kind="html"}

This is the implicit default for every template, so you almost never need to write kind="html" explicitly. It's only worth spelling out for symmetry next to a kind="text" sibling, or when extracting a template from a non-strict file:

1
2
/**
 * @param name
 */
{template .htmlGreeting kind="html"}
  <p>Hello, {$name}!</p>
{/template}

Block-form {let} with a content kind

The same kind="..." attribute is accepted on block-form {let} declarations. This lets you build up a chunk of content in one variable, label it appropriately, and then drop it into a different context without the type system getting in your way.

{let $x kind="text"}

Capture a chunk of plain text. Useful for tooltip values, data-* attribute payloads, or email subjects — any place you compute a string that is not HTML:

1
2
/**
 * @param name
 */
{template .letKindText}
  {let $subject kind="text"}
    Welcome, {$name}!
  {/let}
  Subject: {$subject}
{/template}

{let $x kind="html"}

Capture pre-built HTML and have it survive a later print unchanged (no double escaping):

1
2
/**
 * @param name
 */
{template .letKindHtml}
  {let $card kind="html"}
    <p>Hello, <strong>{$name}</strong>!</p>
  {/let}
  <div class="greeting">{$card}</div>
{/template}
1
2
letKindHtml({ name: 'Ada' });
// → '<div class="greeting"><p>Hello, <strong>Ada</strong>!</p></div>'

Without kind="html" the very same body would be re-escaped on the way into <div class="greeting">, producing <p>Hello, <strong>Ada</strong>!</p>. The kind label is what tells the autoescape system "trust me, this is already valid HTML body content".

Caveats

  • kind="..." attributes only compile under autoescape="strict". Trying to use them in autoescape="contextual" (or the default "true") gives {let} node with 'kind' attribute is only permitted in contextually autoescaped templates.
  • The values produced by kind="text" and kind="html" blocks are SanitizedContent objects in real Closure runtimes. The test runtime in this repo uses simplified stubs that mark such values with a __soySafe flag so that the print-side escapers know to pass them through; if you port these patterns elsewhere make sure your runtime has the corresponding soydata.VERY_UNSAFE.ordainSanitized* and soydata.markUnsanitizedText helpers wired up.

Rendering HTML structure

You can safely include HTML markup directly in templates. This markup is not escaped:

1
2
/**
 * Renders a card with title and content
 * @param title
 * @param content
 */
{template .card}
  <div class="card">
    <h2>{$title}</h2>
    <p>{$content}</p>
  </div>
{/template}

The HTML structure is hardcoded and safe. The {$title} and {$content} variables are escaped, so user input can't break out of the intended structure.

Working with Lists

Checking if a list is empty

Use the length() function to get the number of items in a list:

1
2
{namespace demo.html}

/**
 * Displays a count of items or a no-items message
 * @param items
 */
{template .checkEmpty}
  {if length($items) == 0}
    No items available
  {else}
    Found {length($items)} items
  {/if}
{/template}

Iterating over lists

This fork's {for} loop only accepts a range(...) argument (see Iterating with {for $i in range(...)}). The list-iteration form {for $item in $items} that newer Soy dialects support does not compile here — the parser throws Invalid 'for' command text. To walk a list, use {foreach} instead.

Basic iteration

1
2
/**
 * Lists items from an array
 * @param items
 */
{template .listItems}
  <ul>
    {foreach $item in $items}
      <li>{$item}</li>
    {/foreach}
  </ul>
{/template}

Iteration with index

{foreach} does not bind a separate $index variable. Use the loop helper index($item) (covered under Iteration helper functions) instead:

1
2
/**
 * Lists items with their position
 * @param items
 */
{template .indexedList}
  {foreach $item in $items}
    {if not isFirst($item)}, {/if}Item {index($item)}: {$item}
  {/foreach}
{/template}

Iterating with {for $i in range(...)}

For numeric loops, the {for} command accepts exactly one shape: a single loop variable and a range(...) call. Anything else (including {for $item in $items} over a list, or {for $i, $j in ...}) raises Invalid 'for' command text at compile time.

range(...) accepts 1, 2, or 3 arguments, the same way Python's built-in range does:

FormIterates
range(stop)0, 1, ..., stop-1
range(start, stop)start, start+1, ..., stop-1
range(start, stop, step)start, start+step, ... while < stop

Examples

1
2
{namespace demo.forRange}

/** @param stop */
{template .rangeStop}
  {for $i in range($stop)}
    {if $i > 0}, {/if}{$i}
  {/for}
{/template}

/** @param start @param stop */
{template .rangeStartStop}
  {for $i in range($start, $stop)}
    {if $i > $start}, {/if}{$i}
  {/for}
{/template}

/** @param start @param stop @param step */
{template .rangeStartStopStep}
  {for $i in range($start, $stop, $step)}
    {if $i > $start}, {/if}{$i}
  {/for}
{/template}
1
2
rangeStop({ stop: 5 });                                   // → "0, 1, 2, 3, 4"
rangeStartStop({ start: 2, stop: 6 });                    // → "2, 3, 4, 5"
rangeStartStopStep({ start: 0, stop: 10, step: 2 });      // → "0, 2, 4, 6, 8"

Gotchas

  • range(...) is not a standalone function: {let $r: range(5) /} fails with Unrecognized function range. It is only valid as the immediate argument to {for}.
  • Iteration helpers isFirst(), isLast(), and index() are {foreach}-only — inside a {for $i in range(...)} use the loop variable directly (e.g. {if $i == 0}).
  • An empty range (e.g. range(0) or range(5, 5)) produces zero iterations and emits nothing.

The {foreach} loop with {ifempty}

The {foreach} loop is specifically designed for iterating over lists and provides built-in functions for detecting first/last elements and getting the index.

It also supports an optional {ifempty} clause that renders when the list is empty. This is cleaner than checking length($items) == 0 manually.

1
2
/**
 * @param items
 */
{template .foreachWithIfempty}
  {foreach $item in $items}
    {if not isFirst($item)}, {/if}{$item}
  {ifempty}
    No items found
  {/foreach}
{/template}

Iteration helper functions

Inside a {foreach} loop, you can use these helper functions:

FunctionDescriptionExample
isFirst($item)Returns true if this is the first itemAdd opening bracket
isLast($item)Returns true if this is the last itemAdd closing bracket
index($item)Returns the zero-based index of the itemDisplay "Item 1, Item 2, ..."

Example: First and last detection

1
2
/**
 * @param items
 */
{template .foreachFirstLast}
  {foreach $item in $items}
    {if isFirst($item)}[{/if}
    {$item}
    {if isLast($item)}]{else}, {/if}
  {/foreach}
{/template}

Example: Getting the index

1
2
/**
 * @param items
 */
{template .foreachWithIndex}
  {foreach $item in $items}
    {index($item)}: {$item}{if not isLast($item)}, {/if}
  {/foreach}
{/template}

Practical example: HTML list with empty state

1
2
/**
 * @param items
 */
{template .foreachHtmlList}
  {foreach $item in $items}
    <li>{$item}</li>
  {ifempty}
    <li class="empty">No items available</li>
  {/foreach}
{/template}

{foreach} vs {for}

This fork has two distinct loop commands with non-overlapping responsibilities:

CommandUse forLoop helpers
{foreach $item in $list}Walking a list valueisFirst($item), isLast($item), index($item)
{for $i in range(...)}Counting through a numeric rangeNone — use the loop variable directly

You cannot swap them: {for $item in $items} over a list raises Invalid 'for' command text, and {foreach $i in range(5)} raises a parse error too. See Iterating with {for $i in range(...)} for the numeric form.

Newer Soy dialects added a unified {for $item, $index in $list} form. That syntax does not compile here.

Expression Evaluation and Short-Circuiting

Soy expressions largely follow JavaScript's evaluation rules.

Logical operators and short-circuiting

The logical operators and and or evaluate left-to-right and short-circuit just like JavaScript:

  • A and B only evaluates B if A is truthy.
  • A or B only evaluates B if A is falsy.

This is important when later parts of the expression could fail (for example, accessing properties on nullable data).

1
2
{namespace demo.expressions}

/**
 * Demonstrates evaluation order and short-circuiting for logical operators
 * @param x
 * @param y
 */
{template .evaluationOrder}
  {if $x and $y}
    both
  {elseif $x or $y}
    one
  {else}
    none
  {/if}
{/template}

You can rely on and/or short-circuiting when protecting potentially unsafe operations:

1
2
{if $user and $user.profile}
  {$user.profile.displayName}
{/if}

If $user is null or undefined, the second operand is never evaluated, so this pattern is safe.

Operator precedence

Soy follows the same basic precedence as JavaScript:

  • Multiplication/division before addition/subtraction (*, /+, -)
  • Comparisons (<, >, ==, etc.) after arithmetic
  • Logical and/or after comparisons
  • Ternary ?: after logical operators

Use parentheses to make complex expressions easier to read, to make the evaluation order explicit, or to change the evaluation order:

1
2
{let $score: ($correct * 2 + $bonus) / $max /}
{if ($score >= 0.9) and not $isDisabled}
  Excellent
{/if}

When you write an expression with a print directive, Soy evaluates it in this order:

  1. Evaluate the main expression.
  2. Apply auto-escaping based on the current HTML context.
  3. Apply any print directives (for example |truncate, |escapeUri).

For example, in this template:

1
2
// Conceptual example (not compiled in this repo):
// {template .truncateEscaped}
//   {$text|truncate:10}
// {/template}

In this older fork of Soy the same evaluation order applies, but the examples in this repo use the concrete templates in the Print Directives section (such as .truncateExample) rather than this exact truncateEscaped helper.

  • $text is first converted to a string.
  • HTML special characters (<, >, &, quotes) are escaped.
  • The already-escaped string is then truncated to 10 characters.

If $text is <script>, it becomes <script> (13 characters) and then is truncated to <script....

Built-in Functions

Soy provides several built-in functions for common operations. These functions can be used in expressions anywhere a value is expected.

List Functions

length(list)

Returns the number of items in a list.

1
2
{namespace demo.functions}

/**
 * Demonstrates the length() function
 * @param items
 */
{template .lengthExample}
  Count: {length($items)}
{/template}

Index Access ($list[$index])

Access list elements by their zero-based index:

1
2
/**
 * @param items
 */
{template .indexAccess}
  First: {$items[0]}, Second: {$items[1]}
{/template}

Note: This older Atlassian Soy fork does not support advanced list methods like join(), slice(), reverse(), concat(), or split(). To join a list into a string, you must iterate manually:

Math Functions

round(number[, numDigitsAfterDecimalPoint])

Rounds a number to the nearest integer, or to a specified number of decimal places.

1
2
/** */
{template .roundExample}
  Rounded: {round(3.7)}
{/template}

/** */
{template .roundWithPlaces}
  Rounded: {round(3.14159, 2)}
{/template}

round(3.3)3; round(3.14159, 2)3.14.

floor(number)

Returns the largest integer less than or equal to the number (rounds toward negative infinity).

1
2
/** */
{template .floorExample}
  Floor: {floor(3.9)}
{/template}

ceiling(number)

Returns the smallest integer greater than or equal to the number (rounds toward positive infinity).

1
2
/** */
{template .ceilingExample}
  Ceiling: {ceiling(3.1)}
{/template}

min(number, number)

1
2
/** */
{template .minExample}
  Min: {min(4, 7)}
{/template}

max(number, number)

1
2
/** */
{template .maxExample}
  Max: {max(4, 7)}
{/template}

randomInt(rangeArg)

Returns a non-negative pseudo-random integer in the range [0, rangeArg). The argument is the exclusive upper bound — randomInt(6) produces one of 0, 1, 2, 3, 4, 5.

1
2
{namespace demo.newFunctions}

/**
 * Random integer in [0, $max).
 * @param max number
 */
{template .randomInRange}
  {randomInt($max)}
{/template}

randomInt(0) is undefined behaviour (an empty range) — guard the argument if $max could be zero. There is no two-argument form for an arbitrary [lo, hi) interval; build it as $lo + randomInt($hi - $lo) if you need one. Because every call re-rolls, calling randomInt twice in the same template will produce two independent values.

String Functions

Soy provides several functions for working with strings. These are especially useful for searching, extracting substrings, and checking string contents.

length(string)

Returns the length (number of characters) of a string. This is the same length() function used for lists.

1
2
/** */
{template .stringLength}
  Length: {length('hello')}
{/template}

strLen(string)

A string-specific version of length(). Returns the length of a string.

1
2
/** */
{template .strLenExample}
  Length: {strLen('hello')}
{/template}

strContains(string, substring)

Checks if a string contains a substring. Returns true if found, false otherwise. This check is case-sensitive.

1
2
/** */
{template .strContainsExample}
  Contains: {strContains('hello world', 'world') ? 'yes' : 'no'}
{/template}

strIndexOf(string, substring)

Returns the zero-based index of the first occurrence of a substring within a string. Returns -1 if the substring is not found. The match is case-sensitivestrIndexOf('Hello World', 'world') returns -1, while strIndexOf('Hello World', 'World') returns 6. There is no case-insensitive variant; lowercase both operands first if you need that behaviour (the runtime helper compiles to JS String.prototype.indexOf).

1
2
/** */
{template .strIndexOfExample}
  Position: {strIndexOf('hello world', 'world')}
{/template}

strSub(string, startIndex, endIndex)

Extracts a substring from a string. The extraction starts at startIndex (inclusive) and ends at endIndex (exclusive). Indices are zero-based.

1
2
/** */
{template .strSubExample}
  Substring: {strSub('hello world', 0, 5)}
{/template}

Map and Type Functions

isNonnull(value)

Returns true when the argument is neither null nor undefined. It is the only built-in null/undefined predicate in this fork — there is no separate isNull() (write not isNonnull($x) instead).

1
2
/** @param? value */
{template .isNonnullCheck}
  {if isNonnull($value)}has value: {$value}{else}missing{/if}
{/template}

isNonnull does not check for empty strings or zero — isNonnull('') and isNonnull(0) are both true. For "is this present and truthy" use the value directly in a boolean context ({if $value}) or combine with the null-coalescing operator ({$value ?: 'fallback'}).

augmentMap(baseMap, additionalMap)

Returns a new map that contains every key from baseMap plus every key from additionalMap, with additionalMap winning on conflicts. Neither input is mutated. The most common use is overlaying a small set of overrides onto a record before passing it to a {call}:

1
2
/**
 * @param base map<string, string>
 * @param overrides map<string, string>
 */
{template .augment}
  {let $merged: augmentMap($base, $overrides) /}
  a={$merged.a}, b={$merged.b}, c={$merged.c}
{/template}
1
2
NewFunctionsNS.augment({
  base:      { a: '1', b: '2' },
  overrides: { b: 'X', c: '3' },
});
// → "a=1, b=X, c=3"

Mixing records and maps

augmentMap does not distinguish between Soy record literals and map-typed parameters at runtime — both compile down to plain JavaScript objects, and the function iterates own keys with Object.prototype.hasOwnProperty. Every combination works:

BaseAdditionalWorks?
Record literal ['a': 1, 'b': 2]Record literal ['b': 99, 'c': 3]yes
Record literalMap-typed param (plain object from caller)yes
Map-typed paramRecord literalyes
Map-typed paramMap-typed paramyes
Both dot access ($merged.a) and bracket access ($merged['a']) work on the result regardless of which side each key came from. Keys from additionalMap overwrite keys with the same name in baseMap; keys present only in baseMap are preserved.

Shallow merge — important for nested values

The merge is shallow, so a nested record/map on the additional side replaces the corresponding nested value on the base side wholesale rather than being merged with it:

1
2
{template .augNested}
  {let $base:  ['a': 1, 'nested': ['x': 10, 'y': 20]] /}
  {let $extra: ['nested': ['y': 200, 'z': 300]] /}
  {let $m: augmentMap($base, $extra) /}
  a={$m.a}|nx={$m.nested.x}|ny={$m.nested.y}|nz={$m.nested.z}
{/template}
// renders: a=1|nx=|ny=200|nz=300
//                ^^ base.nested.x is gone — extra.nested replaced the whole nested object

If you need a deep merge, build the result manually with multiple augmentMap calls or {let} bindings — there is no built-in deep-merge function.

Edge cases

  • This is the closest thing to a record-spread/object-spread in this fork. There is no {...spread} syntax.
  • Passing null/undefined as additionalMap is treated as an empty extra (the base passes through unchanged) by the runtime helper. Passing null/undefined as baseMap will fail when the result is later accessed; treat both arguments as required in your own templates.
  • The argument order matters: augmentMap(defaults, overrides) is the safe pattern. Reversing it to augmentMap(overrides, defaults) lets the defaults silently win, which is almost never what you want.

Bidi Functions

The bidi* family supports right-to-left (RTL) languages. None of them are wired up to a real locale in this JS-only test harness — the runtime defaults to LTR and the helpers return their LTR-side answers — but the surface area is documented here for completeness so that templates written today render correctly when a real bidi global is plugged in.

FunctionReturnsUse
bidiGlobalDir()1 for LTR, -1 for RTLRead the page-level direction.
bidiDirAttr(text)An HTML dir="..." attribute (or empty)Stamp a dir= on a wrapper when the text's direction differs from the page.
bidiTextDir(text [, isHtml])1, -1 or 0Estimate the direction of a runtime string. 0 means "neutral / unknown".
bidiStartEdge() / bidiEndEdge()'left' / 'right'Use in CSS so margins and floats flip with the locale.
bidiMark()The Unicode bidi mark for the page direction (LRM \u200E or RLM \u200F)Force a neutral character to render in a chosen direction.
bidiMarkAfter(text [, isHtml])A bidi mark string, or emptyAppend after text if its direction conflicts with the page's, to clean up trailing punctuation.

These are the runtime counterparts of the |bidiSpanWrap and |bidiUnicodeWrap print directives. In a single-locale, LTR-only application — which is the common case in this fork — you can ignore them; reach for them only when you actually need to handle mixed-direction text.

String Functions Reference

FunctionDescriptionExample
length($str)String length (also works on lists)length('hello')5
strLen($str)String lengthstrLen('hello')5
strContains($str, $sub)Check if contains substringstrContains('hello', 'ell')true
strIndexOf($str, $sub)Find position of substringstrIndexOf('hello', 'l')2
strSub($str, $start, $end)Extract substringstrSub('hello', 1, 4)'ell'

Note: These string functions are not Unicode-aware. They operate on characters (UTF-16 code units), not graphemes. Be careful when working with emoji or complex Unicode strings—you may split a multi-byte character incorrectly.

Internationalization (i18n)

Soy provides a {msg} command to mark strings for translation. While the translation extraction and runtime translation features require the full Closure infrastructure, the {msg} syntax is useful for marking up translatable content and provides a clear contract for i18n.

Basic {msg} Usage

Use {msg} to wrap text that should be translatable. Every {msg} requires a desc attribute describing the message's context for translators.

1
2
{namespace demo.messages}

/**
 * Demonstrates basic {msg} for translatable strings
 */
{template .basicMessage}
  {msg desc="A simple greeting"}
    Hello, World!
  {/msg}
{/template}

Note: In this JavaScript-only context, {msg} passes through the content directly. In a full Closure environment with translation files, the message would be replaced with the appropriate translation.

Messages with Parameters

You can include template parameters inside {msg} blocks. They become placeholders for translators:

1
2
/**
 * @param name
 */
{template .messageWithParam}
  {msg desc="Greeting with name placeholder"}
    Hello, {$name}!
  {/msg}
{/template}

Parameters are properly escaped, so user input is safe:

1
2
MessagesNS.messageWithParam({ name: '<script>' });  // "Hello, <script>!"

Messages with Multiple Parameters

1
2
/**
 * @param firstName
 * @param lastName
 */
{template .messageWithMultipleParams}
  {msg desc="Full name greeting"}
    Welcome, {$firstName} {$lastName}!
  {/msg}
{/template}

Messages with Embedded HTML

You can include HTML elements inside messages. This is useful for embedding links or formatting:

1
2
/**
 * @param link
 */
{template .messageWithHtml}
  {msg desc="Message with embedded HTML link"}
    Click <a href="{$link}">here</a> to continue.
  {/msg}
{/template}

The meaning Attribute

When you have the same text that means different things in different contexts, use the meaning attribute to disambiguate for translators:

1
2
/**
 * Demonstrates {msg} meaning attribute for disambiguation
 */
{template .messageWithMeaning}
  {msg meaning="email" desc="Send as in email"}
    Send
  {/msg}
{/template}

For example, "Send" could mean:

  • "Send" (as in sending an email) - meaning="email"
  • "Send" (as in a postal shipment) - meaning="shipping"

Both would have different translations in languages where the words differ.

The hidden Attribute

hidden="true" marks the message as hidden from translation tooling — it is metadata for the extraction pipeline, not a runtime flag. The message still renders normally:

1
2
{template .hiddenMsg}
  {msg desc="Internal placeholder text" hidden="true"}
    [DEBUG] coming soon
  {/msg}
{/template}

Use it for messages that you do not want translators to spend time on (debug strings, internal placeholders that will be removed before launch).

The phname Attribute on Placeholders

Inside a {msg} body, every dynamic print site becomes a placeholder in the extracted message. By default the compiler invents a name from the expression — {$user.firstName} becomes something like FIRST_NAME. Use phname="..." on the print site to give translators a stable, hand-picked name:

1
2
/**
 * @param name
 * @param count
 */
{template .phnameOnPrint}
  {msg desc="Greeting with named placeholders"}
    Hello, {$name phname="USER_NAME"}, you have {$count phname="ITEM_COUNT"} items.
  {/msg}
{/template}

The runtime output is unchanged. phname only matters at extraction time, but two rules trip people up:

  • It must sit on a direct print placeholder inside {msg}. Putting it on an expression nested inside an HTML attribute (e.g. <a href="{$url phname=\"LINK_URL\"}">) is rejected with Found 'phname' attribute not on a msg placeholder.
  • It does not move the value through any sanitisation different from a normal print — it is a label, not a directive.

{fallbackmsg}

{fallbackmsg} lets you deploy a new translatable string while still rendering an older, already-translated string when the new one has not yet made it through the translation pipeline. The body of {fallbackmsg} lives inside an enclosing {msg}:

1
2
{namespace demo.fallbackMsg}

{template .basic}
  {msg desc="Greet the visitor"}
    Welcome to the new site!
    {fallbackmsg desc="Old greeting, kept until the new one is translated"}
      Welcome!
    {/fallbackmsg}
  {/msg}
{/template}

/** @param name */
{template .withParam}
  {msg desc="Personalised greeting (new wording)"}
    Hi {$name}, glad to see you back!
    {fallbackmsg desc="Older personalised greeting"}
      Hello, {$name}!
    {/fallbackmsg}
  {/msg}
{/template}

The runtime always renders the primary {msg} body — the fallback only takes over when the translation system reports that the new message has no translation yet for the requested locale. In a JS-only test harness with no message bundle wired up, both messages compile cleanly and the primary text is what you see.

You may have at most one {fallbackmsg} per {msg}. The fallback's desc is independent of the outer desc and serves the same role for the older string.

{msg} Reference

AttributeRequiredDescription
descYesHuman-readable description for translators
meaningNoDisambiguates identical text with different meanings
hiddenNo"true" marks the message as hidden from the translation pipeline; runtime output is unchanged

Placeholder-level attribute (on a direct print site inside {msg}):

AttributeDescription
phnameStable placeholder name shown to translators, e.g. {$name phname="USER_NAME"}

Injected Data (ij‑data)

Some applications inject global or environment-specific values into Soy templates using injected data (a.k.a ij). It is the standard mechanism for cross-cutting values like context paths, feature flags, the current locale, etc. — values that almost every template needs but no caller wants to thread through {call} chains by hand.

In this fork, $ij works but the newer {@inject} declaration does not compile.

Reading from $ij

The compiler accepts bare $ij.x references inside any template — you don't need a SoyDoc entry for it and you don't need a declaration block:

1
2
{namespace demo.ijdata}

/**
 * Reads a single injected value via `$ij`.
 */
{template .greet}
  Hello, {$ij.userName}!
{/template}

The compiled template function has the signature t(opt_data, opt_sb, opt_ijData). Pass the injected data as the third argument:

1
2
greet({}, undefined, { userName: 'Ada' }); // → "Hello, Ada!"

Defensive access with ?.

$ij is a regular value, so the null-safe access operators apply. Combine them with the ternary to provide a default:

1
2
{template .safeAccess}
  Region: {$ij?.region ? $ij.region : 'unknown'}
{/template}
1
2
safeAccess({}, undefined, { region: 'eu' }); // → "Region: eu"
safeAccess({}, undefined, {});               // → "Region: unknown"
safeAccess({});                              // → "Region: unknown"   ($ij itself absent)

$ij flows through {call} automatically

When one template calls another, the callee sees the same $ij object — you never have to pass it explicitly:

1
2
{template .outer}
  [outer={$ij.userName}]
  {call .inner /}
{/template}

/** */
{template .inner}
  [inner={$ij.userName}]
{/template}
1
2
outer({}, undefined, { userName: 'Ada' });
// → "[outer=Ada][inner=Ada]"

This is the property that makes injected data useful: it gives you a kind of dynamic-scope channel for values that should be available everywhere without polluting every {param} list.

Limitations in JavaScript-Only Context

The following i18n features require the full Closure Compiler/translation infrastructure and are not available in this JavaScript-only setup:

  • {plural}: For count-based pluralization (e.g., "1 item" vs "5 items")
  • {select}: For gender or grammatical form selection
  • Translation file extraction: Extracting messages for translation
  • Runtime translation loading: Loading translated strings at runtime

Workaround for pluralization: Use {if} or {switch} for simple cases:

1
2
/**
 * @param count
 */
{template .simplePlural}
  {if $count == 0}
    No items
  {elseif $count == 1}
    One item
  {else}
    {$count} items
  {/if}
{/template}

Delegate Templates

Delegate templates let you define extensible rendering hooks that can be overridden by other Soy files. Instead of calling a specific template name, you call a delegate key, and whichever implementation is registered for that key at runtime will be used.

In this JavaScript-only repo we demonstrate how to declare and call delegates, plus how allowemptydefault behaves when no delegate is registered.

Basic delegate and delcall

The simplest delegate pattern is a default implementation plus a caller that uses {delcall}:

1
2
{namespace demo.delegates}

/**
 * A delegate template that can be overridden
 */
{deltemplate demo.greeting}
  Hello, default!
{/deltemplate}

/**
 * Calls a delegate template
 */
{template .caller}
  {delcall demo.greeting /}
{/template}

Delegates with parameters

Delegate templates can take parameters just like normal templates. You pass data using {param} blocks inside the {delcall}:

1
2
/**
 * A delegate template with a parameter
 * @param name string
 */
{deltemplate demo.greetingWithParam}
  Hello, {$name}!
{/deltemplate}

/**
 * Calls a delegate template with a parameter
 * @param name
 */
{template .callerWithParam}
  {delcall demo.greetingWithParam}
    {param name: $name /}
  {/delcall}
{/template}

allowemptydefault: safely calling optional delegates

Sometimes a delegate key may or may not have an implementation registered. If you call it directly and no implementation exists, Soy will report an error. Use allowemptydefault="true" on {delcall} to turn a missing delegate into an empty string instead:

1
2
/**
 * Demonstrates delcall with allowemptydefault
 * This will return empty string if no delegate is registered
 */
{template .callerWithAllowEmpty}
  Before{delcall demo.maybeEmpty allowemptydefault="true" /}After
{/template}

If no demo.maybeEmpty delegate is available at runtime, the {delcall} produces an empty string, so the output is simply:

1
2
BeforeAfter

Delegates for HTML structure

Delegates are often used for swappable UI components. This example shows a card delegate that renders a small HTML card. Other Soy files could register different demo.card implementations (e.g., for different products or feature flags).

1
2
/**
 * A delegate template that returns HTML structure
 * @param title
 * @param content
 */
{deltemplate demo.card}
  <div class="card">
    <h2>{$title}</h2>
    <p>{$content}</p>
  </div>
{/deltemplate}

/**
 * Calls a card delegate template
 * @param title
 * @param content
 */
{template .cardCaller}
  {delcall demo.card}
    {param title: $title /}
    {param content: $content /}
  {/delcall}
{/template}

Delegate variants (variant="...")

Beyond the "default vs. one override" pattern shown above, a delegate name can host several parallel implementations distinguished by a string-literal variant. The caller picks one by name:

1
2
{namespace demo.delegatesVariant}

/** Default delegate (no variant). */
{deltemplate demo.themed.button}
  <button class="default">click</button>
{/deltemplate}

/** Dark theme variant. */
{deltemplate demo.themed.button variant="'dark'"}
  <button class="dark">click</button>
{/deltemplate}

/** Light theme variant. */
{deltemplate demo.themed.button variant="'light'"}
  <button class="light">click</button>
{/deltemplate}

/** Hard-coded variant call. */
{template .callDark}
  {delcall demo.themed.button variant="'dark'" /}
{/template}

/** Variant chosen at runtime from a parameter. */
/** @param theme */
{template .callDynamic}
  {delcall demo.themed.button variant="$theme" /}
{/template}

/** No variant given → falls back to the default impl. */
{template .callDefault}
  {delcall demo.themed.button /}
{/template}

Three rules to keep in mind:

  • The variant value on {deltemplate} must be a string literal in single quotes. variant="dark" (no inner quotes) and variant="$theme" are rejected on the declaration side. {delcall}, in contrast, accepts an expression — that is how callDynamic above can resolve the variant at runtime.

  • No matching variant + no default = runtime error. Combine variant-only delegates with allowemptydefault="true" on the {delcall} site if you want "render nothing" instead of throwing:

    1
    2
    /** Variant-only delegate (no default impl). */
    {deltemplate demo.themed.iconOnly variant="'star'"}
      *
    {/deltemplate}
    
    /**
     * Unknown variant + allowemptydefault renders as empty string.
     */
    {template .callUnknownAllowEmpty}
      before{delcall demo.themed.iconOnly variant="'nonexistent'" allowemptydefault="true" /}after
    {/template}
    

    callUnknownAllowEmpty produces "beforeafter" rather than throwing.

  • Variants partition the namespace per-name. demo.themed.button with variant="'dark'" and demo.themed.iconOnly with variant="'dark'" are unrelated — the variant string only disambiguates implementations of the same delegate name.

Use variants when a single decision (theme, role, locale flavour) selects one of a small fixed set of bodies. For "one default, optionally overridden by a plugin", stick with the plain unvariant form covered earlier.

When to use delegate templates

Use delegate templates when:

  • Feature owners need extensibility: Different products or plugins can supply their own implementations for shared hooks.
  • You want overridable defaults: Provide a reasonable default UI, but allow overrides without changing the original file.

In this repo we focus on the declaration and calling patterns. Wiring up multiple implementations and delegate priorities is typically handled by the larger build/runtime system and is outside the scope of these examples.

Common Gotchas and Troubleshooting

A quick recap of things that bite people. Each item links back to the section that covers it in detail.

  • Parameters are required by default. Mark optional ones with ?. See Optional parameters.
  • let is immutable and block-scoped. You cannot redeclare $count inside a loop to accumulate — rewrite using index($item) or another loop helper. See Let bindings.
  • Every variable used must be declared. Either via SoyDoc @param or block-style {@param ...}; an unknown reference is a compile error. See Declaring template parameters.
  • Auto-escaping applies in attributes too. <a href="{$url}"> will HTML-escape $url; opt out with |noAutoescape only after validating the value yourself. See Automatic HTML escaping and |noAutoescape.

Language Version and Compatibility

This repo targets an older Atlassian-maintained fork of Soy. Many examples from Google's latest Closure Templates docs either behave differently here or do not compile at all. This section summarises what is and isn't supported so you can read external docs safely.

Parameter declaration styles

This fork supports two parameter declaration styles, and they enforce different things at runtime.

SoyDoc @param (lighter, no runtime checks)

1
2
{namespace demo.compat}

/**
 * @param name
 */
{template .t}
  Hello {$name}!
{/template}

What the compiler does: requires the param to be referenced (otherwise a compile error — see SoyDoc comments), but emits no runtime type assertion for it. A SoyDoc-declared param that is missing at call time renders as the empty string (the print directive coerces undefined to ''); a wrong type goes through unchallenged. The text after the parameter name is descriptive prose, not a type.

Block-form {@param name: type} (runtime-asserted)

1
2
{namespace demo.compatNewParam}

/**
 * Minimal SoyDoc (required by this compiler)
 */
{template .t}
  {@param name: string}
  Hello {$name}!
{/template}

What the compiler does: emits a goog.asserts.assert(goog.isString(opt_data.name) || ...) call at the top of the function. With Closure's goog.asserts available, this means:

  • A missing required {@param} throws at the call site (Assertion failed: expected param 'name' of type string|...).
  • Passing a wrong type (e.g. a string for {@param count: number}) throws the same way.
  • A correctly-typed value passes silently.

The assertion is enforced whenever goog.asserts.assert is wired up to throw — typically in dev/test builds. Production builds may strip these assertions for size, in which case the type annotation becomes documentation only. In this repository's test harness (tools/soy-node-loader.mjs) goog.asserts.assert throws on failure, so block-form @param failures surface as test errors.

A minimal SoyDoc block (/** ... */) is still required above the template even when all parameters are declared in block form.

Which style to prefer

Prefer block-form {@param x: type} for new code — it documents the intended type, catches missing/wrong-type bugs at the call site instead of silently rendering '', and the runtime assertions disappear in release builds where they cost nothing. Reserve SoyDoc @param for templates that must stay backwards compatible with older callers, or where you want to intentionally allow the param to be missing.

Unsupported newer syntax

Some syntax that appears in newer Soy documentation does not compile in this fork.

import statements

Top-level import is not supported:

1
2
{namespace demo.compatImport}
import {foo} from 'path/to/foo.soy';
{template .t}
  Hello
{/template}

Compiling this with the CLI in this repo fails with an error mentioning import.

Element composition (<{foo()}/>)

Element composition syntax is also not supported:

1
2
{namespace demo.compatElementComposition}
{template .foo}
  <div></div>
{/template}

{template .caller}
  <{foo()} />
{/template}

Compilation fails with a SoySyntaxException/TokenMgrError about <{ or "element composition".

Functions that are unavailable or unsafe here

Several functions from newer Soy versions are either missing entirely or not safe to use in this fork:

  • range()not a standalone function. Calling {let $r: range(5) /} fails with Unrecognized function range. The range(...) keyword is valid as the argument to a {for} loop — see Iterating with {for $i in range(...)}.
  • keys() – compiles but the JS runtime helper (soy.$$getMapKeys) is missing; avoid it and iterate known record fields instead.
  • css() – not available; use literal CSS class name strings.
  • {plural} (and related plural helpers) – not available in this JS-only setup; use {if} / {switch} for pluralisation instead.
  • Advanced list methods like join, slice, reverse, concat – not implemented; manual {foreach} loops are required (see "Working with Lists").
  • Method-style string/list helpers (for example $str.indexOf(), $list.join()) – not supported; use the global functions documented in "Built-in Functions" instead (strIndexOf, length, etc.).

When in doubt, prefer the patterns and functions shown in this document over examples in upstream Google docs; everything here is backed by real templates and tests in this repo.

Advanced Gotchas and Platform-Specific Issues

These should all be solved at a Platform-level, but they're still worth documenting for immediate value and in case you work on older product versions.

Immutable variables and scoping

In Soy, variables are truly immutable. Once defined with let, you cannot change them:

Only one of my templates is present

This happens because the JS is throwing an error after defining the first template. To fix the issue, add this missing web-resource dependency to your soy template web-resource

1
2
<dependency>com.atlassian.soy.soy-template-plugin:soy-deps</dependency>

Full explanation

The Soy compiler automatically adds the template name as a property when debugging, but it checks goog.DEBUG and because the goog global object doesn’t exist, it errors before the rest of the templates are initialised. The compiled JS looks like:

1
2
Confluence.Templates.Feature.example = function(opt_data, opt_ignored) {
  return '<div>hi, I\'m the first example template</div>';
};
if (goog.DEBUG) { // The error happens here
  Confluence.Templates.Feature.example.soyTemplateName = 'Confluence.Templates.Feature.example';
}

Confluence.Templates.Feature.exampleTwo = function(opt_data, opt_ignored) {
  return '<div>hi I\'m the other example template</div>';
};
if (goog.DEBUG) {
  Confluence.Templates.Feature.exampleTwo.soyTemplateName = 'Confluence.Templates.Feature.exampleTwo';
}

Array access can only be 1 level deep

You can declare nested arrays but Soy won’t let you access them directly. To work around this, give the accessed object to another template

1
2
{template .example}
    {let $inputs: [[['one', 'two'], 'other string', false], ] /}
    {foreach $input in $inputs}
        {call .second}
            {param array: $input[0] /}
        {/call}
    {/foreach}
{/template}

/**
* A hack to work around a lack of 2D array accessing in Soy
* @param array
*/
{template .second private="true"}
    {$array[1]}'
{/template}

Rate this page: