all: add `nitish` tag for code-blocks skipped by nitunit
[nit.git] / lib / template / macro.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
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
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
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.
14
15 # String templating using macros.
16 #
17 # There is plenty of macro/templating tools in the worl,
18 # yet another one.
19 #
20 # See `TemplateString` for more details.
21 module macro
22
23 import template
24
25 # Template with macros replacement.
26 #
27 # `TemplateString` provides a simple way to customize generic string templates
28 # using macros and replacement.
29 #
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:
32 #
33 # var tpl = new TemplateString("Hello %NAME%!")
34 # tpl.replace("NAME", "Dave")
35 # assert tpl.write_to_string == "Hello Dave!"
36 #
37 # A macro identifier is valid if:
38 #
39 # * starts with an uppercase letter
40 # * contains only numers, uppercase letters or '_'
41 #
42 # See `String::is_valid_macro_name` for more details.
43 #
44 # ## External template files
45 #
46 # When using large template files it's recommanded to use external template files.
47 #
48 # In external file `example.tpl`:
49 #
50 # ~~~html
51 # <!DOCTYPE html>
52 # <html lang="en">
53 # <head>
54 # <title>%TITLE%</title>
55 # </head>
56 # <body>
57 # <h1>%TITLE%</h1>
58 # <p>%ARTICLE%</p>
59 # </body>
60 # </html>
61 # ~~~
62 #
63 # Loading the template file using `TemplateString`:
64 #
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!")
70 # end
71 #
72 # ## Outputting
73 #
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`
76 # or `write_to_file`.
77 #
78 # tpl = new TemplateString("Hello %NAME%!")
79 # tpl.replace("NAME", "Dave")
80 # assert tpl.write_to_string == "Hello Dave!"
81 #
82 # ## Template correctness
83 #
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.
86 #
87 # tpl = new TemplateString("Hello %NAME%!")
88 # assert tpl.write_to_string == "Hello %NAME%!"
89 #
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.
93 #
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")
99 # exit(0)
100 # else
101 # tpl.write_to sys.stdout
102 # end
103 # assert tpl.write_to_string == "Hello %NAME%!"
104 class TemplateString
105 super Template
106
107 # Template original text.
108 private var tpl_text: String
109
110 # Macros contained in the template file.
111 private var macros = new HashMap[String, Array[TemplateMacro]]
112
113 # Macro identifier delimiter char (`'%'` by default).
114 #
115 # To use a different delimiter you can subclasse `TemplateString` and defined the `marker`.
116 #
117 # class DollarTemplate
118 # super TemplateString
119 # redef var marker = '$'
120 # end
121 # var tpl = new DollarTemplate("Hello $NAME$!")
122 # tpl.replace("NAME", "Dave")
123 # assert tpl.write_to_string == "Hello Dave!"
124 protected var marker = '%'
125
126 # Creates a new template from a `text`.
127 #
128 # var tpl = new TemplateString("Hello %NAME%!")
129 # assert tpl.write_to_string == "Hello %NAME%!"
130 init(text: String) do
131 self.tpl_text = text
132 parse
133 end
134
135 # Creates a new template from the contents of `file`.
136 init from_file(file: String) do
137 init load_template_file(file)
138 end
139
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
144 file.close
145 return text
146 end
147
148 # Finds all the macros contained in `text` and store them in `macros`.
149 #
150 # Also build `self` template parts using original template text.
151 private fun parse do
152 var text = tpl_text
153 var chars = text.chars
154 var pos = 0
155 var out = new FlatBuffer
156 var start_pos: Int
157 var end_pos: Int
158 while pos < text.length do
159 # lookup opening tag
160 start_pos = text.read_until_char(pos, marker, out)
161 if start_pos < 0 then
162 text.read_until_pos(pos, text.length, out)
163 add out.to_s
164 break
165 end
166 add out.to_s
167 pos = start_pos + 1
168 # lookup closing tag
169 out.clear
170 end_pos = text.read_until_char(pos, marker, out)
171 if end_pos < 0 then
172 text.read_until_pos(pos, text.length, out)
173 add "%"
174 add out.to_s
175 break
176 end
177 pos = end_pos + 1
178 # check macro
179 var name = out.to_s
180 if name.is_valid_macro_name then
181 add make_macro(name, start_pos, end_pos)
182 else
183 add "%"
184 add name
185 add "%"
186 end
187 out.clear
188 end
189 end
190
191 # Add a new macro to the list
192 private fun make_macro(name: String, start_pos, end_pos: Int): TemplateMacro do
193 if not macros.has_key(name) then
194 macros[name] = new Array[TemplateMacro]
195 end
196 var macro = new TemplateMacro(name, start_pos, end_pos)
197 macros[name].add macro
198 return macro
199 end
200
201 # Available macros in `self`.
202 #
203 # var tpl = new TemplateString("Hello %NAME%!")
204 # assert tpl.macro_names.first == "NAME"
205 fun macro_names: Collection[String] do return macros.keys
206
207 # Does `self` contain a macro with `name`.
208 #
209 # var tpl = new TemplateString("Hello %NAME%")
210 # assert tpl.has_macro("NAME")
211 fun has_macro(name: String): Bool do return macro_names.has(name)
212
213 # Replace a `macro` by a streamable `replacement`.
214 #
215 # REQUIRE `has_macro(name)`
216 #
217 # var tpl = new TemplateString("Hello %NAME%!")
218 # tpl.replace("NAME", "Dave")
219 # assert tpl.write_to_string == "Hello Dave!"
220 fun replace(name: String, replacement: Streamable) do
221 assert has_macro(name)
222 for macro in macros[name] do
223 macro.replacement = replacement
224 end
225 end
226
227 # Check if all macros were replaced.
228 #
229 # Return false if a macro was not replaced and store message in `warnings`.
230 #
231 # var tpl = new TemplateString("Hello %FIRSTNAME%, %LASTNAME%!")
232 # assert not tpl.check
233 # tpl.replace("FIRSTNAME", "Corben")
234 # tpl.replace("LASTNAME", "Dallas")
235 # assert tpl.check
236 fun check: Bool do
237 warnings.clear
238 var all_ok = true
239 for name, macros in self.macros do
240 for macro in macros do
241 if not macro.is_replaced then
242 all_ok = false
243 warnings.add "No replacement for macro %{macro.name}% at {macro.location}"
244 end
245 end
246 end
247 return all_ok
248 end
249
250 # Last `check` warnings.
251 #
252 # var tpl = new TemplateString("Hello %FIRSTNAME%, %LASTNAME%!")
253 # tpl.check
254 # assert tpl.warnings.length == 2
255 # assert tpl.warnings[0] == "No replacement for macro %FIRSTNAME% at (6:16)"
256 # assert tpl.warnings[1] == "No replacement for macro %LASTNAME% at (19:28)"
257 # tpl.replace("FIRSTNAME", "Corben")
258 # tpl.replace("LASTNAME", "Dallas")
259 # tpl.check
260 # assert tpl.warnings.is_empty
261 var warnings = new Array[String]
262
263 # Returns a view on `self` macros on the form `macro.name`/`macro.replacement`.
264 #
265 # Given that all macros with the same name are all replaced with the same
266 # replacement, this view contains only one entry for each name.
267 #
268 # var tpl = new TemplateString("Hello %FIRSTNAME%, %LASTNAME%!")
269 # for name, rep in tpl do assert rep == null
270 # tpl.replace("FIRSTNAME", "Corben")
271 # tpl.replace("LASTNAME", "Dallas")
272 # for name, rep in tpl do assert rep != null
273 fun iterator: MapIterator[String, nullable Streamable] do
274 return new TemplateStringIterator(self)
275 end
276 end
277
278 # A macro is a special text command that is replaced by other content in a `TemplateString`.
279 private class TemplateMacro
280 super Template
281 # Macro name as found in the template.
282 var name: String
283
284 # Macro starting position in template.
285 var start_pos: Int
286
287 # Macro ending position in template.
288 var end_pos: Int
289
290 # Macro replacement if any.
291 var replacement: nullable Streamable = null
292
293 # Does `self` already have a `replacement`?
294 fun is_replaced: Bool do return replacement != null
295
296 # Render `replacement` or else `name`.
297 redef fun rendering do
298 if is_replaced then
299 add replacement.as(not null)
300 else
301 add "%{name}%"
302 end
303 end
304
305 # Human readable location.
306 fun location: String do return "({start_pos}:{end_pos})"
307 end
308
309 redef class String
310 # Reads `self` from pos `from` to pos `to` and store result in `buffer`.
311 private fun read_until_pos(from, to: Int, buffer: Buffer): Int do
312 if from < 0 or from >= length or
313 to < 0 or to >= length or
314 from >= to then return -1
315 var pos = from
316 while pos < to do
317 buffer.add self[pos]
318 pos += 1
319 end
320 return pos
321 end
322
323 # Reads `self` until `to` is encountered and store result in `buffer`.
324 #
325 # Returns `to` position or `-1` if not found.
326 private fun read_until_char(from: Int, char: Char, buffer: Buffer): Int do
327 if from < 0 or from >= length then return -1
328 var pos = from
329 while pos > -1 and pos < length do
330 var c = self[pos]
331 if c == char then return pos
332 buffer.add c
333 pos += 1
334 end
335 return -1
336 end
337
338 # Is `self` a valid macro identifier?
339 #
340 # A macro identifier is valid if:
341 #
342 # * starts with an uppercase letter
343 # * contains only numers, uppercase letters or '_'
344 #
345 # # valid
346 # assert "NAME".is_valid_macro_name
347 # assert "FIRST_NAME".is_valid_macro_name
348 # assert "BLOCK1".is_valid_macro_name
349 # # invalid
350 # assert not "1BLOCK".is_valid_macro_name
351 # assert not "_BLOCK".is_valid_macro_name
352 # assert not "FIRST NAME".is_valid_macro_name
353 # assert not "name".is_valid_macro_name
354 fun is_valid_macro_name: Bool do
355 if not first.is_upper then return false
356 for c in self do
357 if not c.is_upper and c != '_' and not c.is_digit then return false
358 end
359 return true
360 end
361 end
362
363 private class TemplateStringIterator
364 super MapIterator[String, nullable Streamable]
365
366 private var subject: TemplateString
367 private var key_it: Iterator[String] is noinit
368
369 init do
370 self.key_it = subject.macro_names.iterator
371 end
372
373 redef fun is_ok do return key_it.is_ok
374 redef fun next do key_it.next
375 redef fun key do return key_it.item
376 redef fun item do return subject.macros[key].first.replacement
377 end