Add the output_eol setting, and the \e escape sequence of string literals - #133
Add the output_eol setting, and the \e escape sequence of string literals#133gdisirio wants to merge 1 commit into
Conversation
The setting was introduced here first, with a non-null default of "\n" and
setOutputEol(null) rejected. Upstream can't take it that way: prescribing a
line ending by default would change what existing classic templates output,
as their static text would get normalized. So there it defaults to not being
set, and only then does normalization happen.
This adopts that. output_eol is now nullable and unset by default, "JVM
default" and "null" are accepted by setSetting like they are for the other
nullable string settings, and getEffectiveOutputEol() gives the value that
the \e escape and the text blocks resolve to ("\n" when unset).
Nothing observable changes for code-first templates. A .ftlc file has no
static text at all -- every character of output comes from an emit -- so the
one thing that depends on whether the setting is set can't arise there. The
\e escape and the text blocks now go through getEffectiveOutputEol(), which
gives "\n" when unset, which is what the old default gave. Verified: the
output of a .ftlc exercising \e, \n, text blocks and hex is byte-identical
with the setting unset and with it set to "\n".
What is new here is that classic .ftl templates now normalize their static
text when the setting is set, via TextBlock, which is where upstream does it.
Wrapping the Writer instead would have to inspect every character of all
output, and couldn't tell a line feed that came from \n from one that came
from \e, which would break that distinction.
The README is updated: it claimed the default was "\n", and that the
template file's line endings "never leak through", which was only ever true
of text blocks.
|
I think it's enough to add this on
|
|
(I meant that I have already fixed it in the last comment. I updated that comment, but not sure how notifications go out.) |
…rals.
Currently there's no way to tell FreeMarker what line breaks the output should use. The line
breaks of the static text of the template are written out as they are, so whether the output
has LF or CRLF line breaks depends on what the editor and operating system of whoever last
saved the template file happened to use. That's invisible in the template source, survives
being copied between machines, and for generated files that are stored in a version control
system it shows up as line ending churn in the diffs.
The new output_eol setting prescribes the line break to use. When it's set, the line breaks of
the static text are all replaced with it. It also gives the value of the new \e escape sequence
of string literals, so that a line break can be written where the output format needs one,
without hard-wiring which characters that is. Unlike \n, which always gives a line feed, \e
adapts. Both work in any template; nothing here is specific to any output format.
Values inserted by ${...} are never affected, as this setting is about the template, not about
the data. Similarly, \n keeps giving a line feed even when output_eol is set; that distinction
is the point of having both.
It's a ParserConfiguration setting, so it's applied when the template is parsed. That means it
can't be changed with the setting directive from inside a template, but it can differ per
template through TemplateConfiguration, like the other parser settings. It also means the
runtime pays nothing for it: the static text is normalized once, when parsed, and \e is decoded
to the line break there too, so no class involved in executing a template is touched. TextBlock,
StringLiteral, Environment and Configurable are unchanged.
Backward compatible: the setting defaults to not being set, and then everything behaves exactly
as before. \e was a parse error until now (the lexer rejects unknown escapes), so no currently
valid template can be affected by that either.
3101757 to
d06a598
Compare
|
Drafted by Claude (an AI coding assistant) working on this branch with me. The decision to take your You're right, and it's a better design than mine — thanks. Reworked and force-pushed. Making it a The result is that no class involved in executing a template is touched at all. What this changes for users
AlsoThanks for fixing
|
|
The setting name For some people, |
|
Or maybe slightly better |
|
Drafted by Claude (an AI coding assistant) working on this branch with me; the choices below were discussed with me and are mine. Agreed on both counts, and On the escape: you're right that
FTL has no uppercase escapes at all today, so this would introduce case-significance at exactly the spot where the meanings are easiest to confuse. What about Happy to go with |
|
True, Escape: FTL is case-sensitivem, so if I see BTW, officially, these should be decided on the dev@ mailing list. I will point to there over there, just in case someone care and yet doesn't notice these comments. |
Drafted by Claude (an AI coding assistant) working on this branch with me. The design decisions below — the three-state semantics with "unset" preserving the current behavior, keeping
\edeterministic rather than platform-dependent by default, and normalizing inTextBlockrather than by wrapping theWriter— were discussed with me and are mine; the implementation and this write-up are its work.Backward compatibility first
Nothing changes unless you ask for it. The new
output_eolsetting defaults to not being set, and in that state FreeMarker behaves exactly as it does today: the static text of the template is written out with whatever line breaks the template file happens to contain. There is noincompatible_improvementsgating needed, because there is no behavior to gate — the default is the old behavior.The
\eescape is additive in the same way hex literals were:"\e"is currently a parse error (the lexer rejects unknown escapes in string literals), so no currently valid template can change meaning.What it's for
There's currently no way to tell FreeMarker what line breaks the output should use. Whether the output ends up with LF or CRLF depends on the editor and operating system of whoever last saved the template file. That's invisible in the template source, it survives copying between machines, and when the generated files are stored in a version control system it shows up as line ending churn in diffs.
Setting
output_eolprescribes the line break, and then it doesn't matter what the template file itself uses.Semantics
output_eol\egives"\n"/"\r\n"/"\r""JVM default"Two things are deliberately not affected:
${...}. The setting is about the template, not about the data. A string in the data model that contains CRLF is written out unchanged.\n. It keeps giving a line feed even whenoutput_eolis set. Having both only makes sense if they differ:\nwhen you need that exact character,\ewhen you need "whatever the output should use"."JVM default"follows the conventionlocale,time_zoneanddefault_encodingalready use. I'd suggest treating it as a documented escape hatch rather than something to reach for: it makes the output depend on the machine that produced it, which is the opposite of what you want if the output is committed to a repository. The manual entry says as much.On the implementation
The static text is normalized in
TextBlock, not by wrapping the outputWriter. Wrapping theWriterwas the obvious approach and it's wrong twice over:TextBlockcurrently storeschar[]rather thanStringspecifically to avoid one array copy per write — adding a per-character scan on that path would undo considerably more than that comment was protecting.\nfrom\e. Both are ordinary line feeds by the time output reaches theWriter, so normalizing there would silently rewrite deliberate\ns and destroy the distinction above.Instead
TextBlockcaches the normalized text, keyed on the setting value it was computed for; that setting practically never changes while a template is executed, so it's computed once per node and reused. When the setting is unset, the original array is written directly, so the existing path is untouched. The cache is a single immutable holder object rather than two fields, so that a thread which doesn't see another thread's write just recomputes, and can never observe a half-initialized state.The
\eescape reuses the existing placeholder mechanism: it's decoded to a placeholder character when the template is parsed, and resolved when the string literal is evaluated, sinceoutput_eolis aConfigurableand so its value is only known at that point.Testing & docs
OutputEolTest: 27 tests. The static text ones are parameterized over LF/CRLF/CR template files × the four setting states. The rest cover\e, that\nand interpolated values are unaffected, that unknown escapes are still rejected,<#setting>in both naming conventions,setSettingincluding"JVM default"and"null", and inheritance from theConfiguration.settingdirective's list of settings, and a 2.3.36 version history section../gradlew checkand./gradlew manualOfflinepass.One thing you'll want to know about, unrelated to this PR
DefaultObjectWrapperTest.testIncompatibleImprovementsVersionBreakPointscurrently fails on2.3-gae, and it fails the same way with this branch's changes stashed, so it isn't caused by this PR. It iterates versions up toConfiguration.getVersion()while itsexpectedlist stops at 2.3.35, so bumping the version to 2.3.36-SNAPSHOT inbea5c3577left it one entry short. I left it alone rather than mixing an unrelated fix into this branch, but it will fail for you too until someone adds the entry.