1 # This file is part of NIT ( http://www.nitlanguage.org ).
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
15 # String templating using macros.
17 # There is plenty of macro/templating tools in the worl,
20 # See `TemplateString` for more details.
25 # Template with macros replacement.
27 # `TemplateString` provides a simple way to customize generic string templates
28 # using macros and replacement.
30 # A macro is represented as a string identifier like `%MACRO%` in the template
31 # string. Using `TemplateString`, macros can be replaced by any `Streamable` data:
33 # var tpl = new TemplateString("Hello %NAME%!")
34 # tpl.replace("NAME", "Dave")
35 # assert tpl.write_to_string == "Hello Dave!"
37 # A macro identifier is valid if:
39 # * starts with an uppercase letter
40 # * contains only numers, uppercase letters or '_'
42 # See `String::is_valid_macro_name` for more details.
44 # ## External template files
46 # When using large template files it's recommanded to use external template files.
48 # In external file `example.tpl`:
54 # <title>%TITLE%</title>
63 # Loading the template file using `TemplateString`:
65 # var file = "example.tpl"
66 # if file.file_exists then
67 # tpl = new TemplateString.from_file("example.tpl")
68 # tpl.replace("TITLE", "Home Page")
69 # tpl.replace("ARTICLE", "Welcome on my site!")
74 # Once macro replacement has been made, the `TemplateString` can be
75 # output like any other `Template` using methods like `write_to`, `write_to_string`
78 # tpl = new TemplateString("Hello %NAME%!")
79 # tpl.replace("NAME", "Dave")
80 # assert tpl.write_to_string == "Hello Dave!"
82 # ## Template correctness
84 # `TemplateString` can be outputed even if all macros were not replaced.
85 # In this case, the name of the macro will be displayed wuthout any replacement.
87 # tpl = new TemplateString("Hello %NAME%!")
88 # assert tpl.write_to_string == "Hello %NAME%!"
90 # The `check` method can be used to ensure that all macros were replaced before
91 # performing the output. Warning messages will be stored in `warnings` and can
92 # be used to locate unreplaced macros.
94 # tpl = new TemplateString("Hello %NAME%!")
95 # if not tpl.check then
96 # assert not tpl.warnings.is_empty
97 # print "Cannot output unfinished template:"
98 # print tpl.warnings.join("\n")
101 # tpl.write_to sys.stdout
103 # assert tpl.write_to_string == "Hello %NAME%!"
107 # Template original text.
108 private var tpl_text
: String
110 # Macros contained in the template file.
111 private var macros
= new HashMap[String, Array[TemplateMacro]]
113 # Macro identifier delimiter char (`'%'` by default).
115 # To use a different delimiter you can subclasse `TemplateString` and defined the `marker`.
117 # class DollarTemplate
118 # super TemplateString
119 # redef var marker = '$'
121 # var tpl = new DollarTemplate("Hello $NAME$!")
122 # tpl.replace("NAME", "Dave")
123 # assert tpl.write_to_string == "Hello Dave!"
124 protected var marker
= '%'
126 # Creates a new template from a `text`.
128 # var tpl = new TemplateString("Hello %NAME%!")
129 # assert tpl.write_to_string == "Hello %NAME%!"
130 init(text
: String) do
135 # Creates a new template from the contents of `file`.
136 init from_file
(file
: String) do
137 init load_template_file
(file
)
140 # Loads the template file contents.
141 private fun load_template_file
(tpl_file
: String): String do
142 var file
= new IFStream.open
(tpl_file
)
143 var text
= file
.read_all
148 # Finds all the macros contained in `text` and store them in `macros`.
150 # Also build `self` template parts using original template text.
154 var out
= new FlatBuffer
157 while pos
< text
.length
do
159 start_pos
= text
.read_until_char
(pos
, marker
, out
)
160 if start_pos
< 0 then
161 text
.read_until_pos
(pos
, text
.length
, out
)
169 end_pos
= text
.read_until_char
(pos
, marker
, out
)
171 text
.read_until_pos
(pos
, text
.length
, out
)
179 if name
.is_valid_macro_name
then
180 add make_macro
(name
, start_pos
, end_pos
)
190 # Add a new macro to the list
191 private fun make_macro
(name
: String, start_pos
, end_pos
: Int): TemplateMacro do
192 if not macros
.has_key
(name
) then
193 macros
[name
] = new Array[TemplateMacro]
195 var macro
= new TemplateMacro(name
, start_pos
, end_pos
)
196 macros
[name
].add macro
200 # Available macros in `self`.
202 # var tpl = new TemplateString("Hello %NAME%!")
203 # assert tpl.macro_names.first == "NAME"
204 fun macro_names
: Collection[String] do return macros
.keys
206 # Does `self` contain a macro with `name`.
208 # var tpl = new TemplateString("Hello %NAME%")
209 # assert tpl.has_macro("NAME")
210 fun has_macro
(name
: String): Bool do return macro_names
.has
(name
)
212 # Replace a `macro` by a streamable `replacement`.
214 # REQUIRE `has_macro(name)`
216 # var tpl = new TemplateString("Hello %NAME%!")
217 # tpl.replace("NAME", "Dave")
218 # assert tpl.write_to_string == "Hello Dave!"
219 fun replace
(name
: String, replacement
: Streamable) do
220 assert has_macro
(name
)
221 for macro
in macros
[name
] do
222 macro
.replacement
= replacement
226 # Check if all macros were replaced.
228 # Return false if a macro was not replaced and store message in `warnings`.
230 # var tpl = new TemplateString("Hello %FIRSTNAME%, %LASTNAME%!")
231 # assert not tpl.check
232 # tpl.replace("FIRSTNAME", "Corben")
233 # tpl.replace("LASTNAME", "Dallas")
238 for name
, macros
in self.macros
do
239 for macro
in macros
do
240 if not macro
.is_replaced
then
242 warnings
.add
"No replacement for macro %{macro.name}% at {macro.location}"
249 # Last `check` warnings.
251 # var tpl = new TemplateString("Hello %FIRSTNAME%, %LASTNAME%!")
253 # assert tpl.warnings.length == 2
254 # assert tpl.warnings[0] == "No replacement for macro %FIRSTNAME% at (6:16)"
255 # assert tpl.warnings[1] == "No replacement for macro %LASTNAME% at (19:28)"
256 # tpl.replace("FIRSTNAME", "Corben")
257 # tpl.replace("LASTNAME", "Dallas")
259 # assert tpl.warnings.is_empty
260 var warnings
= new Array[String]
262 # Returns a view on `self` macros on the form `macro.name`/`macro.replacement`.
264 # Given that all macros with the same name are all replaced with the same
265 # replacement, this view contains only one entry for each name.
267 # var tpl = new TemplateString("Hello %FIRSTNAME%, %LASTNAME%!")
268 # for name, rep in tpl do assert rep == null
269 # tpl.replace("FIRSTNAME", "Corben")
270 # tpl.replace("LASTNAME", "Dallas")
271 # for name, rep in tpl do assert rep != null
272 fun iterator
: MapIterator[String, nullable Streamable] do
273 return new TemplateStringIterator(self)
277 # A macro is a special text command that is replaced by other content in a `TemplateString`.
278 private class TemplateMacro
280 # Macro name as found in the template.
283 # Macro starting position in template.
286 # Macro ending position in template.
289 # Macro replacement if any.
290 var replacement
: nullable Streamable = null
292 # Does `self` already have a `replacement`?
293 fun is_replaced
: Bool do return replacement
!= null
295 # Render `replacement` or else `name`.
296 redef fun rendering
do
298 add replacement
.as(not null)
304 # Human readable location.
305 fun location
: String do return "({start_pos}:{end_pos})"
309 # Reads `self` from pos `from` to pos `to` and store result in `buffer`.
310 private fun read_until_pos
(from
, to
: Int, buffer
: Buffer): Int do
311 if from
< 0 or from
>= length
or
312 to
< 0 or to
>= length
or
313 from
>= to
then return -1
322 # Reads `self` until `to` is encountered and store result in `buffer`.
324 # Returns `to` position or `-1` if not found.
325 private fun read_until_char
(from
: Int, char
: Char, buffer
: Buffer): Int do
326 if from
< 0 or from
>= length
then return -1
328 while pos
> -1 and pos
< length
do
330 if c
== char
then return pos
337 # Is `self` a valid macro identifier?
339 # A macro identifier is valid if:
341 # * starts with an uppercase letter
342 # * contains only numers, uppercase letters or '_'
345 # assert "NAME".is_valid_macro_name
346 # assert "FIRST_NAME".is_valid_macro_name
347 # assert "BLOCK1".is_valid_macro_name
349 # assert not "1BLOCK".is_valid_macro_name
350 # assert not "_BLOCK".is_valid_macro_name
351 # assert not "FIRST NAME".is_valid_macro_name
352 # assert not "name".is_valid_macro_name
353 fun is_valid_macro_name
: Bool do
354 if not first
.is_upper
then return false
356 if not c
.is_upper
and c
!= '_' and not c
.is_digit
then return false
362 private class TemplateStringIterator
363 super MapIterator[String, nullable Streamable]
365 var subject
: TemplateString
366 var key_it
: Iterator[String] is noinit
369 self.key_it
= subject
.macro_names
.iterator
372 redef fun is_ok
do return key_it
.is_ok
373 redef fun next
do key_it
.next
374 redef fun key
do return key_it
.item
375 redef fun item
do return subject
.macros
[key
].first
.replacement