edadma / texish   0.0.4

ISC License GitHub

A streaming TeX-inspired macro processor for Scala

Scala versions: 3.x
Scala.js versions: 1.x
Scala Native versions: 0.5

texish

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.

Features

  • 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

Installation

libraryDependencies += "io.github.edadma" %%% "texish" % "0.0.4"

Quick Start

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

Primitives

Macros

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}

Conditionals

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

Loops

\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

Arithmetic

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

Comparisons

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.

String Functions

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

Sequences

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

Maps

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

Special Characters

Character Meaning
\ Escape / command prefix
{ } Grouping / arguments
% Comment (to end of line)
~ Non-breaking space

Escape Sequences

Sequence Output
\{ {
\} }
\% %
\\ \
\~ ~

Templating

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, "

Custom Handler

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 commands

Custom Primitives

Register 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>"

Optional Parameters

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}")

Active Characters

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.

License

ISC