Jamal Macro Language
Jamal is a complex text processor with a wide variety of possible use. The first version of Jamal was developed 20 years ago in Perl. It was used in many projects to simplify build-scripts maintenance, content text, and many other things. The old Perl version is still in use for the build chain maintenance of the ScriptBasic scripting language, which is used in many industrial products. This version of Jamal is a complete rewrite of the original processor in Java. Jamal leverages on the programming language’s features, and the twenty years of experience of using the old version. In this documentation, the term "Jamal" refers to the Java implementation of the macro language.
Jamal’s basic concept is to transform a source text to a target text enabling programmatic constructs in the source text. That way, it enables the maintainer of the text to
-
eliminate repetitions, calculable text, and other redundancies
-
ensure internal consistency, and
-
support consistency with the documented external system(s).
Jamal design is to be transparent and applicable to any target text file and format. It uses special start and stop strings for the macros and all other text is coped to the output. The start and stop string are not hardwired, can be configured and even dynamically changed in the input. Jamal can create your file from a Jamal source file no matter any particular need to keep new lines, spacing, or special characters. That way, Jamal can be applied as a universal, programmable preprocessor to any text file type. The types include document formats, like Markdown, AsciiDoc, HTML, and textual data description formats, like JSON, YAML, XML, or even programming languages.
The source text
-
uses built-in macros (there is a purposefully limited number of them in the core module and there are a lot in different other modules),
-
define and use previously defined user-defined macros,
-
include files,
-
use custom-made macros,
-
include and execute JShell, ScriptBasic, Groovy, Ruby or other scripts,
-
works with yaml, XML data,
-
import documentation and code snippets from the documented application or over the network,
-
can create PlantUML diagrams, and
-
can set assertions checking the consistency of the documentation with itself and with the documented system.
You can start Jamal processing
-
on the command line even without installing Jamal itself automatically downloading from the net,
-
as a Maven plugin,
-
starting with
jbang
, -
as a Maven extension letting you use Jamal macros in your POM files,
-
as a JavaDoc doclet letting you use Jamal macros in your JavaDoc documentation (including markdown), and
-
embedded into applications using a simple API, for example the Yamaledt JUnit 5 YAML test data source library.
You can extend the set of built-in macros creating new macros in any JVM language.
Jamal is a text to text processor. It is also a templating engine to maintain redundant text files. During development, there are often text files with redundant information you may need to maintain.
-
Some property or other resource files in a Java project may be slightly different for the different environments, test support, user acceptance test, production. Changing something in one of them many times should be followed in the other versions. This can be automated using Jamal.
-
A textual documentation has cross-references, but the format does not support automatic symbolic anchors and references. Jamal macros can keep track of the references and give you a warning when a reference becomes obsolete needing update.
-
The documentation may refer to some files in the project or some Java objects. Jamal macros can check that the files, classes, methods or fields exist and in case they were moved, removed, renamed Jamal will warn you to update the documentation.
-
You can use Jamal to automatically copy some part of your code source files as examples into the documentation. Whenever the source changes the output will automatically change when Jamal runs.
-
Jamal can collect and transform documentation text form different sources. You can put some of the documentation into the source code right at the place where the functionality is implemented. You have less chance forgetting the documentation update related to that feature whenever it changes. Jamal will fetch the documentation from the file, transform it to fit the output format and generate up-to-date compiled documentation.
-
You can embed Plant UML diagram text definition into your documentation source. The Plant UML macro extension will generate the diagrams and create the image reference into the output document. Some document format support embedding Plant UML. Using Jamal you can embed Plant UML into any textual documentation format. Because the diagram description is part of a Jamal macro processed source you can use Jamal macros in the diagrams. That way you are not limited by the macro features supported by Plant UML.
-
If you have any other use, please tell us.
Generally, Jamal reads a text and generates another one. In the source file, it processes macros, and the result gets in the macros' place. That way, text, and macros are mixed conveniently.
You can use Jamal as a maven plugin. The Java::Geci code generators also support Jamal, where you can write your Java code template using Jamal. You can use it as an embeddable macro engine in your Java application. Jamal can be used in JavaDoc using the Jamal Doclet implementation.
These different features are documented in their respective module documentations:
Applications, Embedding
-
Jamal Doclet Documentation, How to use Jama in JavaDoc
-
Jamal Maven Plugin README, How to use Jamal as a Maven plugin
Programming Language Modules
-
Ruby Module README, How to use Ruby code in your Jamal source
-
Groovy Module README, How to use Groovy code in your Jamal source
-
ScriptBasic Module README, How to use ScriptBasic code in your Jamal source
Other External Modules
-
Io Module README, How to read and write external files from Jamal macros
-
Jamal Jamal Module README, How to use Jamal inside Jamal as an embedded language
-
Jamal Markdown Module README, Convert markdown to HTML, main usable together with the Jamal Doclet to have Markdown in JavaDoc
-
Jamal PlantUML Module README, Embed PlantUML pictures into your documentation
-
Jamal Snippet Module README, Use snippets to compile your documentation
-
Jamal Yaml Module README, Use data from Yaml files in your macros and use macros in your Yaml files
Test Support
-
Jamal Test Module README, Use this module to test your own Java implemented macros
In this readme, we first discuss how the macros look and how Jamal will convert its input to the output. Then we discuss the API that lets you embed the macro processing into your application.
Table of contents
1. Starting Jamal
JShell
The simplest way to start Jamal is to use the JShell.
All you need to do is execute the following command:
jshell https://git.io/jamal
Note
|
The URL is a shortened URL of GitHub. It redirects to https://raw.githubusercontent.com/verhas/jamal/master/jamal-cmd/jamal.jshell |
It will start Jamal to process all files with .jam
extension in the current directory and below. The output files will have the same name as the processed file without the .jam
at the end. For example, pom.xml.jam
will be processed to pom.xml
.
You do not even need to install Jamal. If you have Java 11 or later installed, you can execute the above command. JShell will download and execute the script from the URL depicted above. The script will check if Jamal is installed on your machine. If it is not installed, it will automatically download the needed JAR. When the JAR files are downloaded, it will start Jamal in the current working directory using the default settings. You can alter the settings using the jamal.options
file. If this file does not exist in the current working directory, then the JShell script will create one containing the default settings.
Maven Plugin
It is also straightforward to start Jamal using the Maven plugin version. To do that, you have to have Maven installed, but as a Java developer, you probably have. Then you can issue the command:
mvn com.javax0.jamal:jamal-maven-plugin:1.0.2:jamal
if you have a pom.xml
file in your directory.
If you do not have, then read the documentation of the Jamal Maven plugin at https://github.com/verhas/jamal/blob/master/jamal-maven-plugin/README.md
It is short and straightforward.
When something goes wrong, then Jamal will give you a detailed error message. The message will include the file name, line number, and character count where the error happened. Jamal may think it works fine in other cases, but the output is not exactly what you expected. Sorry, in this case, the issue, most probably, is with your expectations. Jamal converts the text following the rules defined in this document.
Maven Extension
If you want to use Jamal macros to maintain your Maven POM files, you can do that. Edit the content of the POM XML in the file pom.xml.jam
. This file should contain the POM XML possibly enhanced with Jamal macros. Create a .mvn
directory with an extensions.xml
file in your project root. About the content and how this Maven extension works read the extension’s documentation.
Starting Command Line Version
To start Jamal on the command line, you need a command:
java -cp $HOME/.m2/repository/com/javax0/jamal/jamal-engine/1.10.4/jamal-engine-1.10.4.jar:$HOME/.m2/repository/com/javax0/jamal/jamal-api/1.10.4/jamal-api-1.10.4.jar:$HOME/.m2/repository/com/javax0/jamal/jamal-tools/1.10.4/jamal-tools-1.10.4.jar:$HOME/.m2/repository/com/javax0/jamal/jamal-core/1.10.4/jamal-core-1.10.4.jar:$HOME/.m2/repository/com/javax0/jamal/jamal-cmd/1.10.4/jamal-cmd-1.10.4.jar: javax0.jamal.cmd.JamalMain options
It is not a user-friendly approach. You do not want to type all the paths, and the JARs every time you want to start Jamal. For this reason, there is a file, jamal.sh
that has the following content:
#!/usr/bin/env bash
MODULES="api engine core tools cmd"
REPO=$HOME/.m2/repository/com/javax0/jamal
VERSION=1.10.4
for MODULE in $MODULES ; do
if ! test -f $REPO/jamal-$MODULE/$VERSION/jamal-$MODULE-$VERSION.jar; then
if command -v wget &>/dev/null; then
wget --no-check-certificate https://repo1.maven.org/maven2/com/javax0/jamal/jamal-$MODULE/$VERSION/jamal-$MODULE-$VERSION.jar -O $REPO/jamal-$MODULE/$VERSION/jamal-$MODULE-$VERSION.jar
else
if command -v curl &>/dev/null; then
curl https://repo1.maven.org/maven2/com/javax0/jamal/jamal-$MODULE/$VERSION/jamal-$MODULE-$VERSION.jar -o $REPO/jamal-$MODULE/$VERSION/jamal-$MODULE-$VERSION.jar
else
echo "There is no curl nor wget available"
exit -1
fi
fi
fi
done
CLASSPATH=""
for MODULE in $MODULES ; do
CLASSPATH=$REPO/jamal-$MODULE/$VERSION/jamal-$MODULE-$VERSION.jar:$CLASSPATH
done
java -cp $CLASSPATH javax0.jamal.cmd.JamalMain $*
The shell variable MODULES
should list the Jamal modules you may need to use in your processing. The basic modules needed under every circumstance are listed in the example. The other modules available are snippet
, scriptbasic
, groovy
, ruby
, plantuml
, and debug
. The shell variable REPO
must be set to point to the repository where your local JAR files are. VERSION
has to be the latest version or the one you intend to use.
The invocation of the shell script is ./jamal.sh options
where the options
have the key=value
format. If you use a simple option help
, then Jamal will print out a short screen that looks something like this:
Usage: <main class> [-fhvx] [--dry-dry-run] [--dry-run] [-c=<macroClose>]
[-d=<depth>] [-e=<exclude>] [-g=<debug>] [-i=<include>]
[-o=<macroOpen>] [-s=<sourceDirectory>]
[-t=<targetDirectory>] [-r=<transform> [<transform>]]...
[<inputFile>] [<outputFile>]
[<inputFile>]
[<outputFile>]
-c, --close=<macroClose> the macro closing string
-d, --depth=<depth> directory traversal depth, default is infinite
--dry-dry-run run dry, do not execute Jamal
--dry-run run dry, do not write result to output file
-e, --exclude=<exclude> file name regex pattern to exclude from the
processing
-f, --file convert a single file, specify input and output
-g, --debug=<debug> type:port, usually http:8080
-h, --help help
-i, --include=<include> file name regex pattern to include into the
processing
-o, --open=<macroOpen> the macro opening string
-r, --transform=<transform> [<transform>]
transformation from the input file name to the
output file name
-s, --source=<sourceDirectory>
source directory to start the processing
-t, --target=<targetDirectory>
target directory to create the output
-v, --verbose verbose output
-x, --regex interpret transform, include and exclude options
as regex
The command line can contain options and parameters. Most of the options have single character version and also multiple character versions. The option values have to be written after the option in case the option is single character and with a =
is multiple-character.
The options you can use with the command line version of Jamal are the followings:
-
--dry-dry-run
will tell Jamal to perform a dry run without invoking the conversion. Use this opition to test the input and output pattern to see which files will Jamal process and what output files it will create. -
--dry-run
is a dry run, but not so dry as--dry-dry-run
. When this option is used the Jamal processing is performed, but the result is not saved into the out. Using this option you can see what files Jamal will process and you can also see if there is any error during the processing. -
-c
or--close=<macroClose>
specifies the macro closing string. The default macro closing string is}
. When using this option mind that some characters need escape on the command line. -
-o
or--open=<macroOpen>
specifies the macro opening string. The default macro opening string is{
. When using this option mind that some characters need escape on the command line. -
-f
or--file
instructs Jamal not to parse the directory for input files. When this option is used Jamal will process the command line parameter<inputFile>
and it will write the output to<outputFile>
. -
-s
or--source=<sourceDirectory>
specifies the source directory where Jamal will start looking for input files. The file listing is recursive going into subdirectories. The default value is the current directory. -
-d
or--depth=<depth>
limits the dept of directory recursion. The default value does not limit the depth. -
-e
or--exclude=<exclude>
exclude the files that match the pattern<exclude>
. The pattern can be usualy file matching wild-card pattern or regular expression if the option-x
,--regex
is used. The default value is not to exclude any file. -
-i
or--include=<include> ` include the files that match the pattern `<include>
. The pattern can be usualy file matching wild-card pattern or regular expression if the option-x
,--regex
is used. The default value is*.jam
. -
-r
or--transform=<transform> [<transform>]
define one or more transformation. When multiple files are processed this transformations are used to calculate the output file name from the input file name. The option must have two values. The first value is the regular expression, the second parameter is the replacement string. These are the parameters that will be used in the Java methodinputFileName.replaceAll(a,b)
to calculate the output file name. The default value is\.jam$
and an empty string. The default value will causereplaceAll
to chop off the.jam
extension from the end of the file. That way, for examplepom.xml.jam
will be converted topom.xml
. -
-t
or--target=<targetDirectory>
can specify the target directory where the output will be stored. If input files are under some subdirectories of the<sourceDirectory>
then the same directory structure will be created for the output. The default value is the current directory. -
-x
or--regex
use regular expression for the<include>
and for the<exclude>
values. Transform is always interpreted as regular expression. -
-g
or--debug=<debug>
start the code in debug mode. To use this option the debugger module implementing the debugger must be on the classpath. This is automatically ensured when Jamal is started using jbang using thejbang jamal@verhas
command. The parameter<debug>
is the debugger configuration string. To use the web based debugger you can specifyhttp:8080
. With that parameter the debugger will start to listen on the port8080
on the localhost ip. The client code that runs in the browser can also be downloaded from the same server from thehttp://localhost:8080
address. If you specify a different port, then from that port. -
-v
or--verbose
verbose output -
-h
or--help
help -
<inputFile>
the input file in case the option-f
or--file
was used -
<outputFile>
the output file in case the option-f
or--file
was used
Starting with JBang
JBang (https://www.jbang.dev) is a popular command line tool that eases the startup of Java applications. Jamal can be started using JBang.
This may be the choice for you if you want to use Jamal, but you do not even have Java installed. Installing JBang is extremely simple. When running Jamal using JBang, Jbang will install everything that is needed to execute Jamal is a clean and non-intrusive way.
To start Jamal when you have JBang installed on your machine the command line to start Jamal is
jbang jamal@verhas ... options ...
This command will invoke the command line version automatically caring about all the Jar files. The syntax and meaning of the options are the same as in case of the command line version. This startup also loads all the Jamal extensions, including snippet
, scriptbasic
, groovy
, ruby
, plantuml
, and debug
(1.7.3 and later) and some others. If you want to see the exact list of the modules this startup loads have a look at the starter file.
Note
|
The possibility to start Jamal using JBang was developed for the version 1.7.3, and it was retrofitted for the version 1.7.2 before the release of 1.7.3. |
Note
|
If you have used Jamal with jbang before then jbang will store its catalog file in the local cache. When you start Jamal using
After that you can start jbang again. It will download the new catalog, always pointing to the latest release. |
Debugging Macro Conversions
When something goes wrong, Jamal gives you a detailed error message. The message will include the file name, line number, and character count where the error happened. In other cases, Jamal may think it works fine, but the output is not exactly what you expected. Sorry, in this case, the issue, most probably, is with your expectations.
In cases like that, you can try to debug the execution of the macro engine. There are two possibilities:
-
use the trace functionality, or
-
use the debugger.
The trace functionality can create a detailed XML trace of the execution that can later be examined. The trace information is structured with nested structures. XML is a format that can accommodate such nested structures and has very extensive editor support.
The debugging functionality can execute the macro transformation step-by-step providing interactive debugger user interface. The tracing functionality was developed earlier and its importance lessens by the introduction of the debugger.
Tracing
To get a trace file during the execution of Jamal you can
-Djamal.trace=tracefile.xml
on the command line that starts Jamal. It will specify a trace file, in this case, tracefile.xml
. If it is more convenient, you can also specify the trace file using the environment variable:
export JAMAL_TRACE=tracefile.xml
The environment variable is taken into account only if the jamal.trace
system property is not defined.
The trace file will contain all the macro evaluations' inputs and outputs. Since there can be many Jamal evaluations one after the other, Jamal does not overwrite old trace information. It appends the new trace information. Before starting Jamal, you can manually delete the trace file. Trace files grow large quickly. If you do not want to trace anymore, do not forget to unset the environment variable typing
unset JAMAL_TRACE
to avoid an excessively large trace file growing on your disk.
Debugging
To debug a Jamal macro processing you have to start Jamal in debugging mode. Jamal switches on debugging mode if the system property jamal.debug
or the environment variable JAMAL_DEBUG
is defined. The value of the property or the variable controls which debugger starts and how.
Currently, there are two debuggers implemented:
-
web based debugger with UI written in React.js
-
a TCP/telnet based debugger.
The detailed technical documentation of the server side of the debuggers is described in the document debugger readme. The TCP based debugger is only for experimental purposes or when the web based debugger is not available.
Here we briefly describe the web based debugger. Since the UI can best be described with pictures, especially with moving pictures the documentation is created in screen capture videos.
To start Jamal in debugger mode you can specify
-Djamal.debug=http:8080
on the command line that starts Jamal. It will specify the web based debugger, hence the http
and the port, in this case 8080
. If it is more convenient, you can also specify the http:8080
debug option file using the environment variable:
export JAMAL_DEBUG=http:8080
The environment variable is taken into account only if the jamal.debug
system property is not defined.
If you do not want to debug anymore, do not forget to unset the environment variable typing
unset JAMAL_DEBUG
In debug mode Jamal stops twice for each macro evaluation. Once when it selects the next text without macro from the actual start of the input, or a macro at the start of the input. Second time when the macro was evaluated, and the text is appended to the output.
If you open your browser after you started Jamal in debug mode and try to open the url http://localhost:8080
you will get the debugger UI in your browser. This user interface will let you see the current input, the current output, the defined built-in and user defined macros. You can let the code run, make one step macro evaluation, go into nested macro evaluation, evaluate text interactuvely in the current evaluation envirionment and so on.
2. Simple Example
As a quick sample to have a jump start what Jamal can do:
{@define fruit(color,name,actualSize)=we have an color name of size actualSize}
{fruit/red/apple/20ounce}
{fruit/green/melon/1kg}
will be converted by Jamal to the file
we have an red apple of size 20ounce
we have an green melon of size 1kg
In this sample, the built-in macro define
is used to define a so-called user-defined macro fruit
. This macro has three arguments named color
, name
, and actualSize
. When the user-defined macro is in use, the actual values replace these arguments.
Note that the macros open with the {
character and close with the }
character in this example. These are not hardwired in Jamal, and there is not even a suggested default for that. The embedding application has to define the opening string and the closing string. For example, the embedding Java::Geci application uses {%
and %}
as macro open and macro close strings. It does it because the {
and }
characters frequently appear in the Java source code. On the other hand, Java code rarely uses the {%
or %}
format. In this documentation, we use the {
and }
strings.
However, you have to be aware that this is NOT enforced. It is not even a recommendation or a convention. You can specify the macro opening and closing string as the program parameter, and the Jamal source code can also change it. You can change them using the built-in sep
macro (see later) in the Jamal source.
There is one exception where Jamal uses {
and }
as hardwired strings for macro opening and closing. This exception is implemented in version 1.5.0 and later. When you import a file into your code, and the imported file starts with the characters {@
, the import will use {
and }
. This way, you can easily import files from external sources, like a JAR file or via the web. The package that defines an import file can use the {
and }
characters. Even if your Jamal file uses different macro opening and closing strings, you do not need to change it to {
and }
in this particular case. You may have [[
and ]]
as opening and closing strings. In this case, you write [[@import res:MyResource.jim]]
as an example, and it still will be imported correctly.
The parameters are separated using the first non-space, non-alphanumeric character following the macro’s name in the macro use. Thus, you can write
{fruit/red/apple/20ounce}
{fruit|red|apple|20ounce}
{fruit.red.apple.20ounce}
{fruit :red:apple:20ounce}
the output will be the same for each line:
we have an red apple of size 20ounce
we have an red apple of size 20ounce
we have an red apple of size 20ounce
we have an red apple of size 20ounce
Note
|
In the last example, we used the : character as the separator. When the name of a macro contains one or more : characters, then the macro is global. Since this character can also be part of the macro’s name, there must be a space before it. |
There are also some other rules that make it possible to use a space as separator character. When a macro has exactly one argument then the parsing follows special rules. It is also possible to invoke a macro with more or less number of arguments than are defined using the option lenient
. These are advanced topics and are detailed later.
3. Other Macros
define
is not the only built-in macro in Jamal. The comprehensive list of built-in macros are
You use the built-in macros with #
or @
in front of the macro’s name. These characters signal that the macro is built-in (as opposed to user defined). The typical use is to start a macro with the @
character. In that case, the macro evaluates the rest of the input till the matching closing string.
If the macro starts with the #
character, then the input is first parsed for other macros. These macros are evaluated, and their results replace their occurrences in the code. Only after this, the macro we are looking at is evaluated.
For more about definition scopes and exporting, read the section about export
. In that section we discuss the evaluation order of the macros in great detail.
comment
i. since 1.0.0 (core)
comment
is used to insert comments to the input. It can also be used to enclose definitions without side effects, but this is not recommended. For that purpose, use the [block
](#block) macro.
For more about definition scopes and exporting, read the section about export
. In that section we discuss the evaluation order of the macros in great detail.
this is some {@comment this text
will not appear in the output}text
will generate
this is some text
Note that this is important to use the @
character in front of the keyword comment
to make it a real comment. If the macro character #
is used, like {#comment comment_text}
then the comment_text
part will be evaluated. If there is some macro in the comment_text
that modifies the evaluation state, then the modification will happen. For example, if the comment_text
defines some global macro, then the defined macro can be used after the comment
block.
It is safe to say always to use {@comment …}
. When the code needs the evaluation, then use the [block
](#block) macro.
block
ii. since 1.0.0 (core)
block
is technically the same as comment
. It is recommended to use the comment
macro with the @
starting character. In that case the content of the comment is not interpreted by Jamal. Use the block
with #
to have the content interpreted. Block should be used to enclose definitions to a scope level. Note that the result of the macro {#block … }
is an empty string.
For more about definition scopes and exporting, read the section about export
. In that section we discuss the evaluation order of the macros in great detail.
begin
and end
iii. since 1.0.0 (core)
The macros begin
and end
start and close a local definition scope. This is similar as using a {#ident … }
macro to create a new scope for the evaluation of the macros inside it. The text between the {@begin}
and {@end}
will be evaluated in a new scope. Any user defined macro in this scope is going to be local, unless exported or has a :
in the name.
It is recommended to use begin
and end
when the structure is complex, and it is more readable to use the begin
, end
macros than a simple block
. To ensure that all begin
has an end
you can name the blocks. You can put an arbitrary string after the macro name begin
and if you do then you have to repeat the same string after the macro name end
. The spaces from the beginning, and the end of the parameter are trimmed.
{@define Z=1}
{@begin alma}
{@define Z=2}{Z}
{@define S=2}{@export S}
{@end alma }{Z}{S}
will result
2
12
First Z
is defined to be the string "1"
(without the quotes). Then we start a new scope, named alma
. Inside this new scope we redefine the macro Z
to be 2
. When we use Z
writing {Z}
then it will output 2
here. We also define S
to be 2
and we also export
it. Exporting means that the definition will get to the surrounding scope. After that we close the scope named alma
. When closing the scope there is an extra space after the name, but it does not matter. Now S
is 2
, because it was exported and Z
is 1
, because it was defined to be 1
on this level and was not exported from the nested level.
For more about definition scopes and exporting, read the section about export
. In that section we discuss the evaluation order of the macros in great detail.
Scopes are nested, stacked into each other any levels. Scopes are opened by many things, like macro start, or including a file. You can close a scope using the macro end
that was opened with a matching begin
. You cannot not close a scope using end
that was opened by something else. For example, you cannot get into the scope of the including file putting a pair-less end
macro into an included file. This will trigger a processing error. It is also an error if a {@begin…}
does not have its {@end…}
pair in the main file or in any included or imported file.
define
iv. -
since 1.0.0 (core)
-
since 1.6.4
default
as special macro -
since 1.7.4
default
macro first argument, macro can be defined to evaluate verbatim -
since 1.7.6 optional and extra ignored arguments
Define basics
define
defines a user-defined macro in the current scope.
For more about definition scopes and exporting, read the section about export
. In that section we discuss the evaluation order of the macros in great detail.
The syntax is
{@define id(arguments)=body}
or
{#define id(arguments)=body}
The arguments part is optional in case there are no arguments for the macro. In that case the macro syntax is
{@define id=body}
or
{#define id=body}
or
{@define id()=body}
or
{#define id()=body}
Using the ()
characters after the identifier of the macro is optional, and the result is exactly the same as if it omitted. The two definitions are equivalent.
Note
|
There is one exception, when you have to use () even for empty parameter list. This is the case, when the id ends with a colon : . In this case the definition {@define id:=…} would be ambigous, because using := instead of = has a special meaning (see it later). |
When the macro is used, the arguments are replaced in the body by the actual parameters supplied at the place of use. The arguments are specified as a comma-separated list. They are usually identifiers.
Note that the arguments do not have any special syntax. The only requirement is that they do not contain a comma ,
, a closing parenthesis )
and they do not start or end with …
. That is because the list is comma-separated, because )
terminates the list of the arguments, and a …
prefix or postfix denotes optional arguments. It is recommended, though, to use normal identifiers and no spaces in the argument names. This is only a recommendation and is not enforced by Jamal. You may need to process some special text. You may need some specially named arguments. In the examples, you usually see that the arguments start with a $
character.
Somebody may follow other conventions, like starting every argument with the *
or enclosing the argument names between |
or /
or some other characters. These practices can be absolutely okay so long as long they support the readability of the macro body and the use of the macro. Applying such practices may help to visually separate the macro arguments from the textual content of the macro body.
From practice, we see that in case of longer macros using simple, argument names with one or only a few letters may lead to some error. For example the macro:
{@define fox(x)=The brown fox jumps over the high x}{fox fence}
will result
The brown fofence jumps over the high fence
This is probably not the result that the macro creator wanted. They probably missed the point that the word fox
also contains an x
.
To ensure that the argument replacing is consistent, the argument names cannot contain each other as a substring. Assume that there is an argument a
with an actual parameter value oneA
. There is another argument named aa
with an actual value twoAs
. In this case the occurrences of aa
in the body could be replaced to twoAs
or oneAoneA
.
Although Jamal could define some rule, like left-to-right, or right-to-left, or longer-first evaluation these could still lead to hard to read situations. Jamal suffers from hard to read situations already without this extra headache. To avoid that Jamal does not allow you a
and aa
as argument names to a macro definition the same time.
During the replacement a parameter may be a string that contains the name of one or more argument names. This is absolutely legit. These will NOT be replaced with the parameter value(s) that were provided for the other argument(s) that are inside the value of the parameter. For example:
{@define z(*a,*b,*c,*d)=When a *a can *b then *c can *d}
{z /leopard and a *c/run/fish/fly}
will result
When a leopard and a *c can run then fish can fly
even though *c
is a fish
, but the characters *c
in the output come from the value of a parameter and therefore it is not replaced.
Special User Defined Macros
In Jamal user defined macros are defined using the define
macro. Internally, a user defined macro can be anything that implements a specific Java interface. Jamal only requires that it can be evaluated and that it handles the string arguments passed to it. Some built-in macros implemented in external packages, like the Yaml package have their implementation. It means that they create user defined macros that you can pass parameters, and their evaluation results some output, but they are not "classical" user defined macros. They just behave like the macros that are defined using define
. Foe example the macro `counter:define`from the Snippet package creates a "user defined" macro that results a number when used, but it also changes the value at every invocation.
Macro redefine
Macros can be redefined at any point. For example
{@define a=1}{@define a=2}{a}
will result
2
It is possible to use a question mark ?
after the macro keyword define
. In that case the macro is only defined if is NOT yet defined in the current scope or any other outer scope.
{@define a=1}{@define ? a=2}{a}
will result
1
It is also possible to use a !
instead of the ?
. In this case the macro define
will report an error if the macro is already defined.
{@define a=1}{@try! {@define! a=2}}
will result
The macro 'a' was already defined.
Note
|
The macro try! will catch the error and send the error message to the output. This is mainly used for debugging and in this case for documentation purposes. |
When a user defined macro is evaluated, the result of the macro is evaluated again resolving all the macros that happen to be in the result. This can be prevented using the verbatim
macro. You can also read more details on the macro evaluation order in the chapter verbatim
.
If you use the ~
(tilde) character after the keyword define
then the macro will be evaluated "verbatim" by default. It means that the value of the user defined macro will not be evaluated like if it was used with the macro verbatim
. For example:
{@define x=1966}
{@define a={x}}
{a} evaluates first to the macro `x` and then that evaluates to 1966
{@verbatim a} stops before the evaluation of the result of the macro and this way it is the same as
{@define ~ a={x}}{a}
will result
1966 evaluates first to the macro `x` and then that evaluates to 1966
{x} stops before the evaluation of the result of the macro and this way it is the same as
{x}
If, for any reason, you need to evaluate the result of such a macro you can use eval
or !
when using th e macro.
{@define x=1966}
{@define a={x}}{a} is the same as
{@define ~ a={x}}{!a}
will result
1966 is the same as
1966
Note
|
You cannot use The macros in the module Yaml define the object structures read from the Yaml format as verbatim user defined macros. When you use such a macro, like |
Global Macros
When the name of the macro contains at least one colon character :
then the macro will be defined in the global scope. Global scope is the top-level scope, and it means that a macro like that can be used everywhere in the text after it was defined.
For example modifying a bit our example from the "begin
and end
" section
{@define A:Z=1}
{@begin alma}
{@define A:Z=2}{A:Z}
{@end alma }{A:Z}
will result
2
2
In this case the macro A:Z
is a global macro because it has a :
in the name.
It is also possible to define a user-defined macro to be global without :
in the name. If the very first character of the name of the macro is :
then this character is removed, but the macro is defined in the global scope. Further modifying the example we get:
{@define :Z=1}
{@begin alma}
{@define :Z=2}{Z}
{@end alma }{Z}
which will result:
2
2
Note that you cannot use {:Z}
when using the global macro. The :
character in this case is not part of the name. Also note that you can define a local macro even if there is a global macro of the same name. For example
{@define :Z=1}
{@begin alma}
{@define Z=2}{Z}
{@end alma }{Z}
which will result:
2
1
The define
inside the begin
, and end
delimited scope does not redefine the global scoped Z
. It defines a scope local macro, which gets out of scope with the macro end
.
When a user-defined macro is used, the parameters are defined after the name of the macro. In the case of user-defined macros, there is no @
or #
in front of the name of the macro. Optionally there may be a ?
character. In that case, the result of an undefined user macro will be the empty string. In most other cases using an undefined user macro results an error.
{@try! {undefinedMacro}}
this is empty string >>{?undefinedMacro}<<
which will result:
User defined macro '{undefinedMacro ...' is not defined.
this is empty string >><<
Note
|
The try built-in macro is mainly for debugging purposes and returns the error message itself. |
If you automatically want to interpret all user defined macro reference if there was a ?
in front of them then you can use the option emptyUndef
. With this option there is no need for the ?
in front of the macro name, every undefined macro will evaluate to empty string.
{@options emptyUndef}>{?notDefined}<>{notDefined}<
results
><><
Default macro
In addition to having a ?
character or using the try
macro, there is another possibility to avoid the error in case of an undefined macro. If the macro default
is defined then it will be used instead of any undefined macro, even when the ?
character is used in front of the macro name.
Example:
>>{?hoppala}<<
{@define default=wupppss}{hoppala}
>>{?hoppala}<<
Result:
>><<
wupppss
>>wupppss<<
Note
|
During the design there were two possibilities. One, to let the |
{@define default=wupppss}\
{#ident {@undefine default}>>{?hoppala}<<}>>{?hoppala}<<
results
>><<>>wupppss<<
Because it is cumbersome to write {#ident {@undefine default}{?…}}
every time the option :noUndefault
can also be used.
{@define default=wupppss}\
{@options :noUndefault}>>{?hoppala}<<{@options ~:noUndefault}>>{?hoppala}<<
results
>><<>>wupppss<<
The macro default
can have arguments, and they will be handled as they should be.
{@define default($x)=wupppss $x}{hoppala zumzum}
>>{?hoppala zumzum}<<
Result:
wupppss zumzum
>>wupppss zumzum<<
Note that there can be many undefined macros, and the different macros may expect different number of parameters. If the number of the actual parameters is not the same as what the defined default
expects Jamal will stop with error. Consider the use {@options :lenient}
along with the definition of the default
macro, or a default macro with optional arguments.
Starting with the version 1.7.4 the macro default
can have a special first parameter. If the first argument of the macro is either $macro
or $_
then this parameter will hold the name of the macro, which was not found. That way the default
macro can use the name of the macro in its evaluation.
Example:
{@options :lenient}
{@define default($_,$x)={@if |$x|<$_>$x</$_>|<$_/>}}{hoppala}
{bikkala zz}
Result:
<hoppala/>
<bikkala>zz</bikkala>
Starting with the version 1.7.6 Jamal introduced optional arguments to user defined macros. (Details are a bit later.) You can use optional arguments when you define a default
macro. For example:
{@define default(...)=DEFAULT}{huppala}{bumbala}{wopsydosy}
will result
DEFAULTDEFAULTDEFAULT
The parameters stand after the name of the macro separated by a separator character. The first non-whitespace and non-alphanumeric character after the name of the macro is the separator character. It can be /
as in the examples below, but it can also be any non-alphanumeric character. The number of parameters should be exactly the same as the number of argument unless the {@options :lenient}
was specified, or the …
was used to denote optional arguments. In the case of optional arguments, the missing arguments will be zero-length strings. If there are extra parameters, they will be ignored.
The separator character cannot be an alphanumeric character (letter or digit, Unicode categories Lu, Ll, Lt, Lm, Lo, and Nd). Any other Unicode character can be used as a parameter separator character.
If the user-defined macro has exactly one argument then there is no need to use a separator character. The sole parameter of the macro can start after the name of the macro at the first non-whitespace, alphanumeric character. For example,
{@define enclose(a)=<!!a!!>}
{enclose this text}
will result
<!!this text!!>
The parameter, in this case should start with an alphanumeric character or with a macro start string. If it starts with something else then that character will be the separator character that separates the parameters. In this case, because there is only one parameter it will separate the macro name from the parameter. For example,
{@define enclose(a)=<!!a!!>}
{enclose /-}
will result
<!!-!!>
Writing
{enclose -}
will result
<!!!!>
because -
is not alphanumeric and therefore it is treated as a separator character separating a single empty string. On the other hand
{@define enclose(a)=<!!a!!>}
{@define dash=-}
{enclose {dash}}
will work, and the result will be
<!!-!!>
This is because the {
in this case is the macro start string. in that case, the first character of it is not considered to be as a separator character even though it is not alphanumeric.
There are cases when it is necessary to use a separator character. In some cases the parameter starts with a significant space. In other cases it starts with a character that is not alphanumeric. In that case the above macro should be used like the following three examples:
{enclose |+this text}
{enclose ||this text}
{enclose | this text}
These uses of the above macro will result
<!!+this text!!>
<!!|this text!!>
<!! this text!!>
In the second line in the examples, the separator character is used in the parameter. Because the macro needs only one argument all the rest of the parameter until the macro closing string is used as the single parameter. It is not split up further along the later occurrences of the separator character. Just use any non-alphanumeric character in front of the parameter that looks good. You need not worry that the character itself presents in the content.
{@options ~lenient}
{@define x(a,b)= |a b|}
{@try!{x/s/h/t}}
will result an error, because there are too many arguments:
Macro 'x' needs 2 arguments and got 3
>>>s
>>>h
>>>t
Note
|
In the example above we switched off the lenient mode, because it was already switched on for some previous samples. |
The rule that the separator character is not considered as another separator in the rest of the argument is valid only when there is only one argument. In case of multiple arguments this could easily lead to unreadable macro use. The above example modified to be lenient demonstrates this:
{@define x(a,b)= |a b|}{@options :lenient}
{x/s/h/t}
will result
|s h|
The provided third value, t
is ignored.
There are situations where the use of a separator character is not a must, but the use of it helps the readability. Consider, for example {enclose/a/b/v}
. We know from earlier that enclose
has only one argument, however the use of it looks like it has three. The one argument it has is a/b/v
.
Omitting the separator character, /
in this case, does not help the readability or only a bit. The use {enclose a/b/c}
still looks like a macro with three parameters. In situations like that the most readable solution is to use an explicit separator character that looks good. For example {enclose |a/b/c}
makes it evident and readable that there is only one parameter: a/b/c
.
In the following sample code, you can see some complex examples that demonstrate these cases:
{@define parameterless=this is a simple macro} macro defined
{parameterless}
{@define withparams(a,b,%66h)=this is a b %66h} macro defined
{withparams/A/more complex/macro}
{withparams/%66h/%66h/zazu} <- %66h is not replaced to zazu in the parameters
{@define? withparams(a,b,c)=abc}here 'withparams' is not redefined
{withparams|a|b|c}
{#comment {@define x=local}{@define :x=global} {#define :y=here we are {x}}}
{y}
here we are {x}
will generate
macro defined
this is a simple macro
macro defined
this is A more complex macro
this is %66h %66h zazu <- %66h is not replaced to zazu in the parameters
here 'withparams' is not redefined
this is a b c
here we are local
here we are global
This is a fairly complex example. To ease the understanding note the followings:
-
%66h
is an absolutely valid macro parameter name. Anything can be a parameter name that does not contain a comma, a closing parentheses, does not start or end with…
and is not a substring of any other parameter. -
When a macro parameter is replaced in the body of the macro the processing of that string is finished and is not processed further replacing macro parameters. Macro parameters are only replaced with the actual values in the macro body and not in the parameter actual values. That is why parameters
a
andb
are replaced with the actual string ' %66h' but then this is not replaced with the actual value of the parameter%66h
. -
When we define the macros
x
andy
inside thecomment
macro it happens in a local scope of thecomment
macro. It means that the definition ofx
has no effect outside the macrocomment
. Using the name:x
defines the macrox
in the global scope, that is above the current scope. When we defined the macroy
it also starts with:
and so it gets into the global scope. However, during the definition, it is in the local scope of thecomment
macro where the local definition ofx
overrides the global definition ofx
even though the global definition happened later. Therefore,y
will behere we are local
. That is also becausey
is defined using the#
character before the built-in macro keyworddefine
and thus the content of the definition is evaluated before defining the globaly
.
Pure Macros
It may happen that the macro opening and closing strings are different when the macro is defined and when used. In a situation like that the macro evaluation replaces the macro opening and closing strings in the macro definition to the actual macro opening and closing strings. It can be prevented using :=
instead of a =
between the name, parameter list and the body of the macro.
{@sep [ ]}[@define a=[z]{z}][@sep]{@define z=3}{a}
{@sep [ ]}[@define a():=[z]{z}][@sep]{@define z=3}{a}
results
3{z}
[z]3
When a
is evaluated the result is [z]{z}
on both lines. In the next step this result is evaluated, because the macro is not a verbatim
one. In the first case the macro a normal one and the evaluation knows that the macro opening and closing strings were [
and ]
. In this case the evaluation also knows that the characters {
and }
are just ordinary characters.
In the second case, however, the macro is a "pure" macro and is evaluated as it using the current macro opening and closing strings.
Note that when there are no parameters, and the macro definition does not use the optional ()
after the name of the macro the :=
would be ambiguous. To avoid this ambiguity you have to use ()
after the name of the macro if the name of the macro finishes with a :
character.
Optional Arguments
Setting the option lenient
is a very aggressive way to make all macros inside the current scope evaluated in the lenient way. There are more subtle methods to specify that some macro may work with less or more actual parameter values than their concrete arguments. Macros can define a minimum, and a maximum number of parameters they need when they are called. When an argument in the define
macro starts with …
characters it means that the next argument, and the arguments afterwards are optional. When using the macro these arguments will be empty string when not provided. For example,
{@options ~lenient}{@comment just to be sure}
{@define a(a,b,...c,d,e)=>a< .b. /c/ |d| (e)}
{a :1:2:3}
will result
>1< .2. /3/ || ()
You can also say that all the parameters are optional in case the …
is in front of the first argument:
{@define a(...a,b,c,d,e)=>a< .b. /c/ |d| (e)}
{a :1:2:3:4:5}
{a :1:2:3:4}
{a :1:2:3}
{a :1:2}
{a :1}
{a}
will result
>1< .2. /3/ |4| (5)
>1< .2. /3/ |4| ()
>1< .2. /3/ || ()
>1< .2. // || ()
>1< .. // || ()
>< .. // || ()
Optional parameters are different from leinent mode, that they do not allow extra parameters. For example the
{@try! {a :1:2:3:4:5:6}}
will result the error
Macro 'a' needs (0 ... 5) arguments and got 6
>>>1
>>>2
>>>3
>>>4
>>>5
>>>6
If you want to allow extra parameters then you can append …
after the last argument:
{@define a(...a,b,c,d,e...)=>a< .b. /c/ |d| (e)}
{a :1:2:3:4:5:6}
resulting in
>1< .2. /3/ |4| (5)
Appending …
after some other argument, which is not the last one or using …
prefix on more than one argument is an error. It is also an error to add …
postfix after the first argument when the macro has one argument. One argument macros are treated in a special way and all the text following the macro will be treated as a single argument, thus it is meaningless to use …
after a single argument.
undefine
v. -
since 1.6.6
undefine
can be used to undefine a macro. Undefining a macro works the same way as definition: in scope. When you undefine a macro it will be undefined only for the current scope and later for any lower newly opened scope. Undefining a macro does not affects the definition of the macro in any higher level.
You can undefine a macro on the global level the same way as you can define a macro on the global level.
Simple undefine on one single scope
{@define fruit=apple}{fruit}{@undefine fruit} |{?fruit}|
Here we define the macro fruit
to be apple
, and we use it once. Following it, we undefine the macro. When we use it next time it is undefined. The use of it is not an error because we use the ?
in front of the macro name, but the result is the empty.
Finally, it will generate the following output.
apple ||
Undefine an inherited macro in a local scope
{@define fruit=apple}{fruit} {#ident {@undefine fruit} |{?fruit}|} |{?fruit}|
In this example we define the macro apple
on the top level scope, but we undefine it one level deeper. The macro is undefined only in the local scope, where it was undefined but on higher levels it is still defined.
Finally, it will generate the following output.
apple || |apple|
Being undefined can be exported
{@define fruit=apple} {fruit}\
{#ident {@undefine fruit} |{?fruit}| {@export fruit}}\
|{?fruit}|
In this example we undefine the macro fruit in the local scope, but then we export it from this scope. Being explicitly undefined can be exported the same way as the macro, which is defined. Because the "undefinedness" is exported the macro fruit
becomes undefined in the enclosing scope.
Finally, it will generate the following output.
apple|| ||
Undefine, export and redefine
{@define fruit=apple}\
global scope: {fruit}
{@begin scope_1}\
scope_1: {fruit}
{@begin scope_2}\
scope_2: {fruit}
{@undefine fruit}{@export fruit}\
scope_2: {?fruit}
{@define fruit=pear}\
scope_2: {fruit}
{@end scope_2}\
scope_1: {?fruit}
{@end scope_1}\
global scope: {fruit}
In this example we define the macro fruit
on the top level. After that we open two new scopes nested. We undefine the macro in the most inner scope, and we export this undefinedness to the middle scope. After that, we define the macro again in the most inner scope.
At this moment we have three "definition" of the macro fruit
. In the outer scope it is defined to be apple
. In the middle scope it is undefined. In the most inner scope it is defined to be pear
.
Finally, it will generate the following output.
global scope: apple
scope_1: apple
scope_2: apple
scope_2:
scope_2: pear
scope_1:
global scope: apple
Note
|
For the technically savvy, the following may help get a more comfortable grab of how the macro Jamal stores user-defined macros in maps. The key in the map is the id of the macro. The value in the map is a Java object that represents the user-defined macro. The maps are organized in a stack. The stack has one element for each scope. When a new scope opens in the Jamal source, the stack grows. When a scope is closed, the stack shrinks. Searching for a macro starts in the map stored at the top of the stack (opened latest). If the macro is not in the map, then the search goes deeper. The search finally finds the macro in one of the maps or runs out of stack levels. The maps are very general in the sense that they can store any Java object that implements |
eval
vi. since 1.0.0 (core)
eval
interprets the content of the macro. The text written after the macro keyword eval
is evaluated as a script. The scripting language can be defined following a /
character. If there is no script type defined (or jamal
is defined) then the content will be evaluated as normal Jamal macro text. Otherwise, the script engine named is used.
There are three ways to use the macro in one of the following formats:
eval macro text
eval/scripttype script
eval* text
If eval
is followed by /
character then the next identifier is the type of the script. White space characters before, and after the /
, as well as after the script type name are ignored. You can use any scripting language that
-
implements the Java scripting API and
-
the interpreter is available on the classpath when Jamal is executed.
If the script type is jamal
then it is the same as if there was no script type specified. You may need the explicit specification of jamal
when the content of the macro to be evaluated starts with the /
character.
If character following the keyword eval
is *
then the scripting type is jamal
and the evaluation is repeated until all macros are resolved. The macro assumes that all the macros are resolved when the evaluation of the text does not change any more. This may lead to infinite loop, therefore there is a built-in limit. eval*
evaluates the macro input at most 100 times. This limit can be changed with the option evaluateLoopLimit
. This name can also be used as a user defined macro to set this option globally, for example:
{@define `evaluateLoopLimit`=60}
This option has two aliases limit
, and max
. The aliases can be used interchanged between [
and ]
characters following the \*
character, for example:
{@eval* [limit=60] evaluate this max 60 times}
The following two examples show how eval
can be used to evaluate simple arithmetic expressions using the Java built-in JShell interpreter. Note that in the second example the macro eval
is preceded with the character #
therefore the body of the macro is parsed for other macros before eval
itself is invoked. That way {a}
and {b}
are replaced with their defined values and eval
itself sees 1+2
.
{@eval/JShell 1+3}
{@define a=1}{@define b=2}\
{#eval/JShell {a}+{b}}
will result
4
3
Note
|
Versions prior 1.5.0 used the Nashorn JavaScript interpreter as the default interpreter. |
Starting with version 1.5.0 Jamal introduces the !
modification character. When this character is used in front of a macro, then the result of the macro will be evaluated like it was surrounded with {#eval … }
. This can be used in the case of user-defined macros as well as in the case of built-in macros. Note, that in the case of user-defined macros the result of the macro will be evaluated by default. Using the !
in front of a user-defined macro will repeat the evaluation. You can use more than one !
characters in front of a macro. The macro result will be evaluated so many times as many !
characters there are. In case of a user-defined macro the "so many times" should be interpreted as one, by default plus N times.
For example:
{@define a=this is it}
{@define b={`a}}
{@define c={`b}}
{@define userDefined={`c}}
{userDefined}
{!userDefined}
{!!userDefined}
{!!!userDefined}
and the output is
{c}
{b}
{a}
this is it
In this example the macro userDefined
is {`c}
. User defined macros values are evaluated after the evaluation of the macro itself, therefore when we use {userDefined}
we get {c}
. The back-tick character before the macro after the {
is identical to the use of an ident
macro: {@ident {c}}
.
When there is a single !
in front of it, then the repeated evaluation results {b}
, and so on. To get the final result, in this case we need three !
characters, meaning four post evaluation.
You can use this character together with the back-tick macro modifying character. They do not eliminate each other, because the back-tick prevents pre-evaluation and !
provides extra post evaluation. When using !
to evaluate the result of a macro you cannot specify any scripting language. The evaluation will be Jamal macros evaluation.
defer
vii. since 1.10.0
The macro defer
evaluates its input only when the processing is finished. It can be used to execute some macros at the end of the execution, which have side effect, or to modify the final output using some macros.
At the place of the the macro the value of the macro is an empty string. The result of the evaluation, since it happens after the whole input was already processed and we have a final output, is also ignored. There is, however, a possibility for the content of the macro to read the final result and also to modify it.
When the input of the macro is executed the global macro $input
contains the output of the processing. The naming may be strange at first, but consider that this string is the input for the deferred macro evaluation.
If this evaluation defines the global macro $output
the value of the macro will be used instead of the original output.
The name of the input and output macros can be changed using options. The options
-
$input
with the aliasesinput
, andinputName
can specify the name of the input macro. -
$output
with the aliasesoutput
, andoutputName
can specify the name of the output macro.
As usually the option name can be defined as a macro, like {@define $input=$INPUT}
, the aliases can only be used as macro options, like
{@defer [output=OUTPUT] ... }
Note
|
You probably want to use the macro |
In the followings we will give some examples.
This example is the simplest. It defers an empty string.
{@defer}
Original result.
When the empty string is evaluated nothing happens, the original output is retained:
Original result.
The second example is a bit more complex:
{@define doplikate(a)=aa}\
{@defer
{#define $output={doplikate/{$input}}}
}\
Is this doplikated?
This example defines a user defined macro that duplicates the input. In the deferred evaluation the macro $output
is defined and it will be the same as the $input
repeated.
Is this doplikated?Is this doplikated?
The next example demonstrates that
-
the name of the input and output macros can be redefined, and
-
multiple
defer
macros are executed in the order as they were evaluated in the input during the Jamal processing.
{#block
{@define $output=OUT}\
{@define $input=IN}\
{@defer
{#define OUT=|{IN}|}\
}\
}\
{@defer {#define $output=*{$input}*}}\
Framed
Note that the definition of the macros $input
and $output
are local to the block
and therefore they have no impact on the second defer. Since the {#define OUT=|{IN}|}
is defind before {#define $output={$input}}
the text Framed
is enclosed first between |
characters and only the result is enclosed between \*
.
The output is:
*|Framed|*
The next example is almost the same as the previous. It uses macro options to set the input and output names for the first defer
macro:
{@defer [input=IN output=OUT]
{#define OUT=|{IN}|}\
}\
{@defer {#define $output=*{$input}*}}\
Framed
In this case there is no need for the block
macro, since option setting is always local to the macro where it is set. The result is the same as in the previous case:
*|Framed|*
The next example shows that you do not need to use the input at all to set the output.
{@defer
{#define $output=}{@comment just nothing}
}\
Is this ignored?
In this case the output is an empty string
The following sample shows that the macros used in the text of defer
have to be defined only when it gets executed. In the example the macro doplikate
is not defined when used in defer
only at the end of the file.
{@defer
{#define $output={doplikate/{$input}}}
}\
Annoying?{@define ~ doplikate(a)=aa}\
The output is:
Annoying?Annoying?
The following example is a bit more complex. In this case the code uses the escape*
macro.
{@escape*````}\
{@defer
{#define $output={doplikate/{$input}}}{@comment DEBUG}
}\
{@escape* ``{mememe}``}Mememe?{@define ~ doplikate(a)=aa}\
In this case there are two deferred operations. The first one is the unescaping of escape*
. This is executed first, because the use of the first escape*
macro precedes the macro defer
. When this unescaping is finished the result of the processing will be {mememe}Mememe?
. It contains a string that can be interpreted as a macro. For this reason the macro doplikate
is defined as a "verbatim" macro. This is signalled by the ~
character after the define
keyword. Verbatim user defined macros are not post evaluated. When doplikate
is invoked in the defer
then {mememe}Mememe?
will be converted to {mememe}Mememe?{mememe}Mememe?
. This result also will not be evaluated again.
However, when we set the macro $output
in the line {#define $output={doplikate/{$input}}}
why {mememe}Mememe?
is not evaluated. The reason is that the user defined macro $input
holding the final result of the Jamal processing is also a verbatim macro.
The output is:
{mememe}Mememe?{mememe}Mememe?
Although $input
is verbatim, $output
does not need to be. This macro is used temporarily by the deferred action to change the output of Jamal processing. The following example shows that the value of $output
is not available as input for defer
. The macro $output
can only be set by the input of defer
and $output
is undefined when the evaluation starts:
{@defer {#define $output=aaa{?$output}}}\
{#define $output=this will not survive}
Annoying?
This example tries to use the value of the macro $output
in the deferred code. The deferred code can rely on the macros defined during the Jamal processing. Note, however that only the top level macros are available as all other macros are out of scope and only those, which were defined at the end of the Jama processing.
The macro $output
, however, is used in a special way. Because it serves to pass a modified output from the deferred code it is undefined before the deferred code start. The result of this evaluation is:
aaa
The macro $output
gets undefined before the evaluation of each deferred code. If we extend the previous example and define the output in one deferred code and try to use that in the next one it will still be undefined.
{@defer {#define $output=this will not survive{?$output}}}\
{@defer {#define $output=aaa{?$output}}}\
{#define $output=this also will not survive}
Annoying?
The output is still:
aaa
The last example shows that other macros survive and can be used in subsequent deferred actions. If the macro doplikate
is defined in a deferred action then the subsequent deferred actions can use the macro:
{@defer {#define $output=|{$input}|}}\
{@defer {@define ~ doplikate(a)=a/a}}\
{@defer {#define $output={doplikate {$input}}}}\
wuff
And the output is:
|wuff|/|wuff|
env
viii. since 1.3.0
env
returns the value of an environment variable. The macro can also be used to test that an environment variable exists or not. If the argument to the macro is the name of an environment variable then the result will be the value of the variable. If the variable is not defined then the macro will result empty string.
{@env JAVA_HOME}
is
/Library/Java/JavaVirtualMachines/jdk-14.jdk/Contents/Home
on the machine where the original README.adoc.jam
file was converted to ASCIIDOC.
If there is a ?
after the name of the variable name then the macro will result either the true
or false
. This can be used to test that an environment variable exists or not. Testing the value of the environment variable in an {@if … }
macro may be misleading when the value is literal false
or an empty string.
Starting with Jamal 1.9.0 it is possible to use !
after the name of the variable. In this case the macro will throw exception when the environment variable is not defined.
import
ix. since 1.0.0 (core)
import
opens a file and reads the content of the file and interprets it as Jamal macro file. Anything defined in that file will be imported into the scope of the current file. If the macro opening and closing strings are redefined using the sep
macro it will change for the file that imported the other file. Any user-defined macros defined in the top-level scope of the file will be available in the importing file.
Note that the top-level scope of the file may not be the same as the global scope. If the importing happens
-
from an included file, or
-
from inside a block of from inside a macro, or
-
in scope that was started with a
begin
macro
then the "top-level-scope of the file" is the one, that contains the import
macro. If anything is defined into the global scope in the imported file then those macros will eventually be in the global scope and available to anyone later.
The output that the processing of the imported file generates is discarded.
The syntax of the command is
{@import file_name}
The name of the file can be absolute, or it can be relative to the file that imports the other file. Any file name starting with the letters res:
are considered to be resource files in Java. This makes it possible to load macros that are provided with JAR libraries and are on the classpath. Any file name starting with the letters https:
are downloaded from the net.
The option [top]
can be used along with the import
. In this case a relative file name is relative to the main file that imports the other files. It is not possible to to step one or a few levels up in the import hierarchy. The only two possibilities are to import as file relative to the current one or the top level one.
Note, however, that using the option [top]
does not change the scope of the imports. The definitions will be exported to the importing scope. This option only changes the base directory for the file name calculation.
Use import
to import user-defined macro definitions.
Because the textual output from the evaluation of the file is discarded feel free to use text in the file to be imported as documentation. There is no need to enclose such a text into a {@comment …}
macro.
Starting with version 1.5.0 the import macro looks into the file before evaluating it. If the very first two characters in the file are {@
then it evaluates the content using {
as macro opening string and }
as macro closing string. This way you can freely import resource files provided in JAR file or through the net even if you use different macro opening and closing strings. Such import files cannot redefine the macro opening and closing string unless file importing also uses {
and }
.
include
x. -
since 1.0.0 (core)
-
since 1.7.3 verbatim include
include
reads a file similarly to import
, but it starts a new scope for the processing of the included file, and it also results the content of the file included into the main file.
Use include
to get the content of a file into the main output.
The file included can define user-defined macros. These macros are available only inside the included file unless they are exported. The included file may redefine the macro opening and closing string, but this works only in the included file only. The file that includes the other file is not affected by the redefinition of the macro opening and closing string.
The macro itself is replaced by the output generated by the processing of the included file.
The syntax of the command is
{@include file_name}
or (starting with 1.7.3)
{@include [verbatim] file_name}
If the option verbatim
is specified then the file is inserted into the output as it is without processing. This option can also be used in the form
{@include [includeVerbatim] file_name}
This version of this option name can also used as an option set using {@options includeVerbatim}
. In that case all includes will be executed without processing the read file where and when the option includeVerbatim
is true
.
Note that the macro include
is NOT inner scope dependent. It means that {#include {@options includeVerbatim} …}
will not work. The options set inside the include
macro have no effect when the include
macro is executed.
The option set outside, like {@options includeVerbatim}{#include …}
will work. However it will change the behaviour of all include
macros executing later, while the option is in effect.
The name of the file can be absolute, or it can be relative to the file that includes the other file. Any file name starting with the letters res:
are considered to be resource files in Java. This makes it possible to load macros that are provided with JAR libraries and are on the classpath. Any file name starting with the letters https:
are downloaded from the net.
The option [top]
can be used along with the include
. In this case a relative file name is relative to the main file that includes the other files. It is not possible to to step one or a few levels up in the include hierarchy. The only two possibilities are to include as file relative to the current one or the top level one.
The number of includes are limited to 100 in depth. A file can include another, which can again include another and so on, but only to the maximum depth of 100. This depth limit is set because an included file can be included many times. It is possible to implement recursion including of files. If the recursion does not end the include macros would drive the macro resolution into an infinite loop. This limit prevents this to happen.
The limit can be modified setting the environment variable JAMAL_INCLUDE_DEPTH
.
use
xi. since 1.0.0 since 1.7.4 can define an alias for an already loaded macro
use
declares a Java class as a built-in macro or defines an alias name for an already loaded built-in macro.
How macros are loaded
Built-in macros are classes that implement the javax0.jamal.api.Macro
interface. When they are registered as services, they are automatically loaded when any application embedding Jamal creates a new processor. In other words, the classes that implement some macros are automatically discovered if
-
they are in the
module-info
module descriptorprovides
directive and/or -
the fully qualified name of the class is listed in the JAR file in the
META-INF/services/javax0.jamal.api.Macro
file.
Some libraries contain javax0.jamal.api.Macro
implementations that are not loaded by the service loader. These classes are not advertised in the module-info
file or in the META-INF
directory. To use these classes as built-in macros the macro use
has to be invoked.
Defining the use of a Java Class as a Macro
The use of the use
macro (sic) is the following:
{@use global javax0.jamal.scriptbasic.Basic as scriptbasic}
In this example, the class javax0.jamal.scriptbasic.Basic
implements a macro. The class has to be on the classpath, and it has to implement the interface javax0.jamal.api.Macro
. It will be defined and available as a globally available built-in macro under the alias scriptbasic
.
The keyword global
can be missing:
{@use javax0.jamal.scriptbasic.Basic as scriptbasic}
In this case, the macro will only be available in the current scope and will not be available as soon as the current scope is closed. Note that built-in macros cannot be exported. They can be declared either local for the current scope or global.
Usually, the alias part (the as scriptbasic
in the example above) can also be omitted:
{@use javax0.jamal.scriptbasic.Basic}
In such a case the macro will be registered with the name that the macro provides by itself as an identifier. The interface Macro
defines a method String getId()
that should return the identifier of the macro. The interface also provides a default implementation that returns the lower-case version of the class name (w/o the packages). If there is no defined alias following the as
keyword then the one returned by the macro implementation will be used.
It is recommended to use the alias in the Jamal source file. That way there is no ambiguity when reading the code what the name of the built-in macro is.
Defining the use of a Java Class as a Macro
The syntax of the command is the same to define an alias for an already loaded macro. If there is no .
dot character in the "klass name", then the command will know that it cannot be a class name. In that case it will look for an already loaded built-in macro with the given name and it will register it again with the new alias. Following this both names can refer to the same macro.
The alias will refer to the built-in macro, which is the closest reachable in the current scope. If the evaluation leaves the current scope, and the global
keyword was not used then the alias will also go out of the scope. It is independent of the macro itself. The macro may be reachable via the original name.
The alias will refer to the built-in macro, which is the closest reachable in the current scope even if the global
keyword is used. In this case the alias will be global, and the macro will be reachable via the alias even if the original name was not registered global and goes out of scope.
script
xii. since 1.0.0 (core)
The macro script
defines a user-defined macro that is interpreted as a script. The syntax of the command is
{@script/scripttype id(parameters)=body}
If script
is followed by /
character then the next identifier is the type of the script. If this is missing the default, JShell
is assumed. You can use any scripting language that implements the Java scripting API and the interpreter is available on the classpath.
The parameters are handled differently from the parameters of the user-defined macros defined using the define
built-in macro. In that case, the parameter strings are replaced by the actual value strings during evaluation. In this case, the parameters are used as global variable names. Using these names, the actual values are injected into the context of the script before evaluation.
This also implies that you do not have the total freedom of parameter names. For define
we can use any string as a parameter id so long as long it contains no ,
and no )
. In this case, you should care about the syntax of the scripting language used. The parameter names have to be valid identifiers in the scripting language as they are used as such.
The value injection converts the actual value of the parameter to script values. Because the parameters are injected into global variables Jamal performs some conversions. Without this, all the scripts that use some integer or floating-point calculation were supposed to convert them first from the string.
Therefore, Jamal tries to convert the actual parameters.
-
First it tries treating it as an integer. If it succeeds then the global variable having the name as the parameter will hold an integer value.
-
If the conversion to an integer does not work then it tries the same with double.
-
If that is also not feasible then it will check if the actual value is lower case
true
orfalse
. In this case the global variable of the script will be aBoolean
value. -
In any other case, the global variable will get the actual value as a string assigned to it.
The actual scripting implementation may not have Integer
, Double
or Boolean
type but there will be some script type corresponding.
The following sample shows a simple script that implements a looping construct using JavaScript. The source Jamal file:
{@script for(loopvar,start,end,text)=
var c = "";
for( var i = start ; i <= end ; i++ ){
c = c + text.replaceAll(loopvar, ""+i);
}
System.out.print(c);
}
{for%xxx%1%3%xxx. iterated
}
The output generated by the Jamal preprocessor:
1. iterated
2. iterated
3. iterated
Note that the JavaScript code itself contains the macro opening and closing strings. This does not do any harm so long as long these are in pairs. It is a better practice to change the separator characters to something that cannot appear in the body of the script macro.
Starting with version 1.3.0 Jamal support the JShell built-in scripting engine. You can define JShell
as script type. In this case the content will be passed to the Java built-in JShell engine. When the script is invoked the result of the macro will be the string that is printed by the JShell script. If this is empty then the value of the last Java shell snippet will be used. The argument names have to be valid Java identifiers. When the script is invoked they will be defined as String
, long
, double
or boolean
variables. They will get the actual values of the parameters. The type depends on the actual value. If the value string can be interpreted as a long
then it will be converted to long
. If the string is not a long, but can be converted to double
then the variable will be double
. If the string is either true
or false
case insensitive then the variable will be boolean
. In any other case the variable will be declared as String
.
In short, the arguments to a script
macro will be converted to the following types in this order, whichever first succeeds:
-
int
-
double
-
boolean
-
String
For more information and details see the section xiii. JShell
JShell
xiii. since 1.3.0 (core)
The Java built-in scripting engine JShell can be used to define macros. The macro script and the macro JShell
can be used to define JShell scripts.
The macro JShell
can be used to define methods, classes, variables and so on. The macro script is to define a script macro that later can be invoked like any other used defined script macro.
When the macro JShell
or script is used the result is empty string. When the script is invoked the output of the macro will be what the script prints out to the standard output.
The following example defines a global method, a script using the method and then it invokes the script.
{@JShell
void hello(){
System.out.println("Hello, " + world);
}
}{@script hello/JShell(world)=hello();}
{hello My Dear}
It simply prints
Hello, My Dear
The macro JShell
defines the method hello()
. The macro script
is a script macro that has one argument. Note that this argument is also the name of the global variable world
. This global variable is used in the JShell snippet defined above but this is not an argument to the method. When we use the line
{hello My Dear}
Jamal will invoke the JShell interpreter executing
String world = "My Dear";
first, and then
hello();
Since the method hello()
prints out to the standard output Hello, My Dear
this is the result of this macro.
If there is some error in the code of the snippet then Jamal will throw a BadSyntax
exception. In this exception the causing exception is included if there is any. This causing exception should give some clue to find out what the issue is. If that does not help then using the interactive JShell program should help.
Creating a JShell execution environment is expensive. To do that the Java starts a new JVM process for the JShell. Many Jamal macro processing do not need the extra JShell. It would slow down Jamal if we created the JShell process for each and every processor even when it is not needed. The JShell environment is created only when it is unavoidable. It is when the processing uses the first time a JShell type script. It not when the script is defined. It is when the defined script is used. In the above example the JShell interpreter is created when the {hello …}
macro is evaluated. Only at that point all the prior definitions that were defined in any {@JShell }
macro are fed into the JShell interpreter.
The consequence is that you do not need to worry about the performance when you design a macro library. The processed files can bravely import the macros even if they declare JShell usage. It will not slow down the processing creating a JShell engine, only when the JShell engine is needed.
Another important side effect of this optimization is that you will not get an error message for an erroneous {@JShell }
macro until the JShell interpreter is used. When you design a macro library it is not enough to import the library to discover possible errors in the JShell scrips. The scripts have to be used to manifest the error.
for
xiv. -
since 1.0.0 (core)
-
since 1.5.0 multi-argument
for
-
since 1.6.3 backtick string separator value list
-
since 1.7.3 options between
[
and]
-
since 1.7.8 option
evalist
The macro for
can be used to repeat the same text many times. This macro has two forms. The syntax of the macro is either
{@for variable in (a,b,c,d)= content to be repeated
containing variable}
or
{@for (v1,v2,v3) in (a|w|1,b|q|2,c|r|5,d|t|9)= content to be repeated
containing v1 v2 and v3}
The variable
or the multiple variables can be used in the content and will be replaced for each iteration with the respective element on the comma-separated list. When there are multiple variables then the sub-list of the values is separated using the |
character. Both the command and the |
character can be modified to use something else instead of these characters.
The list of the values can also be separated by other strings. If the macro $forsep
is defined, like in
{@define $forsep=\s+}
then the arguments will be separated by one or more spaces. The string between the (
and the )
will be split using the string defined in $forsep
as a regular expression.
Similarly, if the macro $forsubsep
is defined, like in
{@define $forsubsep=:}
then the values for the different variables will be separated by a semicolon.
Note that the macros $forsep
and $forsubsep
can also be defined inside the for
macro body in case the macro is used with the #
character at the start. In this case the definition of these macros is limited to the evaluation of this very for
macro.
Starting with version 1.7.3 you can also define these options locally using the format
{@for [options] x in (a,b,c)=...}
where the options can be
-
$forsep
,separator
to specify the separator regular expression -
$forsubsep
,subseparator
to specify the sub separator regular expression -
trimForValues
,trim
to trim off the sapces from the values -
skipForEmpty
,skipEmpty
to skip empty parameter list (see below) -
lenient
for lenient operation (see below) -
evaluateValueList
, orevalist
to instruct the loop that the list of the values between the(
and)
has to be evaluated.
For example the macros:
{#for $a in (a:b:c)={@define $forsep=:}a is $a
}{?$forsep}
will result
a is a
a is b
a is c
In this case the value of the macro $forsep
is effective inside the for
, but it is undefined outside. Another example:
{#for {@options trimForValues}{@define $forsep=:} $a in ( a : b :c )=a is $a
}
{@for [trim separator=":"] $a in ( a : b :c )=a is $a
}
will result the same output:
a is a
a is b
a is c
a is a
a is b
a is c
Note
|
Using the However, some built-in macros, like If the built-in macro is surrounded with an The macro Also, the second example shows that the same effect can also be reached using the macro options. Macro options are always between |
The number of the actual values separated by |
character should be the same as the number of the variables in the for loop. If this is not the case then the macro evaluation will throw a bad syntax exception. This can be suppressed with the option lenient
. If the option lenient
is used then extra values are ignored and missing values are presented as empty strings. Note that this same option controls how user defined macro arguments are paired to the parameters.
Starting with version 1.5.3 you can fine tune how a for
loop treats the empty elements. By default, the empty elements in a for loop value list represent empty strings. The loop body will be rendered with these values replacing the loop variable with an empty string. In a situation like that the use of the option lenient
is also a must if the loop has multiple variables. In that case the empty value will be split into a one, empty string value for the empty value in the loop and this has to be assigned to the multipled loop variables. For example
{#for (k,z) in ()=wukz}
will not work, because the empty string cannot be split into two strings (it results one empty string when it is split). On the other hand the following code will work
{#for (k,z) in ()=wukz{@options lenient}}
{@for [lenient] (k,z) in ()=wukz}
and it will result
wu
wu
as both k
and z
are empty strings. Here, you can see two versions. The first one is declaring the lenient
option inside the for
macro, the second one is using the option, which is more coincise and shorter.
This default behaviour can be altered using the option skipForEmpty
. If this option is used the for
loop will skip the empty values. The previous example with this option:
{#for (k,z) in ()=wukz{@options skipForEmpty}}\
{@for [skipEmpty] (k,z) in ()=wukz}\
will evaluate to an empty string. Also note that in this case there is no need to use the option lenient
. That is because the empty value is skipped and there is no issue splitting it up into a less number of values than the number of the loop variables.
The example above contains one loop value and that loop value is an empty string. There can be more than one empty values in a for loop and empty and non-empty values can be mixed. The option skipForEmpty
and the alias skipEmpty
works in any of those cases. For example:
{#for k in (,)=wuk{@options skipForEmpty}}\
{@for [skipEmpty] k in (,)=wuk}
will also result an empty string and
{#for k in (,k)=wuk{@options skipForEmpty}}
will result
wuk
Sometimes the values for the for
loop come from some macro. In that case the for
macro should start with the #
character, otherwise the macro will not be evaluated to the list of values. For example:
{@define list=x,y,z}{@for z in ({list})={@define z=zz}}{?x}{?y}{?z}
will result
{@define {list}={list}{list}}
That is because the content of the macro for
is not evaluated before the for
loop is executed because we used the @
character. The result of the for loop is not evaluated. We will have to attend to that, but first we have to solve the issue that the macro list
is not evaluated. To do that we need to use the #
character in front of the for
loop.
{@define list=x,y,z}{#for z in ({list})={@define z=zz}}{?x}{?y}{?z}
will result an empty string:
The reason is that the content of the for
macro is evaluated before executing the macro itself. That way the macro reference {list}
will become x,y,z
, but the same time the part, which is after the =
is also evaluated. The evaluation will define the macro z
to be zz
, but this macro is within the scope of the for
macro. As soon as the for
macro execution is finished the definition of z
is lost. What we want is to protect the body of the for
macro from evaluation before for
the macro is executed and we want it to execute after.
{@define list=x,y,z}{!#for z in ({list})={@ident {@define z=zz}}}{?x}{?y}{?z}
will result
xxyyzz
The macro {@ident …}
is evaluated and its result is the content of the macro and it is not evaluated further before the evaluation of the macro for
. The macro for
gets evaluated and then the output is evaluated because the macro is preceeded with the back-tick character, which is a shorthand for the core built-in macro eval
. This evaluation defines x
, y
and z
.
Because the case that we want to evaluate the list part of the for
loop but not the body part is so common there is an option that helps with this. The option evaluateValueList
(alias evalist
) instructs the macro for
to evaluate the value list before iterating through it.
{@define list=x,y,z}{!@for [evaluateValueList] z in ({list})={@define z=zz}}{?x}{?y}{?z}
will result
xxyyzz
We still need the !
character in front of the for
but we could get rid of the ident
macro and the extra level of nesting.
Sometimes you may need to do a for loop over values that contain the )
character. With the conventional form of the for
macro it was not possible, because the first )
character terminates the list of the values. Jamal 1.6.3 introduced a new, backward compatible format for the for
macro.
Instead of the (
and )
characters it is possible to use an arbitrary string to denote the end of the values. When the first character after the keyword in
(after optional spaces) is the backtick character then the string till the next backtick character will be used to denote the end of the values. The starting and ending backtick should also be part of the string closing the values.
For example the following
{@for x in `END`a),b),c),d)`END`=x }
will result
a) b) c) d)
Note that this alternative format can only be used for the values list and not for the variables. The variables of the for loop should always be listed between (
and )
characters.
if
xv. The if
macro makes it possible to evaluate the content conditionally. The syntax of the macro is:
{#if [options]/test/then content/else content}
Here we use /
as a separator character but this is not hardwired. The if
macro uses the Standard Built-In Macro Argument Splitting to parse the body of the macro.
The result of the evaluated macro will be the then content
when the test
is true, and the else content
otherwise.
When no options are specified the test
is true, if
-
it is the literal string
true
(case-insensitive), -
it is a signed or unsigned integer number, and the value is not zero,
-
it is any other string that contains at least one non-space character, except
-
when the
test
is the literal stringfalse
(case-insensitive).
The literal false
is false using any combination of upper and lower case letters with or without surrounding spaces.
The evaluation of the test string can be modified using options. There are 8 options.
The first three options are "boolean" options. It is enough to use their keyword between the [
and ]
. (See examples later.)
-
blank
will test true if the test string is blank, it is empty or contains spaces only. -
empty
will test true if the test string is zero length and does not contain even spaces. -
not
will negate the test result. -
or
can be used with the numerical options when more than one test is needed. When you specify more than oneequals
,lessThan
, orgreaterThan
option the test is true if any of the tests is true. This is the default behaviour, so this option is not needed. Settingor=false
has no effect and is not the same as using the optionand
. This option is included only to add readability if needed. -
and
after the evaluation of multiple tests. When this option is used the test is true if all of the tests are true. This option cannot be used together with the optionor
and is also needs multiple numeric options.
Note that the options and
and or
are simple boolean options. They can appear only once in the list of the macro options. You cannot write {@if [equals=3 or equals=4 or equals=6] /9/a/b}
. It is recommended to use the or
or the and
option following the first numeric option. For example, {@if [lessThan=7 and greaterThan=2] /6/it is in (2,7)/out of range}`. If you feel it is more readable you can put these options at any place in the list.
The following options will do numerical comparison. When any of them are used then the test string is converted to a number. If the test string is not a number an error will happen. These options are integer options, which means that you have to specify a number following them like lessThan=55
.
-
lessThan
(aliasesless
,smaller
,smallerThan
) is true if the number is less than the value. -
greaterThan
(aliasesgreater
,bigger
,biggerThan
,larger
,largerThan
) is true if the number is greater than the value. -
equals
(aliasesequal
,equalsTo
,equalTo
) is true if the number is equal to the value.
There is no separately "less than or equal" and "greater or equal" option. If, for example, you want to test that a number is greater than or equal to a certain value then you can use the greaterThan
and the equals
options together. An alternative is to use the lessThan
option along with the boolean not
option.
Note
|
The option
The only problem was that the
instead of To fix that we need to use the option
This will result the desired
|
The following examples show a few cases, as demonstrations:
{@if /1/true/false}=true non-zero integer
{@if /true/true/false}=true literal true
{@if /0/true/false}=false zero integer
{@if ::true:false}=false condition is empty string
{@if :false:true:false}=false literal false
{@if :FaLSe:true:false}=false literal false
{@if :avraka kedabra:true:false}=true condition is non-empty string
{@if/0/anything can come here}= 'else' part is missing, output is empty
{@if/+1/true}=true non-zero integer
{@if/-1/true}=true non-zero integer
{@if/0.000/true}=true non-empty string, floating points don't work
{@if [not blank]/false/true/false}=true true because 'false' is not blank
{@if [not empty]/false/true/false}=true true because 'false' is not empty
{@if [not]/1/true/false}=false the option 'not' can be used solitary
{@if / /true/false}=false spaces mean false by default, but
{@if [not empty]/ /true/false}=true not empty
{@if [not blank]/ /true/false}=false is the same in this case as no option
{@if [empty]/ /true/false}=false no, not empty
{@if [not]/ /true/false}=true blank is false by default, but it is negated
{@if [blank]/ /true/false}=true is the same as the above in this case
{@if [lessThan=13]/12/true/false}=true 12 is really less than 13
{@if [lessThan=13]/13/true/false}=false 13 is not less than 13
{@if [lessThan=13 equals=13]/13/true/false}=true 13 is less than or equals 13, note the value twice
{@if [greaterThan=13 not]/13/true/false}=true 13 is not greater than 13, it is the same logic as the previous
{@if [lessThan=13 equals=14]/13/true/false}=false 13 is not less than 13 and does not equal 14
{@if [lessThan=13 and largerThan=2]/12/true/false}=true 12 is in the range (2,13)
Note
|
The above example is generated running the samples. The composition of the sample is somewhat complex. It uses sophisticated macros that heavily use the macro evaluation order. These macros also check that the 'if' macro really works the way it is supposed to. If you are interested in how it looks, check the file |
ident
xvi. since 1.0.0 (core)
ident
is an extremely simple macro. It just returns the body of the macro. The name stands for identity. It is helpful in some complex cases when you need to fine-tune the macro evaluation order. It is the case when Jamal should not evaluate some macro while it should others in a local scope. For example:
{@define b=92}{#define c={@ident {a}}{b}}{@define a=14}{c}
When we define the macro c
we do not want to evaluate {a}
. There are two reasons for this. One is that at that point, a
is not defined. The other is to use the actual definition of a
whenever the macro c
is used. On the other hand, we want to evaluate b
. This way, c
will become {a}92
. When later c
is used, and we already defined a
as 14
, then the final result will be 1492
.
1492
Note that c
is defined using the #
character before define
. At the same time, we used @
in front of ident
. Jamal evaluates the content of define
. In this evaluation {@ident {a}}
is evaluated and {b}
is also evaluated. {@ident {a}}
becomes {a}
. {b}
becomes '92`. This way c
will become {a}92
.
If we redefine later a
to some different value then c
will follow this change. If we redefine b
the value of c
will still remain 1492
assuming a
is still 14
.
You can also use this macro to enclose some text into a block where the definitions are local. For example, you may want to modify the macro start and end strings temporarily. In that case, you can use the sep
macro at the start and use the sep
macro without argument to reset the previous value. You can also enclose the setting of the macro start and end string into an ident
block.
Specific use of ident
is to insert a "null length separator" into the text. Imagine that the macro start and close strings should be and
. We may want to use those because the curly braces are used in the text frequently, and so are the single
(
and )
characters.
Note
|
Generally, it is not a good idea to use opening and closing strings that contain repeated characters. The reason for this is precisely the situation we describe in the example below. It isn’t easy to read the closing strings when there is more than one. For example, how many |
As an example, we may want to define a macro that creates a markdown image reference:
((@define image($ref)=![](images/$ref.png) ))
This example needs a space after the closing )
character at the end of the image url. If we did not have this space, the macro would be closed one )
sooner than needed. To avoid that, we insert an extra space after the image reference. Usually, it is not a problem. In some situations, however, we do not want to have that extra space there. It is possible using ident
.
((@define image($ref)=![](images/$ref.png)((@ident))))
The macro @ident
will prevent Jamal from interpreting the )
character after the .png
as the first character of a macro closing string. At the same time @ident
produces no character, not even a space in the output. You can also use the macros comment
and block
the same way.
Be aware that the macro ident
consumes the white spaces (including newlines) that follow the ident
keyword. It is to avoid extra white spaces when tabulation gives better readability. If you need the whitespace (e.g., newline) in the output, you can put those in the ident
macro.
Starting with Jama 1.5.0, there is a built-in language syntax similar to ident
. If a macro is preceded with a
`
backtick character, then the macro will not be evaluated. The above example, which is
{@define b=92}{#define c={@ident {a}}{b}}{@define a=14}{c}
can also be written as:
{@define b=92}{#define c={`a}{b}}{@define a=14}{c}
This built-in "ident" can be used many times if you want to postpone the macro evaluation multiple times. You can have
{``c}
or
{``````c}
as many times as it makes sense. You can use this macro modification character together with the !
character. There is no restriction on ordering the !
and the backtick characters in case they are used together. If you use many of them in extreme cases, you can mix them. Note, if the macro does not get evaluated fully, Jamal may not preserve the order of these characters in the output.
verbatim
xvii. since 1.0.0 (core)
verbatim
is a special macro, which affects macro evaluation order and is used for advanced macro evaluation. To understand what it does, we have to discuss first how Jamal evaluates the different macros.
Jamal parses the input from the start towards the end and copies the characters from the input to the output. Whenever, when it sees a macro then it evaluates the macro, and the result of the evaluation is copied to the output. This evaluation is done in three steps, two of those are recursive. Let’s have a simple example:
{@define a=this is it}{@define b={a}}{#define c={b}}{c}
The macro a
is defined simply. It is this is it
. Whenever a
is evaluated it will result the string this is it
.
The macro b
has the value {a}
. When macro b
is defined the content {a}
is not evaluated before the definition because there is a @
before the define
. When b
is evaluated it results {a}
and then before using this output in place of the use of the macro b
this result is evaluated by Jamal as a new input. This second recursive evaluation will result in the string this is it
.
The macro c
is defined using the #
character before the keyword define
, therefore Jamal will process the body of the macro before processing the built-in macro define
itself. Essentially, it will evaluate {b}
first. It will put the resulting characters after the =
sign in the definition of c
and then it will evaluate the define
built-in macro.
As we discussed above when this time {b}
is evaluated it results {a}
, which also gets evaluated and then it results this is it
. Therefore, the value of the macro c
is this is it
and that is what we see in the output:
this is it
This way the evaluation of a macro is done in three steps:
-
Evaluate the body of the macro unless the macro is built-in and starts with the character
@
. For this evaluation Jamal starts a new scope and evaluate the macros following these three steps. -
Evaluate the macro itself. If it is a built-in macro then it calls the
evaluate()
method of the Java class that implements the macro. If the macro is user defined then it evaluates as described in the sectiondefine
. -
If the macro is user-defined or starts with a
!
character then Jamal evaluates the output of the macro. If it contains macros then evaluate those using these three steps.
As you can see the first, and the last steps are recursive steps. The first step can be skipped using the @
character, but only in case of built-in macros. The second step cannot be skipped, and after all, there is no reason to do so. However, the third step can be
-
skipped using the macro
verbatim
if the macro is user defined, or -
enforced using a
!
in front of the@
or#
character if the macro is built-in.
The use of the !
character in front of a built-in macro is similar to the use of the macro eval
. For example
{@define tag(_x)={@define _x(_y)=<_x>_y</_x>}}
{#eval {@for _tag in (groupId,artifactId,version)=
{tag/_tag}}}
can be shortened as
{@define tag(_x)={@define _x(_y)=<_x>_y</_x>}}
{!@for _tag in (groupId,artifactId,version)=
{tag/_tag}}
The only difference is that the eval
macro consumes the white-space characters at the start of its argument. In the example above the {#eval macro …}
before its evaluation is
{#eval
{@define groupId(_y)=<groupId></groupId>}
{@define artifactId(_y)=<artifactId></artifactId>}
{@define version(_y)=<version></version>}}
The body starts with a new line. The macro eval
deletes this new line, while using the !
in front of the macro does not.
The syntax of the verbatim
macro is the following:
{@verbatim userDefinedMacroUse}
The verbatim
macro has to be followed by a user defined macro use. If we modify the previous example to use verbatim
we can do it the following way:
{@define a=this is it}{@define b={a}}{#define c={@verbatim b}}{c} {@verbatim c}
In this example {@verbatim b}
is the same as {b}
in the previous example. The only exception is that after b
is evaluated the result is not processed further for macros. It is used directly as the value of the new macro c
because of the verbatim
keyword. The value of c
will be {a}
. Also, when we use {c}
the result of c
is scanned as a third step for further macros. In this case, there is one because the value of the macro c
is {a}
, that further evaluates to this is it
. On the other hand when we use {@verbatim c}
then the result {a}
is not processed any further.
this is it {a}
Note that the macro verbatim
is a special one because it is hardwired into the evaluation logic of Jamal and it is not a "real" built-in macro. In other words, if there are user-defined macros and built-in macros then verbatim
is one level deeper built-in than the other built-in macros. To understand this may be important if you want to write your own built-in macros as Java classes. You cannot "redefine" verbatim
.
You cannot use verbatim
together with the !
macro modifying character. Their meaning is exactly opposite.
Fine points of macro evaluation
Note
|
This section does not apply to any version prior 1.2.0 |
Recall the three steps of macro evaluation:
-
Evaluate the body of the macro unless the macro is built-in and starts with the character
@
. For this evaluation Jamal starts a new scope and evaluate the macros following these three steps. -
Evaluate the macro itself. If it is a built-in macro then it calls the
evaluate()
method of the Java class that implements the macro. If the macro is user defined then it evaluates as described in the sectiondefine
. -
If the macro is user-defined or starts with a
!
character then Jamal evaluates the output of the macro. If it contains macros then evaluate those using these three steps.
These points can be refined further:
-
First the beginning of the macro text is evaluated if the text contains macros. The user-defined macro name itself in the text can be the result of another macro. For example, calling the macro named
white
can be{white}
. If there is another macro{@define black=white}
then using{{black}}
will result the same as{white}
. In this case first{black}
is evaluated towhite
and then{white}
is evaluated. There may be multiple macros at the start. For example, we can have{@define bla=whi}
and{@define ck=te}
. Using these we can get{{bla}{ck}}
to{white}
. -
The second step is that the content of the macro is split up into the macro name and the parameters. Recall that the first character that is not part of the name of the macro is used as a parameter separator character. This is a non-space character that cannot be part of a macro name, or the first character that follows the spaces after the macro name. The splitting process takes care of the macro calls that are in the arguments. For example the macro
{q/a/{b|c/g}}
will get two parameters. The first parameter toq
isa
, the second is{b|c/g}
. The first/
character separates the name of the macro from the parameters. At the same time, it defines which character is used as a separator character. The second/
character separates the first and second parameters. The third/
is not used as a separator character because it is inside a macro use. This character is not used as a separator character, even when the macro{b|c/g}
is evaluated, because in that macro use the separator character is|
. Similarly, if we look at the macro{q/a/{b/c}}
then the parameters area
and{b/c}
. In this case, the third/
is ignored and is not considered as a parameter separator. Although this character is a parameter separator when the macrob
is evaluated. The characters that are inside further macro calls are not used as parameter separators. -
When the parameter strings are identified then they are evaluated one after the other. In the previous example
a
and{b|c/g}
are evaluted beforeq
is evaluated. When the macroq
is evaluated, the parameters already contain the result of the evaluation of these macro uses.
The versions of Jamal prior 1.2.0 (so up to and including 1.1.0) evaluated user-defined macros simpler. In those versions the body of the macro was evaluated as a whole in one simple step. The parameter separator character was used in a very simple splitting operation. Those versions did not check if the separation character was inside an embedded macro use.
That way it may have happened that some macro was evaluated, and the resulting string contained the separator character. This is usually not what the users intend, and creates a bug that is hard to find. In the previous examples the evaluation of the macro use {q/a/{b/c}}
would evaluate first a/{b/c}
. After that, the splitting takes place on the resulting string.
Usually, this results in the same as the new algorithm. However, if the definition of b
is for example {@define b(Z)=shoot/Z}
, then the evaluated string will be a/shoot/c
. In this case the final evaluation will get (prior 1.2.0) {q/a/shoot/c}
. It will result in three parameters. This is probably an error because q
in the example needs only two. Even, if the option lenient
was declared the result is not the one the author of the text expected.
The versions 1.2.0 and later can revert to the earlier algorithm if the Jamal code defines the option omasalgotm
. Using the macro options
as {@options omasalgotm}
you can switch to the old algorithm. The name of the option is an abbreviation and is hard to remember to distract from the use of it. If you need this option then your Jamal source file does some shady thing that it should not. This option is obsolete from the very start of the introduction and is meant as a last resort to keep backward compatibility. It is removed from Jamal versions 1.10.0 and later.
sep
xviii. since 1.0.0 (core)
This macro can be used to change the macro opening and closing string. In the examples, in this documentation, we use {
as the opening string and }
as the closing string. Jamal itself does not impose any such predefined setting.
The syntax of the command is
{@sep /startString/endString}
If both the start and end strings are a single character, for example [
and ]
then you can use the simple form:
{@sep []}
A two-character argument to the macro sep
will use the first character as macro opening string and the second as macro closing string. You can also use three character. For example:
{@sep [.]}
The separating character between the opening and closing string characters can be any character except any of the opening or closing string character. It is also possible to use the format
{@sep openingString \s+ closingString}
separating the opening and closing strings with spaces. This format is very readable and convenient in many cases. For example, you can specify
{@sep (( )) }
{@sep ([ ]) }
and other, similar opening and closing strings. There are some definitions that can be misleading. For example, the following declarations can be interpreted by humans in multiple ways.
{@sep/[/ ] } <- is it "/[/" and "]" or "[" and "]"
{@sep/[ /]} <- is it "/[" and "/]" or "[" and "]"
Many human readers would tend to think the second. The syntax however matches the \S+\s+\S+
pattern. To avoid any such ambiguous situation Jamal does not allow the use of this form when
-
the opening string
-
starts and ends with the same character
-
is at least three characters long, and
-
it does not contain the first character inside
or
-
the closing string
-
starts with the same character as the opening string
-
at least two character long
-
does not contain this character after the first character.
These seem to be complex rules. They contain a bit of heuristics. They were designed to let the users use the most readable format of the sep
macro. The same time they help avoid unreadable declarations and errors.
If in doubt then you can always use the last, definitive syntax that does not rely on any heuristics. This syntax is described in the followings.
If the syntax does not match and of the previous cases, Jamal will use the syntax that is defined with the following "regular expression" like line:
{@sep \s* (\S) opening_string (\1) closing_string \s*}
There can be whitespace characters after the macro name sep
, and at the end, but these are optional. The first non-space character is used as a separator character that separates the macro opening string from the macro closing string. It is usually the /
character, but it can be anything that does not appear in the opening string. Prior to 1.3.0 this character could appear in the closing string, although it is not recommended. Starting with 1.3.0 it is an error. It is possible to use spaces inside the macro opening and closing strings, but it is not recommended. Leading and trailing spaces of the opening and closing strings will be trimmed off. That way
{@sep /[[/]]}
{@sep /[[ / ]]}
{@sep /[[ / ]] }
{@sep / [[ / ]] }
are all the same. Note though that {@sep / [[ /]]}
would be logical in the above list, but it is missing. There is only one space (\s+
) separator between the /
and /]]
strings, and it matches the
{@sep openingString \s+ closingString}
format, and it will set the separators to / [[
and /]]
.
Note that the macro sep
should be terminated with the original macro closing string. The macros after it already have to use the altered opening and closing strings. This makes it a bit tricky when you want to use a closing string that happens to contain the original closing string. Assume that the current opening string is {
and the current closing string is }
. You want to have {{
as an opening string and }}
as a closing string. This is often the choice when using Jamal in a programming language environment that heavily uses {
and`}` braces. In this case
{@sep/{{/}}}
will not work. It will set the closing string empty which is not valid and will raise an error. To overcome the situation you have to change the separator strings in two steps:
{@sep/[/]}[@sep/{{/}}]
Also, do not forget that the end you should call sep
without an argument twice:
{{@sep}}[@sep]
unless you want this change till the end of the scope.
The change of the opening and the closing strings always happens in pairs. You cannot change only the closing or only the opening string. You can, however, redefined one of them to be something that is different from the current value, and the other one to be the same as the current value. To do that you will need two steps for the reason described above. Even in this case, the definitions should specify both strings.
The change of the opening and closing strings is valid only for the current scope. Returning from the scope the original value is restored even if the strings were set to different values multiple times.
Neither the opening nor the closing string can be empty. Trying to set it to an empty string will raise an error.
Note
|
|
When the opening and the closing strings are set, the original values are stored in a list. It is possible to use the macro sep
without any separator string specification. In this case the macro call is nothing more than the macro name, like {@sep}
. In this case the last opening and closing strings are restored. The strings are stored in a stack, so you can define new strings and return to the previous one many times nesting the redefinitions.
The following sample is executed with {
and }
as opening and closing string at the beginning. After that, it sets the strings to [[
and ]]
. This is used to define the macro apple
. After this when the scope of the next macro, comment
starts the opening and closing strings are still [[
and ]]
. Starting a new scope does not change the macro opening and closing strings.
It would be an error to use [[@sep]]
inside the scope of the macro comment
at this point trying to restore the original macro opening and closing strings. In that scope at the start, there are no opening and closing strings to be restored. The opening and closing strings do not belong to this scope, they are simply inherited from the outer scope. On the other hand, the sample can change the strings, as it does to <<
and >>
. Using these it defines the macro z
. Note that z
is not exported from this scope.
After that the <<@sep>>
restores the opening and closing strings to the inherited one and with these, it defines a1
and a2
and exports them. Note, that a1
will have the actual value of the macro z
evaluated inside the scope of the comment
macro. The macro a2
starts with @
thus the body is not parsed during the macro definition and thus the value of a2
is unevaluated, as it is. Similarly, the macro
a3
will have the value`{z}`.
All these macros are evaluated because the macro comment
is started with the character #
. It means that Jamal will evaluate the body of the macro before evaluating the macro itself.
After the comment
macro the separators are set back to the original value {
and }
automatically. Then we have a simple macro definition that defines z
and then this z
is used, and the exported a1
, a2
, and a3
.
z
is now, as defined in the outer scope is SSS
. a1
has the value that came from the macro z
as it was defined inside the scope of the macro comment
. Macro a2
has the value that has nothing special in the current scope. The macro
a3
has the value {z}
which is evaluated after the macro a3
is replaced with its value.
{@sep/[[/]]}
[[@define apple=fruit]]
[[apple]]
[[#comment [[@sep/<</>>]]
<<@define z=zazi>>
<<#sep>>
[[#define a1=[[z]]]]
[[@define a2=[[z]]]]
[[@define a3={z}]]
[[@export a1,a2,a3]]
]]
[[@sep]]
{@define z=SSS}
{z}{a1}{a2}{a3}{@verbatim a3}
fruit
SSSzaziSSS{z}{@escape `a`{`a`}z{@escape `a`}`a`}
export
xix. since 1.0.0 (core)
export
moves the definition of one or more user-defined macros to a higher scope.
The syntax of the macro is
{@export macroname,macroname, ... ,macroname}
exporting one or more macros, comma separated.
When a macro is defined it is defined in the current scope (unless the name contains one or more :
, or it starts with :
).
The Jamal input file is one scope and if there is a macro defined in the file on the top-level then that macro can be used anywhere inside the file. However, when Jamal includes a file into another it opens a new scope. The macro include
should include some text in the output. It can be used, for example, to split up a long document into chapters and then use Jamal to create the final output. In that case, the macros defined in the included files should not interfere with the definitions in the file that includes the other one. To accomplish this separation Jamal starts a new scope when it includes a file. Scopes are embedded into each other like a call stack in a programming languages. When a macro is defined in scope it is available in that scope and all other scopes that are opened from that scope. When a macro is redefined in a scope the redefined value is used until the scope is closed. In the case of an included file, the user-defined macros defined in the included file disappear as soon as the included file processing is finished.
The setting and resetting of the separator characters is also limited to the scope. You cannot reset the separator character to a value that was set in a lower, or higher scope.
Jamal opens a new scope in the following cases:
-
When a file is processed with the
include
macro. -
When macros are evaluated inside another macro. This is the case of user-defined macros or in case of built-in macros when they are started with the character
#
. -
Other built-in macros that are not part of the core package may also open and close scopes. Built-in macros are provided in form of JAR files.
Note that the macro import
does NOT open a new scope to process the imported file. This is because of the aim of import
is to have the macros defined in the imported file available in the file that imports them.
In the following example, we define the macro Z
in the scope of the macro comment
. The {@define Z=13}
is evaluated before the comment
macro because we use the #
in front of the comment
macro. When the comment
evaluation finishes the scope is closed and Z
is not defined anymore. In the second case the macro Z
is exported using the export
macro. The export
macro moves the definition of the macro from the scope of the comment
to the enclosing scope.
The example:
A comment starts a new scope {#comment {@define Z=13}} Z {?Z} is not defined here unless...
{#comment {@define Z=14}{@export Z}}Z is exported. In that case Z is {Z}.
will result:
A comment starts a new scope Z 1 is not defined here unless...
Z is exported. In that case Z is 14.
You cannot export a macro defined in a higher scope. You can use those macros and you can reference them. It is just that you cannot export them to the enclosing scope because they do not belong to the current scope. You can export a macro that was defined in a lower scope and was exported to the current scope. However, you cannot export a macro that was defined in a lower scope but was not exported to the current scope, simply because they do not exist anymore when the export is executed. You cannot export macros from the top-level scope, because there is no enclosing scope above that.
options
xx. since 1.0.3 (core)
The options macro can be used to alter the behavior of Jamal. The options can be listed |
separated as an argument to the macro.
{@options lenient|failfast|mySpecialOption}
The macro does not check the option’s name. For example the option lenient
is used by Jamal itself and by the for
macro, however, if you type {@options lenuent}
misspelled the options
macro will not recognize it as an error. The option lenuent
could be used by some other macros and the options
macro just treats it as a new option. It stores the options specified, and they can be queried by any other built-in macros. Any extension can define and use any options it likes.
The scope of the options is local, or global the same way as the scope of user-defined macros.
Note
|
Technically the options are stored along the user-defined macros. These objects cannot be evaluated only queried for their stored value, which is either |
{@define macro($a,$b,$c)=$a is $a, $b is $b{#if :$c:, and $c is $c}}\
{macro :apple:pie:truffle}{@comment if there are three arguments, we handle it}
{macro :apple:pie:}{@comment here we need : at end, default is not lenient}
{#ident {@options lenient}{macro :apple:pie:}}{@comment options is local, but lenient is a global option}
{macro :apple:pie:}{@comment here we must have the trailing : because we still do not have a globally defined option options is local}
{#ident {#ident {@options lenient}{macro :apple:pie:}{@export lenient}}{@comment local but gets exported one level up, still not global}
{macro :apple:pie:}{@comment still not global}}
{macro :apple:pie:}{@comment was not exported to this level, only to inside the outer ident block}
{@options lenient}{@comment now this is on the global level}{macro :apple:pie}{@comment nice and easy, global}
{@options ~lenient}{@comment and we can switch it off}
{macro :apple:pie:}
{@options any|option|can | go | ~go | no go}
An option can be switched off using the ~
character in front of the options name. There can be no space between the ~
character and the name of the option.
Similar to user defined macros, options containing a :
are global. You can define a global value for an option using the :
prefix in front of the name of the option. This character will be removed from the name, the same way as it is removed from the name of global user defined macros. If the :
is inside the name then it remains part of the name, and it is not possible to have a local definition for the option.
The options implemented currently:
:lenient
In the lenient mode, the number of the arguments to a user-defined macro do not need to be exactly the same as it is defined. If there are fewer values provided then the rest of the arguments will be an empty string in the lenient mode. Similarly, if there are more arguments than needed the extra arguments will be ignored. The option lenient
is global. Nothing will stop you to redefined the option in a local scope, but macro evaluation will use the global value even in that scope.
The lenient mode also applies to the multi variable for loops. In lenient mode there may be more or less actual values than the number of loop variables.
omasalgotm
(since 1.2.0 < 1.10.0)
Jamal 1.2.0 changed a lot from 1.0.0 in the way how macros are evaluated. The version 1.2.0 is safer and more flexible and is compatible with the older versions in most of the cases. There may be some cases when the macros are not compatible with the old version. In this case, it is recommended to alter the macros so that they do not rely on the old evaluation algorithm. In the meantime, it is possible to use the option omasalgotm
to force Jamal to the old evaluation style.
Version 1.10.0 and later versions do not implement this option. The code providing compatibility with the old evaluation style is not included in the distribution.
nl
(since 1.3.0)
-
1.3.0 till 1.7.6 introduces the option
nl
. -
1.7.7 removes this option
When this option is in effect then all new-line characters are copied into the output. This was the default and non-changeable behavior prior 1.3.0.
In versions 1.3.0 and later it is possible to escape a newline character that is following a macro closing string. For example the macro {@define z=1}
can be followed by a \
character before the newline. That way {@define z=1}\
will tell Jamal that the next newline character is not needed in the output. The backslash, the newline character following it and the spaces that may be between the two will be skipped.
The \
character has to follow the macro closing string immediately, spaces are not allowed. There can be spaces between the \
character and the following new-line character.
{@define z=1}\n <- new line will get into the output
{@define z=1}\\n <- the \ and new-line will be skipped, it does not get into the output
{@define z=1}\ ... \n <- there can be spaces between the \ and the \n, still the
\ and new-line characters will be skipped
{@define z=1} ... \\n <- nothing is skipped, there are spaces before the \ character
A backslash in any other places is just a character and will not escape a newline. This escaping works only following built-in and user defined macros.
Note
|
Since this is a slight behavioral change in the input processing, therefore it may break some of the source files. We decided to change the default behavior because there is a little chance to have escaped new-line characters in existing jam files. On the other hand, we envision that with the introduction of this feature most of the Jamal source files will use this feature. We wanted to avoid starting every new Jamal source file with the nl option setting. |
With the release 1.7.7 this option is not available anymore. The default behavior, skipping new lines after a \
character that follows a macro close string cannot be switched off.
failfast
(since 1.7.8)
This option tells the Jamal processor to stop at the first error. With the version 1.7.8 and later the Jamal processor does not stop the processing at the first syntax error. This helps the discovery of all the syntax errors in the input. Prior 1.7.8 Jamal stopped at the first error. The user could fix the error, restart Jamal and repeat this process for each error one by one. The feature introduced in 1.7.8 collects all the errors and displays them at the end of the processing as an aggregate error.
Using this option Jamal 1.7.8 and later revert to the old behaviour.
try
xxi. since 1.5.0 (core)
The macro try will evaluate its content and return the result. The evaluation does not open a new scope, just like in the case of the macro {@eval }
. In case the evaluation results an error then the result will be empty string.
For example the following macro will produce an empty string.
{@try {!@verbatim macro}}
The macro macro
is not defined, but the error is caught with the macro try
.
The macro try
can also be used to include the error message into the output. If we use an !
character right after (no spaces) the try
keyword the result will be the error message. If there is no error then the result is the result of the evaluated text inside the macro.
Note
|
Jamal usually allows you to have spaces in places like the keyword try and the following ! or ? . For example you can have spaces between the macro name define and the ! , ? or ~ character. In case of try there must be no space. The reason for this strictness is that try is followed by arbitrary text evaluated by the macro itself. Allowing space between the macro name and the ! or ? character would result ambigous syntax. We could not tell if the ! or ? character is part of the macro use or already the first starting character of the text to be evaluated. |
If we use a ?
character right after (no spaces) the try
keyword then the result will be the string true
if there was no error and false
is there was an error. This can be used to test the "computability" of the text.
The macro try
should only be used to debug certain macro files. When an error happens, and the try
macro catches the exception thrown the scopes may not be properly closed.
escape
xxii. -
since 1.5.4 (core)
-
since 1.9.1 (
escape*
)
The built-in macro escape is a special one, like verbatim
. It is implemented as Java a macro class, but the macro evaluation process takes the presence of an escape
macro into account. The syntax of the macro is the following:
{@escape `SEP`escaped string`SEP`}
The part SEP
between the back-tick characters can be any string, which does not appear inside the escaped string. This string has to be repeated at the end of the macro before the macro end string.
The result of the macro is the escaped string without any modification. The macro itself is very simple. The speciality of macro is that Jamal takes care of this macro when searching for macros in the text. When Jamal sees a macro opening string, then it checks way before the evaluation of the macro if the actual macro is an escape
macro or not. It the macro is an escape macro, then it finds the SEP
strings, and the macro end string after that. If there is any macro opening or macro closing string inside the escape
macro they are ignored.
{@escape `a`{`a`}
will result
{
It is recommended not to use this macro in your Jamal source. If you can solve your task without this macro, then you should do it without such a strong tool.
Note
|
This macro originally was intended to be used by Jamal itself when evaluating a user defined macro, which was defined using different macro opening and closing strings than the actual one. In this case the macro opening and closing strings, which were in effect at the time of the macro definition are replaced with the current one. That way the macros defined inside the macros will be evaluated even though the macro opening and closing strings have changed. At the same time the current macro opening and closing strings had no special effect by the time of the macro definition. If there is any current macro opening and closing string in the definition of the macro then they should not play a special role. They get protected using the |
There is a special way to escape content from macro evaluation. In this special case you can write a *
right after the escape
keyword, as
{@escape* `a`{`a`}
In this case the escape not only escapes the macro opening and any other otherwise processable content but also results the protecting shell around the escaped string. The result in this case will be
{@escape*`a`{`a`}
Note
|
You will never see in your output the |
This macro comes in handy when you want to protect something from evaluation that should never be interpreted as a macro text. For example you can have a Maven property in a pom.jam
file, like ${project.build.sourceDirectory}
. You can redefine at the start of the file the macro open and macro close strings, but it may be simpler to protect the one or few special strings. A normal escape, without the *
after the keyword hides the content from evaluation only once. When the *
character is used the content will be protected even when it is deep inside macros and target for many different evaluations. It should only be eliminated at the very end of the processing on the top level, which is done automatically.
The macro escape*
will liberate the content. The liberation will happen after the whole content was already processed in a so called "closer". Built-in macros implemented in Java have the possibility to register an object to be executed after the processing of the whole content. Such an object is a closer and it can be used to close resources that were open by the macro during processing. During the execution of this closer the code can access and modify the final result.
The macro escape*
registers a closer that will invoke the the Jamal processing again for the output with a flag that tells every escape*
macro to release its content.
In some cases it may happen that you want the escape*
closer run before some other closers, but the escape*
happens later than the register of the other closer.
The different closers are invoked in the order they were registered. You may use a macro X
, which also creates a closer. The use of the macro X
precedes the first evaluation of an escape*
macro. In this case the closer registered by X
would be evaluated before the escape*
closer. If you want the escape*
closer to run first, you have to use the macro escape*
before X
. The simplest form is
{@escape*````}
It essentially escapes an empty string delimited by two backtick delimited empty strings. (Hence the four back ticks.) After the content liberation the result will be an empty string, thus there is no harm using this before any X
macro.
The closer registered by escape*
works very simple. It simply evaluates the result setting a flag that tells escape*
that this time it should ignore the \*
character.
require
xxiii. since 1.6.4
Since Jamal has many different versions, there may be a need to stick to a specific version. To do that, the built-in macro introduced in version 1.6.4 require
can be used. The syntax of the macro is
{@require [<|>|<=|>=|=] version }
The evaluation of this built-in macro will check that the currently running version of Jamal is
-
less-than
-
greater-than
-
lees-then or equal
-
greater than or equal, or
-
equal
to/than the version specified after the comparison sign. The use of the comparison sign is optional. The default comparison is greater than or equal.
If the comparison fails, the evaluation of the macro will result in an error. If the current version matches the requirement, then the result of the macro is an empty string.
For example:
{@try!{@require 6666.5.3}}
will result
The current version 1.10.4 is older than the required version. It has to be newer.
You can specify only one version in a require
macro. If you want to specify a minimum and a maximum version, you should use two require
macros.
Using a version before 1.6.4
in the require
argument is an error.
4. Standard Built-In Macro Argument Splitting
The built-in macros access their input as one single string. Technically the input parameter of the method evaluate()
is not a string but for now we can assume it is something like a string. The Java code of the macro is free to interpret this string the way it wants. Different macros implement different syntax analysis and later their behaviour based on the syntax of the input.
To manage the input and to easy the format analysis and interpretation of the input there is a utility class named InputHandler
. This class defines a method named getParts()
which does a simple analysis. It splits up the input into an array of strings in a "standard" way.
This method is used, for example, by the implementation of the if
built-in macro. When there is no special requirement for a macro it is recommended to use this method. Using this method provides a concise way for macro argument separation. The way it splits the arguments is defined here so that the extension documentations can refer to this section.
The splitting offers three syntax variations:
-
macroName / a / b / c / … /x
-
macroName a b c … x
-
macroName `regex` separator a separator b separator ... separator x
In the first case the argument separator character is the first special character. This character can be any unicode character except
-
letter or digit,
-
back-tick character,
-
white space character.
If the first non-white space character is a letter or digit character then the second syntax is used. In this case the input is split up along the white space characters. Multiple adjacent white space characters are counted as one. The splitting does not create empty parameters.
The third possibility is when the fist non-space character is backtick (`
). If the first non-whitespace character after the name of the macro id is a backtick then the parsing expects to be a regular expression till the next backtick. After the regular expression and after the closing backtick the rest of the input is spit up using the regular expression as separator.
Backtick was selected during the design of the syntax to enclose the regular expression because this character is very rare in Java regular expression. In case you need one inside the regular expression then you have to simply double it, and the parsing will single it back.
5. Standard Built-In Parameter Parsing
In addition to the method getParts()
there is another utility that the built-in macros can use. It is the class Params
. The class is a utility to parse some particular part of the whole input of the built-in macro looking for parameters. This particular part is usually the first line of the input, but it can be the part between (
and )
following the macro ID or the whole input. This utility is used by some of the built-in core macros. The core macros use the [
and ]
characters to enclose the parameters.
Note
|
The core macros cannot use the |
The documentation of the parameter handling in these macros is not part of this readme. It can be found in the separate PARAMS document.
6. Jamal Environment Variables
You do not need to configure Jamal. The environment variables that you may set to modify the behavior of Jamal are documented in this section. All environment variables start with the prefix “JAMAL_”. For every environment variable, there is a corresponding system property. The name of the system property is the same as the environment variable lower case converted and replacing the _
to .
characters. For example, for the environment variable JAMAL_CHECKSTATE
, the system property is jamal.checkstate
. First, the system property is consulted, and the environment variable has only effect when the system property is not defined. The following sections describe the individual environment variables.
JAMAL_CONNECT_TIMEOUT
This variable can define the connection timeout value for the web download in millisecond as unit.
The default value for the timeouts is 5000, meaning five seconds.
The proxy setting can be configured using standard Java system properties. For more information see the JavaDoc documentation of the class java.net.HttpURLConnection
in the JDK documentation.
JAMAL_READ_TIMEOUT
This variable can define the read timeout value for the web download in millisecond as unit.
The default value for the timeouts is 5000, meaning five seconds.
JAMAL_TRACE
This environment variable defines the name of the trace file. When a trace file is defined the evaluation and all the partial evaluations are appended to this file during processing. This file can grow very fast, and it is not purged or deleted by Jamal.
JAMAL_STACK_LIMIT
sets the recursive call depth in macro evaluation. Macros may be recursive and in some cases it may create infinite recursive calls in Jamal. Try a simple Jamal file that contains {@define a={a}}{a}
. This will drive Jama into an infinite recursive call. During the macro evaluation {a}
will result {a}
again and this will be evaluated again and again. Infinite recursive calls result StackOverflowError
which should not be caught by any program. To avoid this Jamal limits the recursive calls to the maximum depth 1000. This is a reasonable limit.
-
Most Jamal sources are not complex, and will not get above this limit recursively.
-
At the same time, most Java implementations can handle this dept.
This limit may be too much in your environment. Jamal may still throw StackOverflowError. In this case set this to a smaller value. It may also happen that you deliberately create complex recursive macros. In that case this limit may be too small. Set your value to a limit that fits your need.
JAMAL_CHECKSTATE
This environment variable can switch off macro statefulness checking during macro registration. It is generally recommended that the macros are stateless to support multi-thread evaluation when a single JVM runs multiple Jamal processors in one or more threads. If a macro has to have a state, it must be annotated using the annotation Macro.Stateful
. The statelessness or annotation is checked during macro registering since Jamal version 1.8.0. You can switch off the functionality setting this environment variable to false
. It may be needed if you want to use an older, prior 1.8.0 library or a library that does not follow this rule.
JAMAL_DEBUG
This environment variable can switch on debugging of Jamal. To use the debugger this variable has to set to a value, which is recognized by a debugger on the classpath. The web based debugger recognizes the http:port
format variables. Set this variable to http:8080
, put the jamal-debug
module on the classpath and after starting Jamal processing open your browser at `http://localhost:8080. The debugger and the use of it is detailed in a separate section.
JAMAL_INCLUDE_DEPTH
This variable can set the maximum number of file include nesting. The default value is 100.
JAMAL_HTTPS_CACHE
JAMAL_DEV_PATH
This environment variable can define replacements for files.
The aim of this feature is to use a local file during development, and still refer to it using the https://
URL, which will be the production URL. You want to run tests without pushing the file to a repository, but at the same time you do not want your code to refer to a dev location to be changed before releasing.
Only absolute file names can be replaced.
For example, you include the file https://raw.githubusercontent.com/central7/pom/1/pom.jim
in your Jamal file. You want to replace it with a local file ~/projects/jamal/pom.jim
. In that case you should set the environment variable
export JAMAL_DEV_PATH=\|https://raw.githubusercontent.com/central7/pom/main/pom.jim?SNAPSHOT=~/github/jamal/pom.jim
The environment value is a list of =
separated pairs. The list is parsed using the standard InputHandler.getParts(Input)
method. This is the reason why the first character in the example is the separator |
JAMAL_OPTIONS
This environment variable can define options for the Jama processor. The value of the variable is interpreted as a multi-part input. The list is parsed using the standard InputHandler.getParts(Input)
method. If you just have one option then you can define that with the name. If there are multiple options then you have to select a non-alphanumeric separator character and present it in front of the list.
Note
|
that the usual | character has a special meaning for the bash, and therefore you may need escaping. Also note that using : as a separator character may work, but it may be misleading as it can also be part of an option name. |
The options are set on the top level, there is no need to use a :
prefix. To set an option to false
, you can use the ~
character, but please do not. Every option default value is false
when not set.
The typical use of this possibility is to set the option failfast
. This option alters the error processing, and it is more "bound" to the execution than to the document. It may be a better option to include it in an environment variable, or system property than in the document itself. Both approaches work.
7. Resource Files and Web Resources
When the macros import
or include
reference a file with a name that starts with either
-
res:
, or -
https:
then these files are treated in a special way. In any other case the files are loaded from the local disk. The following two subsections detail the mechanism of these two cases.
Java Resource Files
When the file name starts with the characters res:
it is a Java resource file. It means that the file is in a JAR file among the classes. The JAR file has to be on the classpath. When Jamal is started from the command line then the JAR file has to be added to the classpath. The classpath is usually after the -cp
or -classpath
argument of the Java command line. If Jamal is started as a Maven plugin then the configuration in the pom.xml
file should include the dependency. For example to add the pomlib library JAR to the classpath you can use the following fragment in your pom.xml
:
<plugin>
<groupId>com.javax0.jamal</groupId>
<artifactId>jamal-maven-plugin</artifactId>
<version>1.10.4</version>
<executions>
<execution>
<id>execution</id>
<phase>clean</phase>
<goals>
<goal>jamal</goal>
</goals>
<configuration>
... configuration tags ...
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>com.javax0.jamal</groupId>
<artifactId>jamal-pomlib</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
</plugin>
Note
|
The module jamal-pomlib was discontinued and is not part of the current Jamal library structure. |
Web Resources
Web resources can be downloaded using the https:
prefix. The only protocol supported is https
. Jamal does not download any resource using the unencrypted HTTP
protocol.
It is possible to cache the downloaded files. The environment variable JAMAL_HTTPS_CACHE
can define a directory to store the web resources. In case the environment variable is not defined then the default value ~/.jamal/cache/
will be used. If the cache directory exists Jamal will store there the downloaded files. Jamal will create the subdirectories if needed, but Jamal will never create the cache directory itself. If you do not want to use the caching then do not create the cache directory.
Jamal will not cache a downloaded files that has SNAPSHOT
in the URL (all capital letters). There is no cache eviction or expiration. You can find the files in the cache directory in subdirectories. You can also find there corresponding property files that contain information about the caching. These properties files are information purpose and Jamal does not use them at the moment.
The connection to the web can be configured if needed. The environment variables that can be used are the followings:
-
JAMAL_CONNECT_TIMEOUT
, and -
JAMAL_READ_TIMEOUT
can define two timeout values for the web download in millisecond as unit.
The default value for the timeouts is 5000, meaning five seconds.
The proxy setting can be configured using standard Java system properties. For more information see the JavaDoc documentation of the class java.net.HttpURLConnection
in the JDK documentation.
8. Error Messages
When there is a processing error then Jamal throws a Java exception. The message of the exception contains at the end the location of the error. Sometimes it is not simple to identify the location due to the string replacement nature of Jamal processing. To help locating the error as precise as possible the location is given as a series as triplets. Each triplet contains a file name, a line number and a column number. The location is given in the following format:
file/line:column
When a file includes or imports another file then the location of the file is given in the error message along with the location from where the file is included or imported. The format in this case is
file/line:column <<< file/line:column
If the include/import hierarchy is deeper then the location is also given in several levels. In some cases the hierarchy is created in the error message inside a single file to help locating the error.
9. Snippet Handling
Snipetts are text fragments from the source code or from other text files that are to be included into the documentation as samples. Jamal Snippet handling macros can automate the copiing of such lines, but they can do more. These macros can trim, line number, transform the source text before inserting into the output document.
The snippet handling macros are documented in the Snipped module readme. These macros include also date handling, run-time checking of existence of referenced files, directores, classes, methods and so on.
10. Groovy Integration
The Groovy language integration lets you include Groovy code fragments into your document. These scripts are executed during the file processing, and can programmatically calculate the macro outputs.
The integration and how to use the module macros are described in the Groovy readme.
11. Ruby Integration
The Ruby language integration lets you include Ruby code fragments into your document. These scripts are executed during the file processing, and can programmatically calculate the macro outputs.
The integration and how to use the module macros are described in the Ruby readme.
12. Yaml Integration
The Yaml module can be used to simplify the maintenance of Yaml files using macros and splitting up the Yaml files into smaller chunks. The documentation of the module is jamal-yaml/README.adoc[Yaml readme]
13. IO Module
The Io module can be used to write text into separate files during the processing of a Jamal input file. The documentation of the module is jamal-io/README.adoc[Io readme]
14. Jamal API
Embedding Jamal into an application is very simple. You need the Jamal libraries on your classpath. If you use Maven, you can simply have
<dependency>
<groupId>com.javax0.jamal</groupId>
<artifactId>jamal-engine</artifactId>
<version>1.10.4</version>
</dependency>
in your pom file.
The library jamal-engine
transitively depends on the other libraries that are needed (jamal-core
, jamal-api
and jamal-tools
).
You also have to specify that you use these modules (Java 9 and later) if your code uses modules.
module jamal.maven {
requires jamal.api;
requires jamal.tools;
requires jamal.engine;
}
The code invoking Jamal needs a processor that will process the input.
import javax0.jamal.engine.Processor;
var processor=new Processor(macroOpen,macroClose);
var result=processor.process(input);
The macroOpen
and macroClose
parameters are String
values. The parameter input
to the method process()
has to be an object that implements the javax0.jamal.api.Input
interface. The easiest way to do that is to use the readily available class javax0.jamal.tools.Input
.
You can see an example to create an Input
from an existing file in the jamal-maven-plugin
module. The method createInput()
reads a file, and then it creates a new input:
private Input createInput(Path inputFile) throws IOException {
try (final var lines = Files.lines(inputFile)) {
final var fileContent = lines.collect(Collectors.joining("\n"));
return new javax0.jamal.tools.Input(fileContent, new Position(inputFile.toString(), 1));
}
}
An Input
holds the content the processor has to process. It also has a reference file name used to resolve the absolute names of the included and imported files. It also keeps track of the line number, and the column of the actual character as the macro evaluation progresses. A new Position(s,1)
creates a new position that identifies the file by the name’s` and the line number 1.
When a new processor is instantiated, it uses the ServiceLoader
mechanism to find all the built-in macros that are on the classpath. If your application has special macros implemented in Java then you can just put the library on the modulepath
. If the classes are defined in the provides
directive of the module then Jamal will find and load them automatically.
It is also possible to define user-defined and built-in macros via API. To do that you need access to the MacroRegister
object that the Processor
object has. To get that you can invoke the method getRegister()
on the processor object:
var register=processor.getRegister();
The register has API to define macros and user-defined macros. For further information see the API JavaDoc documentation.
There is a very simple API class that makes it possible to use Jamal as a templating engine. The utility class javax0.jamal.Format
has the method public static String format(String content, Map<String, String> predefinedMacros)
that can format the content
string using the entries of the predefinedMacros
as user-defined macros. These macros eventually cannot have arguments. This is a simplified interface to access the functionality of Jamal.
15. JavaDoc
The current and past versions of the JavaDoc can be read online at the address:
16. Maintenance of this document
The source of this document is README.adoc.jam
. The Jamal conversion uses the snippet macros and some aux built-in macros. The conversion is part of the execution of the tests. The code samples are automatically executed using this process, and the sample output is automatically inserted into the document.