Property definitions

template $ TemplateString :: defaultinit
# Template with macros replacement.
#
# `TemplateString` provides a simple way to customize generic string templates
# using macros and replacement.
#
# A macro is represented as a string identifier like `%MACRO%` in the template
# string. Using `TemplateString`, macros can be replaced by any `Writable` data:
#
#     var tpl = new TemplateString("Hello %NAME%!")
#     tpl.replace("NAME", "Dave")
#     assert tpl.write_to_string == "Hello Dave!"
#
# A macro identifier is valid if:
#
# * starts with an uppercase letter
# * contains only numbers, uppercase letters or '_'
#
# See `String::is_valid_macro_name` for more details.
#
# ## External template files
#
# When using large template files it's recommanded to use external template files.
#
# In external file `example.tpl`:
#
# ~~~html
# <!DOCTYPE html>
# <html lang="en">
#  <head>
#   <title>%TITLE%</title>
#  </head>
#  <body>
#   <h1>%TITLE%</h1>
#   <p>%ARTICLE%</p>
#  </body>
# </html>
# ~~~
#
# Loading the template file using `TemplateString`:
#
#     var file = "example.tpl"
#     if file.file_exists then
#         tpl = new TemplateString.from_file("example.tpl")
#         tpl.replace("TITLE", "Home Page")
#         tpl.replace("ARTICLE", "Welcome on my site!")
#     end
#
# ## Outputting
#
# Once macro replacement has been made, the `TemplateString` can be
# output like any other `Template` using methods like `write_to`, `write_to_string`
# or `write_to_file`.
#
#     tpl = new TemplateString("Hello %NAME%!")
#     tpl.replace("NAME", "Dave")
#     assert tpl.write_to_string == "Hello Dave!"
#
# ## Template correctness
#
# `TemplateString` can be outputed even if all macros were not replaced.
# In this case, the name of the macro will be displayed wuthout any replacement.
#
#     tpl = new TemplateString("Hello %NAME%!")
#     assert tpl.write_to_string == "Hello %NAME%!"
#
# The `check` method can be used to ensure that all macros were replaced before
# performing the output. Warning messages will be stored in `warnings` and can
# be used to locate unreplaced macros.
#
#     tpl = new TemplateString("Hello %NAME%!")
#     if not tpl.check then
#         assert not tpl.warnings.is_empty
#         print "Cannot output unfinished template:"
#         print tpl.warnings.join("\n")
#         exit(0)
#     else
#         tpl.write_to sys.stdout
#     end
#     assert tpl.write_to_string == "Hello %NAME%!"
class TemplateString
	super Template

	# Template original text.
	var tpl_text: String

	# Macros contained in the template file.
	private var macros = new HashMap[String, Array[TemplateMacro]]

	# Macro identifier delimiter char (`'%'` by default).
	#
	# To use a different delimiter you can subclasse `TemplateString` and defined the `marker`.
	#
	#     class DollarTemplate
	#         super TemplateString
	#         redef var marker = '$'
	#     end
	#     var tpl = new DollarTemplate("Hello $NAME$!")
	#     tpl.replace("NAME", "Dave")
	#     assert tpl.write_to_string == "Hello Dave!"
	protected var marker = '%'

	# Creates a new template from a `text`.
	#
	#     var tpl = new TemplateString("Hello %NAME%!")
	#     assert tpl.write_to_string == "Hello %NAME%!"
	init do
		parse
	end

	# Creates a new template from the contents of `file`.
	init from_file(file: String) do
		init load_template_file(file)
	end

	# Loads the template file contents.
	private fun load_template_file(tpl_file: String): String do
		var file = new FileReader.open(tpl_file)
		var text = file.read_all
		file.close
		return text
	end

	# Finds all the macros contained in `text` and store them in `macros`.
	#
	# Also build `self` template parts using original template text.
	private fun parse do
		var text = tpl_text
		var pos = 0
		var out = new FlatBuffer
		var start_pos: Int
		var end_pos: Int
		while pos < text.length do
			# lookup opening tag
			start_pos = text.read_until_char(pos, marker, out)
			if start_pos < 0 then
				text.read_until_pos(pos, text.length, out)
				add out.to_s
				break
			end
			add out.to_s
			pos = start_pos + 1
			# lookup closing tag
			out.clear
			end_pos = text.read_until_char(pos, marker, out)
			if end_pos < 0 then
				text.read_until_pos(pos, text.length, out)
				add "%"
				add out.to_s
				break
			end
			pos = end_pos + 1
			# check macro
			var name = out.to_s
			if name.is_valid_macro_name then
				add make_macro(name, start_pos, end_pos)
			else
				add "%"
				add name
				add "%"
			end
			out.clear
		end
	end

	# Add a new macro to the list
	private fun make_macro(name: String, start_pos, end_pos: Int): TemplateMacro do
		if not macros.has_key(name) then
			macros[name] = new Array[TemplateMacro]
		end
		var macro = new TemplateMacro(name, start_pos, end_pos)
		macros[name].add macro
		return macro
	end

	# Available macros in `self`.
	#
	#     var tpl = new TemplateString("Hello %NAME%!")
	#     assert tpl.macro_names.first == "NAME"
	fun macro_names: Collection[String] do return macros.keys

	# Does `self` contain a macro with `name`.
	#
	#     var tpl = new TemplateString("Hello %NAME%")
	#     assert tpl.has_macro("NAME")
	fun has_macro(name: String): Bool do return macro_names.has(name)

	# Replace a `macro` by a streamable `replacement`.
	#
	# REQUIRE `has_macro(name)`
	#
	#     var tpl = new TemplateString("Hello %NAME%!")
	#     tpl.replace("NAME", "Dave")
	#     assert tpl.write_to_string == "Hello Dave!"
	fun replace(name: String, replacement: Writable) do
		assert has_macro(name)
		for macro in macros[name] do
			macro.replacement = replacement
		end
	end

	# Check if all macros were replaced.
	#
	# Return false if a macro was not replaced and store message in `warnings`.
	#
	#     var tpl = new TemplateString("Hello %FIRSTNAME%, %LASTNAME%!")
	#     assert not tpl.check
	#     tpl.replace("FIRSTNAME", "Corben")
	#     tpl.replace("LASTNAME", "Dallas")
	#     assert tpl.check
	fun check: Bool do
		warnings.clear
		var all_ok = true
		for name, macros in self.macros do
			for macro in macros do
				if not macro.is_replaced then
					all_ok = false
					warnings.add "No replacement for macro %{macro.name}% at {macro.location}"
				end
			end
		end
		return all_ok
	end

	# Last `check` warnings.
	#
	#     var tpl = new TemplateString("Hello %FIRSTNAME%, %LASTNAME%!")
	#     tpl.check
	#     assert tpl.warnings.length == 2
	#     assert tpl.warnings[0] == "No replacement for macro %FIRSTNAME% at (6:16)"
	#     assert tpl.warnings[1] == "No replacement for macro %LASTNAME% at (19:28)"
	#     tpl.replace("FIRSTNAME", "Corben")
	#     tpl.replace("LASTNAME", "Dallas")
	#     tpl.check
	#     assert tpl.warnings.is_empty
	var warnings = new Array[String]

	# Returns a view on `self` macros on the form `macro.name`/`macro.replacement`.
	#
	# Given that all macros with the same name are all replaced with the same
	# replacement, this view contains only one entry for each name.
	#
	#     var tpl = new TemplateString("Hello %FIRSTNAME%, %LASTNAME%!")
	#     for name, rep in tpl do assert rep == null
	#     tpl.replace("FIRSTNAME", "Corben")
	#     tpl.replace("LASTNAME", "Dallas")
	#     for name, rep in tpl do assert rep != null
	fun iterator: MapIterator[String, nullable Writable] do
		return new TemplateStringIterator(self)
	end
end
lib/template/macro.nit:25,1--274,3