Skip to content

Version 4 Release Notes

Textwire 4 is a major release focusing on better error handling and API consistency. Most changes are internal and won't affect your existing code, but there are a few things you'll want to know.

NOTE

This is not an upgrade guide, it covers all the changes in version 4 and explains why they were made. If you are transitioning from Textwire v3, you can follow this Upgrade Guide for all the instructions. v4 contains several breaking changes, so review it carefully.

1. New Error Return Type

Previously, getting detailed error information from Textwire was limited to the String() method on *Template. While Go's standard errors only contain a message, the *fail.Error type gives you rich context to debug issues faster. With *fail.Error, you can access:

  • File path with Filepath() method
  • Error position with Pos() method
  • Error message with Message() method
  • Convert to standard error with Error() method

This is especially useful if you're building LSP servers, developer tools, or libraries that need precise error reporting.

go
tpl, err := textwire.NewTemplate(nil)
if err != nil {
    fmt.Printf(
        "Error in %s at line %d, col %d: %s\n",
        err.Filepath(),
        err.Pos().StartLine,
        err.Pos().StartCol,
    )
}

See the Upgrade Guide for details.

2. Postfix is an Expression or Statement?

Increment and decrement operators often trip up developers, not because they're complex, but because their behavior isn't always obvious. Let's look at why.

Understanding the Postfix Problem

Consider how JavaScript handles the postfix increment:

javascript
let i = 0
console.log(i++)

What will this output? Here's what happens:

  1. i is set to 0
  2. The current value of i (0) gets printed
  3. Then i increments to 1

This is true for many programming languages like PHP, C, C++, C#, and Java. It's not specific to JavaScript. Remember that JavaScript was built on the foundation of C, so it inherited a lot of its syntax and behavior from C. The postfix increment operator is one of those features that has been around for a long time and is widely used.

If we unwrap the code example above and make it clearer, we get:

javascript
let i = 0
console.log(i)
i = i + 1

In other words, increment/decrement is a shortcut for:

  1. Return the current value of i
  2. Then increment i by 1

In Textwire prior to v4, the postfix increment and decrement operators were just shortcuts to i + 1 since I didn't want to make it confusing. Unfortunately, this led to some confusion. In Textwire 4 we follow Go's pattern and treat increment and decrement as statements, not expressions.

Go's Approach to Increment and Decrement

Unlike other programming languages, Go decided to go a different route. Instead of confusing developers with post-increment, Go treats increment and decrement as statements, not expressions. This means you can't use them where an expression is expected, such as in function calls or as part of larger expressions. In Go's view, n++ is the same as n = n + 1, and it doesn't return a value.

I decided to do the same for Textwire to avoid confusion and make increment and decrement behavior consistent with Go. The good thing is, your for loops will work the same as before. You only need to check places where you expect a value to be returned from an increment/decrement operation.

3. Control Directives Case Change

@continueIf and @breakIf directives should now be written lowercase as @continueif and @breakif to match other directives like @slotif and @elseif.

textwire
@each(user in users)
    @continueIf(user.id == 1)
    @continueif(user.id == 1)
    <li>{{ user.name }}</li>
@end

4. Improved String Encoding (Security Fix)

In v4, single and double quotes are now HTML-encoded. Previously, only <> & were encoded, leaving quotes vulnerable to XSS attacks.

Before (v3 output):

textwire
{{ userInput }}
{{-- Input:  Hello "world" --}}
{{-- Output: Hello "world" --}}

After (v4 output):

textwire
{{ userInput }}
{{-- Input:  Hello "world" --}}
{{-- Output: Hello &#34;world&#34; --}}

XSS Risk

If you need unencoded output, use raw() with extreme caution. Never use raw() with user input - only for trusted hardcoded strings.

5. time.Time Auto-Conversion

Passing Go's time.Time to templates now works as expected. Previously, it rendered as an empty object {}.

Before (v3):

textwire
{{ createdAt }} {{-- Renders as: {} --}}

After (v4):

textwire
{{ createdAt }} {{-- Renders as: 1990-12-23 11:22:33 --}}

Dates render in a fixed YYYY-MM-DD HH:MM:SS format. If you need custom formatting, use the formatDate global function:

textwire
{{ createdAt }} {{-- 1990-12-23 11:22:33 --}}
{{ formatDate(createdAt, "January 2, 2006") }} {{-- December 23, 1990 --}}

6. Component System Improvements

Components are one of Textwire's most powerful features, allowing you to build reusable UI pieces. In v4, we've made two significant changes to make components more flexible and powerful. Unfortunately, Textwire v4 component implementation is very different from previous version. If your project is big and relies heavily on components, you will need to review all your component files and update them according to the new syntax. Please refer to the Upgrade Guide for detailed instructions.

6.1 Simplified Default Content

Previously, when passing content to a component, you had to wrap default (unnamed) content with a @slot directive:

textwire
@component('user-card', { user })
    @slot
        <h2>{{ user.name }}</h2>
    @end
@end

This extra wrapper felt unnecessary. In v4, you can simply pass the content directly:

textwire
@component('user-card', { user })
    <h2>{{ user.name }}</h2>
@end

The syntax is now cleaner and more natural. Content placed directly inside the component automatically goes to the default slot.

6.2 Clearer Terminology

The biggest source of confusion in v3 was the dual meaning of @slot. It was used both to:

  1. Define a placeholder in a component file
  2. Pass content to that placeholder from a parent template

This ambiguity made it hard to understand the relationship between components and their callers. The parser was confused in specific scenarios because it wouldn't know what you are trying to do.

To give you a good example, imagine we define a component file components/book.tw:

textwire
<div class="book">
    @slot('header')

    @component('components/book-image')
        @slot('center')
            @slot
        @end
    @end

    @slot('footer')
</div>

Here we are want to pass default content into the center section of book-image component. The parser would not be able to do that because it cannot know which slot you are referring.

In v4, we've separated these concerns with clear, distinct terminology.

  • @pass('name')<content>@end - sends content (component's block)
  • @slot('name') - receives content (component file)

Think of it like a mailbox system:

  • @slot creates the mailbox (in the component file)
  • @pass delivers the mail (from the parent template)

Component file defines the structure:

textwire
<div class="card">
    @slot('header')  {{-- Mailbox for header content --}}
    @slot            {{-- Default mailbox for main content --}}
    @slot('footer')  {{-- Mailbox for footer content --}}
</div>

Parent template sends the content:

textwire
@component('card')
    @pass('header')
        <h2>Card Title</h2>
    @end
    
    {{-- Default content goes here without @pass --}}
    <p>Main card content</p>
    
    @pass('footer')
        <small>Last updated today</small>
    @end
@end

This separation makes the code more readable and the relationship between components and their content clearer. But the main benefit is that the parser can now correctly understand your intentions and won't get confused in complex scenarios.

When you see @pass, you know content is being sent. When you see @slot, you know it's a placeholder waiting to receive content.

6.3 Closing Component is Required

In Textwire v4, you should always close your components even if they don't have any content. This is to avoid confusion and make the syntax more consistent. In previous versions, you could write:

textwire
@component('components/icons/book')

Starting from version 4, you need to write this instead:

textwire
@component('components/icons/book')@end

7. Cleaner Output with Block Trimming

All block directives in v4 now automatically trim surrounding whitespace. This affects:

  • @component
  • @insert
  • @pass, @passif
  • @if, @elseif, @else
  • @each, @for

What this means:

Before, extra newlines and indentation inside blocks would appear in your output:

textwire
@insert('content')
    <h1>Header</h1>
@end

Would pass \n <h1>Header</h1>\n to a matching @reserve('content') in your layout file.

In v4, this content is automatically trimmed to just <h1>Header</h1>, producing cleaner output without extra whitespace.

Why this is safe:

Since Textwire generates HTML, this change won't affect your rendered pages, browsers ignore extra whitespace anyway. However, it makes your source output cleaner and more predictable, especially when working with APIs or when whitespace matters (like in <pre> tags or plain text emails).

Conclusion

These changes make Textwire more consistent with Go's patterns and provide better developer experience through improved error reporting. While there are breaking changes, the upgrade path is straightforward for most use cases.

Last updated: