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.
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:
let i = 0
console.log(i++)What will this output? Here's what happens:
iis set to0- The current value of
i(0) gets printed - Then
iincrements to1
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:
let i = 0
console.log(i)
i = i + 1In other words, increment/decrement is a shortcut for:
- Return the current value of
i - Then increment
iby 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.
@each(user in users)
@continueIf(user.id == 1)
@continueif(user.id == 1)
<li>{{ user.name }}</li>
@end4. 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):
{{ userInput }}
{{-- Input: Hello "world" --}}
{{-- Output: Hello "world" --}}After (v4 output):
{{ userInput }}
{{-- Input: Hello "world" --}}
{{-- Output: Hello "world" --}}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):
{{ createdAt }} {{-- Renders as: {} --}}After (v4):
{{ 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:
{{ 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:
@component('user-card', { user })
@slot
<h2>{{ user.name }}</h2>
@end
@endThis extra wrapper felt unnecessary. In v4, you can simply pass the content directly:
@component('user-card', { user })
<h2>{{ user.name }}</h2>
@endThe 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:
- Define a placeholder in a component file
- 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:
<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:
@slotcreates the mailbox (in the component file)@passdelivers the mail (from the parent template)
Component file defines the structure:
<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:
@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
@endThis 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:
@component('components/icons/book')Starting from version 4, you need to write this instead:
@component('components/icons/book')@end7. 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:
@insert('content')
<h1>Header</h1>
@endWould 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.
