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:

<!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%!"

Introduced properties

fun check: Bool

template :: TemplateString :: check

Check if all macros were replaced.
init from_file(file: String)

template :: TemplateString :: from_file

Creates a new template from the contents of file.
fun has_macro(name: String): Bool

template :: TemplateString :: has_macro

Does self contain a macro with name.
fun iterator: MapIterator[String, nullable Writable]

template :: TemplateString :: iterator

Returns a view on self macros on the form macro.name/macro.replacement.
fun macro_names: Collection[String]

template :: TemplateString :: macro_names

Available macros in self.
protected fun marker: Char

template :: TemplateString :: marker

Macro identifier delimiter char ('%' by default).
protected fun marker=(marker: Char)

template :: TemplateString :: marker=

Macro identifier delimiter char ('%' by default).
fun replace(name: String, replacement: Writable)

template :: TemplateString :: replace

Replace a macro by a streamable replacement.
fun tpl_text: String

template :: TemplateString :: tpl_text

Template original text.
protected fun tpl_text=(tpl_text: String)

template :: TemplateString :: tpl_text=

Template original text.
fun warnings: Array[String]

template :: TemplateString :: warnings

Last check warnings.
protected fun warnings=(warnings: Array[String])

template :: TemplateString :: warnings=

Last check warnings.

Redefined properties

redef type SELF: TemplateString

template $ TemplateString :: SELF

Type of this instance, automatically specialized in every class
redef init init

template $ TemplateString :: init

Creates a new template from a text.

All properties

fun !=(other: nullable Object): Bool

core :: Object :: !=

Have self and other different values?
fun ==(other: nullable Object): Bool

core :: Object :: ==

Have self and other the same value?
type CLASS: Class[SELF]

core :: Object :: CLASS

The type of the class of self.
type SELF: Object

core :: Object :: SELF

Type of this instance, automatically specialized in every class
fun add(element: Writable)

template :: Template :: add

Append an element (String, other Template, etc.) at the end of the template.
fun add_all(elements: Collection[Writable])

template :: Template :: add_all

Append a bunch of elements at the end of the template.
fun add_list(elements: Collection[Writable], sep: Writable, last_sep: Writable)

template :: Template :: add_list

Append a bunch of elements at the end of the template with separations.
fun addn(element: Writable)

template :: Template :: addn

Append element and the end of the template then append a "\n".
fun check: Bool

template :: TemplateString :: check

Check if all macros were replaced.
protected fun class_factory(name: String): CLASS

core :: Object :: class_factory

Implementation used by get_class to create the specific class.
fun class_name: String

core :: Object :: class_name

The class name of the object.
fun force_render

template :: Template :: force_render

Call rendering, if not already done
fun freeze

template :: Template :: freeze

Disable further modification: no more add is allowed
init from_file(file: String)

template :: TemplateString :: from_file

Creates a new template from the contents of file.
fun get_class: CLASS

core :: Object :: get_class

The meta-object representing the dynamic type of self.
fun has_macro(name: String): Bool

template :: TemplateString :: has_macro

Does self contain a macro with name.
fun hash: Int

core :: Object :: hash

The hash code of the object.
init init

core :: Object :: init

fun inspect: String

core :: Object :: inspect

Developer readable representation of self.
protected fun inspect_head: String

core :: Object :: inspect_head

Return "CLASSNAME:#OBJECTID".
fun is_frozen: Bool

template :: Template :: is_frozen

Is the template allowing more modification (add)
protected fun is_frozen=(is_frozen: Bool)

template :: Template :: is_frozen=

Is the template allowing more modification (add)
intern fun is_same_instance(other: nullable Object): Bool

core :: Object :: is_same_instance

Return true if self and other are the same instance (i.e. same identity).
fun is_same_serialized(other: nullable Object): Bool

core :: Object :: is_same_serialized

Is self the same as other in a serialization context?
intern fun is_same_type(other: Object): Bool

core :: Object :: is_same_type

Return true if self and other have the same dynamic type.
fun iterator: MapIterator[String, nullable Writable]

template :: TemplateString :: iterator

Returns a view on self macros on the form macro.name/macro.replacement.
fun macro_names: Collection[String]

template :: TemplateString :: macro_names

Available macros in self.
protected fun marker: Char

template :: TemplateString :: marker

Macro identifier delimiter char ('%' by default).
protected fun marker=(marker: Char)

template :: TemplateString :: marker=

Macro identifier delimiter char ('%' by default).
fun new_sub: Template

template :: Template :: new_sub

Return a new basic template that is automatically added in self (using add)
intern fun object_id: Int

core :: Object :: object_id

An internal hash code for the object based on its identity.
fun output

core :: Object :: output

Display self on stdout (debug only).
intern fun output_class_name

core :: Object :: output_class_name

Display class name on stdout (debug only).
protected fun rendering

template :: Template :: rendering

Service used to render the content of the template.
fun replace(name: String, replacement: Writable)

template :: TemplateString :: replace

Replace a macro by a streamable replacement.
fun serialization_hash: Int

core :: Object :: serialization_hash

Hash value use for serialization
intern fun sys: Sys

core :: Object :: sys

Return the global sys object, the only instance of the Sys class.
abstract fun to_jvalue(env: JniEnv): JValue

core :: Object :: to_jvalue

fun to_s: String

core :: Object :: to_s

User readable representation of self.
fun tpl_text: String

template :: TemplateString :: tpl_text

Template original text.
protected fun tpl_text=(tpl_text: String)

template :: TemplateString :: tpl_text=

Template original text.
fun warnings: Array[String]

template :: TemplateString :: warnings

Last check warnings.
protected fun warnings=(warnings: Array[String])

template :: TemplateString :: warnings=

Last check warnings.
abstract fun write_to(stream: Writer)

core :: Writable :: write_to

Write itself to a stream
fun write_to_bytes: Bytes

core :: Writable :: write_to_bytes

Like write_to but return a new Bytes (may be quite large)
fun write_to_file(filepath: String)

core :: Writable :: write_to_file

Like write_to but take care of creating the file
fun write_to_string: String

core :: Writable :: write_to_string

Like write_to but return a new String (may be quite large).
package_diagram template::TemplateString TemplateString template::Template Template template::TemplateString->template::Template core::Writable Writable template::Template->core::Writable ...core::Writable ... ...core::Writable->core::Writable

Ancestors

interface Object

core :: Object

The root of the class hierarchy.
interface Writable

core :: Writable

Things that can be efficienlty written to a Writer

Parents

class Template

template :: Template

Templates are simple hierarchical pieces of text used for efficient stream writing.

Class definitions

template $ TemplateString
# 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