One wayto say it.

Topaz is a language for typesetting books and documents, and for many other programs besides. Name functions and variables in Korean, Russian, or English, then run the code on your computer or build it as a package for the browser. You can start in the playground without installing anything.

TOPAZ
import std.io

io.writeLine("Hello, Topaz!")
output
Hello, Topaz!

Printing to the screen also uses a standard module you import, so the code shows how a program talks to the outside world.

std.typeset

Typesetting in the standard library

std.typeset in Topaz 5.21 is the standard module that breaks paragraphs into lines. The program below sets one Korean sentence at a width of ten Hangul syllables. Looking at the whole paragraph keeps a lone word off the last line.

TOPAZ
import std.io
import std.typeset as ts

let sentence = "문단 전체를 보고 줄을 나누면 마지막 줄이 짧게 남지 않고 줄마다 길이가 고르게 됩니다."
let clusters = sentence.scalars().map(cluster)
let result = ts.classifyBreaks(clusters, koreanRules(clusters))
    .flatMap((breaks: Array<ts.BreakOpportunity>) => ts.breakParagraph(paragraph(clusters, breaks)))
match result {
    case Ok(plans) => {
        io.writeLine("Whole paragraph\n{linesOf(clusters, plans.optimized)}\n")
        io.writeLine("Line by line\n{linesOf(clusters, plans.greedy)}")
    }
    case Err(error) => io.writeLine("{error}")
}

The main part of the program. The full source adds the helpers that set character widths and build the paragraph.

output
Whole paragraph
문단 전체를 보고
줄을 나누면 마지막
줄이 짧게 남지
않고 줄마다 길이가
고르게 됩니다.

Line by line
문단 전체를 보고 줄을
나누면 마지막 줄이
짧게 남지 않고 줄마다
길이가 고르게
됩니다.

Paragraph breaking

Get the breaks chosen for the whole paragraph together with a greedy, line-by-line result. Choose the Knuth-Plass cost model or a fixed-point cost model.

Unicode line breaking

Find break opportunities with the Unicode 16.0 line breaking algorithm (UAX #14).

Rules for each locale

Add the Korean (KLReq), Japanese (JLReq), or Chinese (CLReq) rules, and an opening bracket never ends a line while a closing bracket or period never starts one. The Japanese rules follow the JLReq character classes.

East Asian width

The Unicode East Asian Width property decides breaks around quotation marks and brackets next to CJK text.

Spacing

Set how far justification may stretch or shrink the space between Latin words, and how far it may stretch the space between CJK characters.

Length units

Write lengths such as 12pt, 3mm, 1cm, 1in, and 1em directly in code and convert between units.

ATLAS

A typesetting tool built with Topaz

ATLAS turns a manuscript into print-ready book pages. Here is a page it set, with part of its line-breaking rules in Topaz. ATLAS names these functions with Korean typesetting terms, shown here in English.

Page 18 of a Korean book set with ATLAS
Page 18 of a book set with ATLAS
TOPAZ
function nextIsNotLatinSpace(next: Option<string>) -> bool {
  match next {
    case Some(text) => !isLatinSpace(text)
    case None => true
  }
}
function nextHasNoLineStartBan(next: Option<string>) -> bool {
  match next {
    case Some(text) => !hasLineStartBan(text)
    case None => true
  }
}

Part of the ATLAS line-breaking rules, with names translated into English for illustration

Why Topaz handles typesetting well

Typesetting words as names

Name functions and variables with the words of typesetting, such as badness, in Korean, Russian, or English. Topaz also keeps the characters of a manuscript exactly as entered.

Strings & Templates →

One rule for preview and output

Write a typesetting rule once and build the same source as a package for the browser and an executable for your computer. Both follow the same language rules, so the on-screen preview and the print output break lines by the same typesetting rules.

Execution Targets →

No silent arithmetic errors

An int is a signed 64-bit integer, and overflow stops the program at that point instead of continuing with a wrong value. When you measure line widths and layout costs in integers, an overflow cannot hide in the result.

Runtime Behavior →

Missing cases stop the build

Put a fixed set of cases, such as line-break classes, in an enum, and the compiler checks that each match handles every one. When you add a case later, the build reports each match that does not handle it yet.

Patterns & Control Flow →

Lispex

Topaz computes, Lispex decides

Computing such as line breaking happens in Topaz code. Decisions that change with each publisher and book, such as which page a chapter starts on or how many lines may stand alone, go into separate Lispex rules. The Lispex 1.21 evaluator built into Topaz 5.21 runs those rules within the limits set in a limits file. Change a rule file and lock the package again, and the same manuscript takes another style without any change to the application code.

  • Rules never touch files, the system clock, or the network.
  • A rule stops where it runs past its limit.
  • The same rule, input, and settings give the same decision every time.
LISPEX
(let ((min-lines 2))
  (if (< input min-lines) "next-page" "keep"))

All of rules/widow-lines.lspx

TOPAZ
import std.io
import src.values { singleDigit, textOf }
import std.lispex { defaultLimits, evaluate }
import std.lispex.rules as rules

export function main(args: Array<string>, stdin: string) -> Result<int, string> {
    let rule = rules.widow_lines()
    for linesLeft in [1, 2] {
        match evaluate(rule, singleDigit(linesLeft)?, defaultLimits(rule)) {
            case Ok(Complete(result)) => io.writeLine("lines left {linesLeft}: {textOf(result)}")
            case _ => return Err("the rule did not finish")
        }
    }
    Ok(0)
}
output
lines left 1: next-page
lines left 2: keep

All of src/main.tpz. singleDigit and textOf are small helpers in src/values.tpz that convert Lispex values.

Lispex rules run in the interpreter and in native builds.

Business rules and data processing, too

TOPAZ
import std.io

function discount(customer: { tier: string, years: int }) -> float {
    return match customer {
        case { tier: "vip", years } if years > 5 => 0.3
        case { tier: "vip" } => 0.2
        case { years } if years > 1 => 0.1
        case _ => 0.0
    }
}

io.writeLine("{discount({ tier: "vip", years: 7 })}")
io.writeLine("{discount({ tier: "new", years: 0 })}")
output
0.3
0.0
TOPAZ
import std.io

function clean(words: Array<string>) -> Array<string> {
    return words.filter((w: string) => w != "")
}

function shout(words: Array<string>) -> Array<string> {
    return words.map((w: string) => w + "!")
}

let line = ["one", "", "way", "", "to", "say", "it"]
    |> clean
    |> shout
io.writeLine("{line}")
output
[one!, way!, to!, say!, it!]

Run independent tasks together, and close files you are done with by a cleanup step at the end of the block.