A streaming TeX-inspired macro processor for Scala. Unlike AST-based interpreters, texish processes tokens as they come, expanding macros immediately - similar to how TeX itself works.
- Streaming expansion: Macros expand during tokenization, avoiding timing issues
- Named parameters:
\def\greet name{Hello, \name!}instead of TeX's#1 - Cross-platform: Supports JVM, Scala.js, and Scala Native
- Extensible: Custom primitives and handler integration
libraryDependencies += "io.github.edadma" %%% "texish" % "0.0.4"import io.github.edadma.texish.*
val handler = new StringHandler
val proc = new Processor(handler)
proc.process("\\def\\greet name{Hello, \\name!}\\greet{World}")
println(handler.result) // "Hello, World!"| Command | Description | Example |
|---|---|---|
\def\name{body} |
Define a macro | \def\foo{bar} |
\def\name params{body} |
Define with parameters | \def\greet name{Hi \name} |
\gdef\name{body} |
Global definition | \gdef\version{1.0} |
\set\name{value} |
Set variable / copy definition | \set\x{42} |
\the\name |
Output variable value | \the\x |
\include{path} |
Include and process file | \include{macros.texish} |
| Command | Description | Example |
|---|---|---|
\if{cond}...\fi |
Conditional | \if{1}yes\fi |
\if{cond}...\else...\fi |
If-else | \if{0}yes\else no\fi |
\ifx\a\b...\fi |
Compare tokens | \ifx\foo\bar same\fi |
Truthy values: non-empty text, non-zero numbers, true
Falsy values: empty text, 0, false, nil, undefined
\for\i{\range{1}{5}}{\the\i, }
Loop metadata available via \forloop:
\forloop.index- 1-based index\forloop.indexz- 0-based index\forloop.first- true on first iteration\forloop.last- true on last iteration\forloop.length- total items\forloop.rindex- reverse index (1-based)\forloop.element- current element
| Command | Description | Example | Result |
|---|---|---|---|
\+{a}{b} |
Add / concatenate | \+{3}{4} |
7 |
\-{a}{b} |
Subtract | \-{10}{3} |
7 |
\*{a}{b} |
Multiply | \*{6}{7} |
42 |
\/{a}{b} |
Divide | \/{20}{4} |
5 |
| Command | Description |
|---|---|
\={a}{b} |
Equal |
\<{a}{b} |
Less than |
\>{a}{b} |
Greater than |
\<={a}{b} |
Less or equal |
\>={a}{b} |
Greater or equal |
\!={a}{b} |
Not equal |
Returns true or false.
| Command | Description | Example | Result |
|---|---|---|---|
\upcase{text} |
Uppercase | \upcase{hello} |
HELLO |
\downcase{text} |
Lowercase | \downcase{HELLO} |
hello |
\trim{text} |
Trim whitespace | \trim{ hi } |
hi |
\size{text} |
Length | \size{hello} |
5 |
| Command | Description | Example |
|---|---|---|
\seq{items} |
Create sequence from space-separated items | \seq{a b c} |
\range{start}{end} |
Create numeric range | \range{1}{5} |
\head{seq} |
First element | \head{hello} -> h |
\tail{seq} |
All but first | \tail{hello} |
\last{seq} |
Last element | \last{hello} -> o |
| Command | Description | Example |
|---|---|---|
\map{k1 v1 k2 v2} |
Create map from key-value pairs | \map{name Alice age 30} |
Access map values with dot notation: \user.name, \user.age
| Character | Meaning |
|---|---|
\ |
Escape / command prefix |
{ } |
Grouping / arguments |
% |
Comment (to end of line) |
~ |
Non-breaking space |
| Sequence | Output |
|---|---|
\{ |
{ |
\} |
} |
\% |
% |
\\ |
\ |
\~ |
~ |
Use texish as a template engine:
import io.github.edadma.texish.*
val result = Template.render(
"Hello \\the\\name! Items: \\for\\i{\\items}{\\the\\i, }",
Map(
"name" -> Value.Text("World"),
"items" -> Value.Seq(Vector(Value.Text("a"), Value.Text("b")))
)
)
// result: "Hello World! Items: a, b, "Implement the Handler trait to integrate with your application:
class MyHandler extends Handler:
def text(s: String): Unit = // handle text output
def space(): Unit = // handle space
def newline(): Unit = // handle newline
def get(name: String): Value = // get variable
def set(name: String, value: Value): Unit = // set variable
def enterScope(): Unit = // begin scope
def exitScope(): Unit = // end scope
def suppressOutput(suppress: Boolean): Unit = // control output
def command(name: String, args: Seq[Value], pos: CharReader): Value =
// handle unknown commandsRegister custom commands with full control over argument parsing:
import io.github.edadma.texish.*
val handler = new StringHandler
val proc = new Processor(handler)
proc.registerPrimitive("bold", new Primitive {
def execute(proc: Processor, pos: CharReader): Unit =
val bodyTokens = proc.readArgument(pos)
proc.handler.text("<b>")
proc.processTokenList(bodyTokens)
proc.handler.text("</b>")
})
proc.process("\\bold{hello}")
// Result: "<b>hello</b>"Primitives can read name:value parameters:
proc.registerPrimitive("box", new Primitive {
def execute(proc: Processor, pos: CharReader): Unit =
val opts = proc.readOptionalParams(pos) // e.g., width:100 height:50
val body = proc.readArgument(pos)
val w = opts.get("width").collect { case Value.Num(n) => n }.getOrElse(0)
val h = opts.get("height").collect { case Value.Num(n) => n }.getOrElse(0)
// ...
})
proc.process("\\box width:100 height:50 {content}")Register custom active characters that trigger special behavior:
val proc = new Processor(handler)
proc.registerActive('#', new Active {
def execute(proc: Processor, c: Char, pos: CharReader): Unit =
proc.handler.text("[HASH]")
})
proc.registerActive('&', new Active {
def execute(proc: Processor, c: Char, pos: CharReader): Unit =
proc.handler.text("[AMP]")
})
proc.process("a#b&c")
// Result: "a[HASH]b[AMP]c"Active characters take precedence over normal text processing. The default active character ~ produces a non-breaking space.
ISC