Strings and Text
Introduction
Strings
A string is a sequence of characters such as "Hello, 🌍!" or "Simplify(👨🚀 × ⚡️) → 👨🎤".
A string is an indexed collection of characters. Its elements are the grapheme clusters of the string — what a reader perceives as individual characters — so a string can be counted, indexed (1-based, like every other indexed collection), iterated, and searched with the ordinary collection operators.
["Length", {str: "shop"}]
// ➔ 4
["At", {str: "abc"}, 2]
// ➔ "b"
["Contains", {str: "abc"}, {str: "b"}]
// ➔ "True"
In Epsil:
Length("shop") // ➔ 4
"abc"[2] // ➔ "b"
isDigit(c) = c in "0123456789" // a character-membership test
isDigit("7") // ➔ True
A grapheme cluster is not the same thing as a Unicode scalar (a code point), and neither is the same thing as a byte in an encoding. A string offers all three views; the collection view is the grapheme one, and the other two are reached with the explicit conversions described in Views of a string.
For example:
- The character
écan be represented as one Unicode scalar (U+00E9) or two scalars (U+0065+U+0301, i.e.e+ combining acute). Both are one grapheme cluster, and strings are normalized to Unicode NFC at construction, so both produce the same string value withLength1. - The emoji
👨🚀is one grapheme cluster made of three scalars ([U+1F468, U+200D, U+1F680]). In UTF-8 it is eleven bytes ([240, 159, 145, 168, 226, 128, 141, 240, 159, 154, 128]); in UTF-16 it is five code units ([55357, 56424, 8205, 55357, 56960]). ItsLengthis 1.
Membership is character membership, not substring search. c in s (that
is, ["Element", c, s], and equivalently Contains) asks whether c is one
of the string's characters, exactly as it does for a list. "ab" is a
substring of "abc", not an element of it:
["Contains", {str: "abc"}, {str: "b"}]
// ➔ "True"
["Contains", {str: "abc"}, {str: "ab"}]
// ➔ "False"
Substring search is a different operation and needs a different operator: the
contiguous-subsequence family RangeOf,
ContainsSequence, StartsWith and
EndsWith, which read their second argument as a sequence of
elements rather than as one element.
["ContainsSequence", {str: "abc"}, {str: "ab"}]
// ➔ "True"
Well-formedness. A native JavaScript string can hold an unpaired UTF-16
surrogate, on which segmentation, UTF-8 encoding and equality are undefined.
Every string entering the engine is scanned once and each unpaired surrogate
is replaced with U+FFFD (REPLACEMENT CHARACTER), so every string value is
well-formed Unicode and every operation on it is total.
The character type
A character is exactly one user-perceived character: one NFC-normalized
grapheme cluster. It is a scalar, alongside boolean and number.
character and string are disjoint siblings: a character is not a
one-element string, and a string is not a character. This is what makes a
character a leaf — it has no elements, so operations that descend into a
collection terminate on it structurally.
string is likewise not an alias for list<character>. The two are
siblings under indexed_collection<character>: same element type, same
iteration interface, neither a subtype of the other. They differ because
grapheme segmentation is not stable under concatenation — joining an e and
a lone combining acute produces the single character é, so a two-element
list of characters becomes a one-character string.
| Type | Relationship |
|---|---|
character | character <: scalar |
string | string <: indexed_collection<character> |
string vs scalar | not a subtype — a scalar is a boolean, a character or a number |
string vs list<character> | siblings; neither matches the other |
string vs character | disjoint; neither matches the other |
Getting a character. Use CharacterFrom, or let a
string literal narrow: in a position that expects a character, a literal
that is exactly one grapheme cluster becomes that character, and a literal
that is not is a type error. Narrowing applies to literals only — a
string-typed expression does not implicitly convert, so write
CharacterFrom(s).
Equality and ordering. Two characters are equal when their NFC scalar
sequences are identical. A character also compares equal to the one-character
string with the same content: this is a value law (equal scalar sequences
are equal values), and it is what makes c == "a", "a" in "abc" and
IndexOf("abc", "b") work without a conversion at every call site. The types
stay disjoint regardless. Ordering is by the code-point sequence of the
cluster; it is deliberately not locale-aware, and never will be — engine
identity, membership and hashing must be deterministic on every host.
Serialization. MathJSON has string literals but no character literal, so
a character's wire form is the call form ["CharacterFrom", "'x'"], which
canonicalizes back to the identical character:
["Characters", {str: "abc"}]
// ➔ ["List", ["CharacterFrom", "'a'"], ["CharacterFrom", "'b'"], ["CharacterFrom", "'c'"]]
A narrowed literal does not survive serialization as a character; the call form is the wire format.
Strings stay whole under broadcast and flattening
A string is an atom for the operations that spread over a collection:
-
Broadcast atomicity. A broadcasting operator applied to a string receives the whole string, not its characters — otherwise every such operator, including
Stringitself, would start mapping over graphemes, and a lambda with a scalar parameter would map instead of being applied.["String", {str: "ab"}, 1]// ➔ "ab1" (not ["ab1", "ab1"], and not ["a1", "b1"]) -
Flattenatomicity. Deep-descent walkers treat a string as a leaf:["Flatten", ["List", {str: "ab"}, {str: "cd"}]]// ➔ ["ab", "cd"]["Flatten", {str: "ab"}]// ➔ ["ab"]
Which operations return a string
Whether a collection operator gives back a string or a list follows from
what the operator does, not from a list of exceptions:
-
Element-preserving operators — those whose result is a subset or a reordering of the input's own characters — return a string for a string input:
Reverse,Rest,Most,Take,Drop,Slice,Unique,Sort,RotateLeft,RotateRight,Filter,TakeWhile,DropWhile,Dedup,DeleteAt,RandomShuffle,RandomSample.["DeleteAt", {str: "abcdef"}, 2]// ➔ "acdef"["RandomShuffle", {str: "abcdef"}]// ➔ "dbeafc" (a string, not a list of characters) -
Chunking and combinatorial operators — those whose result is a list of runs, each run being a contiguous stretch, a reordering or a subset of the input's own characters — return a
list<string>for a string input:Chunk,Partition(chunk, sliding-window and predicate forms),ChunkBy,SlidingWindow,Permutations,Combinations.Tallyis not one of these: its first component holds the distinct elements paired with their counts, so its values stay characters.["Chunk", {str: "abcdef"}, 2]// ➔ ["abc", "def"]["SlidingWindow", {str: "abcd"}, 2]// ➔ ["ab", "bc", "cd"]["Tally", {str: "banana"}]// ➔ (["b", "a", "n"], [1, 3, 2]) -
Element-transforming higher-order operators —
Map,FlatMap,Scan,Zip— return a list, always, even when the callback returns characters. There is no type-level rule for "this callback produces characters" worth its complexity; rejoin explicitly withStringorStringJoin.["Map", ["Function", "c", "c"], {str: "abc"}]// ➔ ["a", "b", "c"]["String", ["Map", ["Function", "c", "c"], {str: "abc"}]]// ➔ "abc" -
Operators whose result is not a collection of the source's kind —
Length,IsEmpty,Contains,Count,Any,All,IndexOf,At,First,Last,Position,Find,Reduce,Fold— behave on a string exactly as they do on any other indexed collection. -
Set operators read a string operand as its characters:
["Union", ["Set", 1], {str: "ab"}]isSet(1, "a", "b").SetMinusis the exception by design: its trailing operands name values to exclude, soSetMinus(S, "ab")removes the string"ab"fromS, not the charactersaandb. -
Numeric aggregators (
Sum,Product,Mean,Max,GCD, …) do not expand a string into characters. A character is not a number, so these either produce a typed error or stay symbolic — never a silent wrong answer.
The re-segmentation caveat
A string-preserving operator segments the string, operates on the characters, then joins and re-segments the result. Joining can merge adjacent characters, so the result may have a different character count than the input. Three consequences, all inherent to grapheme segmentation rather than defects:
String(Characters(s)) == s— always.Characters(String(cs))may have fewer elements thancs. Joining the charactereand the lone combining acute◌́— each its own cluster — yields the single characteré.- A string-preserving
Reversecan therefore change the character count: a combining mark that followed one base character can land next to another.
If you need the segmentation frozen — a list whose element count cannot
change under later operations — take it explicitly with
Characters.
Views of a string
The default view of a string is its characters. When a different decomposition is needed, ask for it explicitly:
| View | Operator | Element | When to use it |
|---|---|---|---|
| Characters | Characters (synonym GraphemeClusters) | character | The safest decomposition, and the one iteration and indexing already use. Use it to freeze the segmentation into a list. Not guaranteed stable across Unicode versions; the most expensive to compute. |
| Unicode scalars | UnicodeScalars | integer code point | Stable and fast. Use for code-point-level work. Not suitable for substring search or display. |
| UTF-8 | Utf8 | integer byte | Encoding-level work: byte buffers, I/O, hashing. |
| UTF-16 | Utf16 | integer code unit | Encoding-level work against a UTF-16 host. |
| Substrings | StringSplit | string | Splitting on whitespace or a separator. StringSplit(s, "") splits into one-character strings (not characters), the same segmentation Characters uses. |
Unicode scalars are not an encoding: one grapheme cluster may be several scalars, and each scalar maps to different byte sequences depending on the encoding.
Unicode-version stability
Grapheme cluster boundaries are defined by Unicode® Standard Annex #29 and can change when the host's Unicode data (ICU) is updated. Two consequences worth planning for:
- Character counts of exotic clusters can drift between hosts, so avoid pinning them in snapshot tests without a comment naming the Unicode version assumed.
- Literal narrowing is decided by the same segmenter, so an ICU upgrade that changes a literal's cluster count changes whether that source type-checks. Unicode scalars and their UTF-8/UTF-16/UTF-32 encodings are fixed and stable; only the grapheme view moves.
Compiling string operations
Grapheme segmentation is not available on every compilation target, so each target either implements a string operation grapheme-correctly or fails closed with a diagnostic — never by silently counting code units.
| Operation | JavaScript | Python | GLSL/WGSL |
|---|---|---|---|
Length(s), At(s, i) / s[i] | Compiled grapheme-aware (never the host .length) | Not supported | Not supported |
Iteration-derived (Map, Filter, Reduce, Any, All, Contains, IndexOf, … over a string) | Segmented, then the ordinary list lowering | Not supported | Not supported |
String-preserving operators (Reverse, Take, …) | Segment, operate, rejoin | Not supported | Not supported |
character values, String(c), ==, < | A one-character host string; equality is ===, ordering by code point | Not supported | Not supported |
CharacterFrom(x) for a non-literal x | Not supported (needs a runtime cluster count) | Not supported | Not supported |
Python has no grapheme segmentation in its standard library (len() counts
code points), so string collection operations report a target-capability
diagnostic rather than compiling to something subtly wrong. GLSL and WGSL
have no string support at all, and string-typed operands are rejected as
before.
Annotated Expressions
An annotated expression is an expression that carries additional visual or semantic metadata that is not material to the interpretation of an expression such as text color and size or other typographic variations, a tooltip or a hyperlink data to link to a web page.
While annotated expressions can be applied to strings, they can also be used to annotate mathematical expressions, such as variables, operators, or functions, to provide additional context or visual emphasis.
For example, an annotated expression can be used to highlight a specific part of a mathematical expression:
["Equal",
"circumference",
["Multiply", 2, ["Annotated", "Pi", {"dict": {"color": "blue"}}], "r"]
]
// ➔ Pi (in blue)
which would correspond to the LaTeX expression:
\mathrm{circumference} = 2 \cdot \textcolor{blue}{\pi} \cdot r
Annotated expressions are similar to attributed strings in other systems.
When \textcolor (or \color) wraps a bare operator rather than an operand
— for example x \textcolor{red}{=} y — the operator is parsed as usual
(["Equal", "x", "y"]) and the color is dropped: MathJSON has no way to attach
an Annotated wrapper to a lone operator glyph. Coloring an operand (such as
\textcolor{red}{x + 1}) still produces an Annotated expression.
Text Expressions
A ["Text"] expression is a sequence of strings, annotated expressions or
other ["Text"] expressions. It is used to represent formatted text content,
for example from a LaTeX expression like \text{Hello \mathbf{world}}.
What would happen if you used a string expression instead of a text expression?
The arguments of a ["String"] expression get converted to their string
representation, then joined together with no spaces. The text representation
of an annotated expression is the name of the expression, not its formatted
version. For example, ["Annotated", "world", {"dict": {"color": "blue"}}] would
be serialized to LaTeX as \mathrm{Annotated}(\text{world}, {color \to "blue"}), which is not what you want.
The arguments of a ["Text"] expression remain a sequence of elements. When
serialized to LaTeX, the elements are serialized to appropriate LaTeX commands
to preserve their formatting and structure.
const stringExpr = ce.expr([
"String",
"Hello",
["Annotated", "world", {dict: {"color": "blue"}}]
]);
console.info(stringExpr.latex);
// ➔ "\text{\mathrm{Hello}\textcolor{blue}{\mathrm{world}}}"
const textExpr = ce.expr([
"Text",
"Hello",
["Annotated", "world", {dict: {"color": "blue"}}]
]);
console.info(textExpr.latex);
// ➔ "\mathrm{Hello}\textcolor{blue}{\mathrm{world}}"
Functions
CharacterFrom(s: string) -> character
The character denoted by s. After NFC normalization, s must segment to exactly one grapheme cluster; an empty or multi-character string is an error value, never a silent truncation.
"One character" means one cluster, not one code point: a precomposed or
decomposed é, a ZWJ emoji sequence and a regional-indicator flag all
qualify.
["CharacterFrom", {str: "x"}]
// ➔ "x" (a character, not a string)
["CharacterFrom", {str: "👨👩👧"}]
// ➔ "👨👩👧" (one grapheme cluster)
["CharacterFrom", {str: "ab"}]
// ➔ Error: incompatible-type — expected character, got string
CharacterFrom is also the wire form of a character value: a character
serializes as ["CharacterFrom", "'x'"] and boxing that call form gives back
the identical character. CharacterFrom(String(c)) == c holds for every
character c, since one cluster always re-segments to itself.
See also: String, Characters.
String(any*) -> string
A string created by joining its arguments. The arguments are converted to their default string representation.
["String", {str: "Hello"}, {str: ", "}, {str: "🌍"}, {str: "!"}]
// ➔ "Hello, 🌍!"
["String", 42, {str: " is the answer"}]
// ➔ "42 is the answer"
Called with exactly one finite collection, String joins that
collection's elements instead of broadcasting over them. This is what makes
the round-trip law hold:
["String", ["Characters", {str: "abc"}]]
// ➔ "abc"
["String", ["CharacterFrom", "'x'"]]
// ➔ "x"
With more than one argument the ordinary coercing-join-with-broadcast semantics apply, and a string operand stays whole:
["String", {str: "x"}, ["List", 1, 2]]
// ➔ ["x1", "x2"]
["String", {str: "ab"}, 1]
// ➔ "ab1"
A single non-finite collection argument leaves the expression unevaluated.
Join(...strings: string) -> string
Join(...collection) -> collection
Join is variadic concatenation. When every argument is a string, the
result is the strings run together, as a string:
["Join", {str: "ab"}, {str: "cd"}]
// ➔ "abcd"
["Join", {str: "ab"}, {str: "cd"}, {str: "ef"}]
// ➔ "abcdef"
This is the same Join that concatenates any two collections (see the
collections reference); the
string result is an overload of it, not a separate operator. As soon as one
argument is not a string the generic arm applies and the result is a list of
the operands' elements — which, for a string operand, are its characters:
["Join", {str: "ab"}, ["Characters", {str: "cd"}]]
// ➔ ["a", "b", "c", "d"] (a list<character>)
Concatenation joins and re-segments, so the result can have fewer
characters than the operands together (see
the re-segmentation caveat): joining "e" and
a lone combining acute produces the single character "é".
In Epsil, string interpolation is the idiomatic concatenation of a few pieces:
"\(a)\(b)". It differs from Join in strictness — interpolation coerces
each hole to its default string representation, Join requires strings.
See also: StringJoin for joining a collection of
strings, optionally with a separator; String for a coercing join.
StringJoin(xs: collection<string | character>, separator: string?) -> string
StringJoin joins one collection, optionally inserting a separator
between consecutive elements. It is the counterpart of
StringSplit, and the counterpart of Python's
separator.join(parts).
["StringJoin", ["List", {str: "a"}, {str: "b"}, {str: "c"}]]
// ➔ "abc"
["StringJoin", ["List", {str: "a"}, {str: "b"}, {str: "c"}], {str: ", "}]
// ➔ "a, b, c"
["StringJoin", ["Characters", {str: "abc"}]]
// ➔ "abc"
An empty collection gives ""; a one-element collection gives that element.
Unlike String, which coerces any operand to its default string
representation, StringJoin is strict: an element that is neither a
string nor a character is rejected — as an incompatible-type error where the
operand's own type shows it, otherwise by leaving the expression unevaluated.
A non-finite collection also leaves the expression unevaluated.
["StringJoin", ["List"]]
// ➔ ""
["StringJoin", ["List", {str: "a"}, 1]]
// ➔ Error: incompatible-type — expected collection<character | string>
A string subject is a collection of its characters, so joining it with a
separator interleaves the separator between them — the same reading Python
gives sep.join(s):
["StringJoin", {str: "abc"}, {str: "-"}]
// ➔ "a-b-c"
A character is not a collection, so it must be wrapped:
["StringJoin", ["CharacterFrom", "'a'"]] is a type error; write
["String", ["CharacterFrom", "'a'"]] or put the character in a list.
StringJoin used to be variadic — StringJoin("ab", "cd") meant
"abcd". It no longer is, and because a string is now a collection of
characters, that same call is still accepted and now means something else:
"acdb" (the characters of "ab" joined with the separator "cd"). This
change is silent — no error, a different answer — so audit every
multi-argument StringJoin call.
Migration: for concatenation of a fixed number of strings use
Join, Join(a, b), or Epsil interpolation, "\(a)\(b)". Keep
StringJoin only where the subject really is a collection to be joined.
StringFrom(any, format:string?) -> string
Convert the argument to a string, using the specified format.
| format | Description |
|---|---|
| (omitted) | The argument's default string representation |
utf-8 | The argument is a collection of UTF-8 bytes |
utf-16 | The argument is a collection of UTF-16 code units |
unicode-scalars | The argument is a collection of Unicode scalars (same as UTF-32), or a single Unicode scalar |
The three explicit formats require a collection of integers (or, for
unicode-scalars, a single integer). A string argument is a type error:
a string is a collection of characters, not of code units, so decoding one as
bytes would be nonsense. Convert it with Utf8 /
Utf16 / UnicodeScalars first.
For example:
["StringFrom", ["List", 72, 101, 108, 108, 111], {str: "utf-8"}]
// ➔ "Hello"
["StringFrom", ["List", 55357, 56607], {str: "utf-16"}]
// ➔ "🔟"
["StringFrom", 128287, {str: "unicode-scalars"}]
// ➔ "🔟"
["StringFrom", ["List", 127467, 127479], {str: "unicode-scalars"}]
// ➔ "🇫🇷"
Utf8(string) -> list<integer>
Return a list of UTF-8 code points for the given string.
Note: The values returned are UTF-8 bytes, not Unicode scalar values.
["Utf8", {str: "Hello"}]
// ➔ ["List", 72, 101, 108, 108, 111]
["Utf8", {str: "👩🎓"}]
// ➔ ["List", 240, 159, 145, 169, 226, 128, 141, 240, 159, 142, 147]
To create a string from UTF-8 code points, use the ["StringFrom", _list_, "utf-8"] function.
See also: Utf16, UnicodeScalars and Characters.
Utf16(string) -> list<integer>
Return a list of UTF-16 code points for the given string.
Note: The values returned are UTF-16 code units, not Unicode scalar values.
["Utf16", {str: "Hello"}]
// ➔ ["List", 72, 101, 108, 108, 111]
["Utf16", {str: "👩🎓"}]
// ➔ ["List", 55357, 56425, 8205, 55356, 57235]
To create a string from UTF-16 code units, use the ["StringFrom", _list_, "utf-16"] function.
See also: Utf8, UnicodeScalars and Characters.
UnicodeScalars(string) -> list<integer>
A Unicode scalar is any valid Unicode code point, represented as a number
between U+0000 and U+10FFFF, excluding the surrogate range
(U+D800 to U+DFFF). In other words, Unicode scalars correspond exactly to
UTF-32 code units.
This function returns the sequence of Unicode scalars (code points) that make up the string. Note that some characters perceived as a single visual unit (grapheme clusters) may consist of multiple scalars. For example, the emoji 👩🚀 is a single grapheme but is composed of several scalars.
["UnicodeScalars", {str: "Hello"}]
// ➔ ["List", 72, 101, 108, 108, 111]
["UnicodeScalars", {str: "👩🎓"}]
// ➔ ["List", 128105, 8205, 127891]
To create a string from Unicode scalars, use the ["StringFrom", _list_, "unicode-scalars"] function.
See also: Utf8, Utf16, and Characters.
Characters(string) -> list<character>
A grapheme cluster is the smallest unit of text that a reader perceives as a single character. It may consist of one or more Unicode scalars (code points).
For example, the character é can be a single scalar (U+00E9) or a
sequence of scalars (e U+0065 + combining acute U+0301),
but both form a single grapheme cluster.
Here, NFC (Normalization Form C) refers to the precomposed form of characters, while NFD (Normalization Form D) refers to the decomposed form where combining marks are used.
Similarly, complex emojis (👩🚀, 🇫🇷) are grapheme clusters composed of multiple scalars.
The exact definition of grapheme clusters is determined by the Unicode Standard (UAX #29) and may evolve over time as new characters, scripts, or emoji sequences are introduced. In contrast, Unicode scalars and their UTF-8, UTF-16, or UTF-32 encodings are fixed and stable across Unicode versions.
The table below illustrates the difference between grapheme clusters and Unicode scalars:
| String | Grapheme Clusters | Unicode Scalars (Code Points) |
|---|---|---|
é (NFC) | ["é"] | [233] |
é (NFD) | ["é"] | [101, 769] |
👩🎓 | ["👩🎓"] | [128105, 8205, 127891] |
This function splits a string into a list of characters — the user-perceived characters of the string. It is the explicit projection that freezes the current segmentation into a genuine list:
["Characters", {str: "Hello"}]
// ➔ ["H", "e", "l", "l", "o"]
["Characters", {str: "👩🎓"}]
// ➔ ["👩🎓"]
["UnicodeScalars", {str: "👩🎓"}]
// ➔ ["List", 128105, 8205, 127891]
The elements are character values, so the MathJSON of the result uses the
CharacterFrom wire form:
["Characters", {str: "ab"}]
// ➔ ["List", ["CharacterFrom", "'a'"], ["CharacterFrom", "'b'"]]
Iterating a string directly gives the same characters, so Characters is
needed only when you want the list itself — for instance to keep the
segmentation from changing under a later join (see
the re-segmentation caveat).
String(Characters(s)) == s always holds. The converse does not:
Characters(String(cs)) may have fewer elements than cs.
For more details on how grapheme cluster boundaries are determined, see Unicode® Standard Annex #29.
Synonym: GraphemeClusters — the original name of this function, kept
for compatibility.
See also: CharacterFrom, StringSplit, Utf8, Utf16, and UnicodeScalars.
StringSplit(string) -> list<string>
StringSplit(string, separator:string) -> list<string>
Split a string into a list of substrings.
With no separator, the string is split on runs of whitespace, and empty
parts are dropped. Whitespace is defined as the code points with the Unicode
White_Space property (U+0009–U+000D, U+0020, U+0085, U+00A0,
U+1680, U+2000–U+200A, U+2028, U+2029, U+202F, U+205F,
U+3000) — the definition does not depend on the host's interpretation of
\s.
With a separator string, the string is split on each occurrence of the separator, and empty parts are kept.
["StringSplit", {str: "the quick brown fox"}]
// ➔ ["the", "quick", "brown", "fox"]
["StringSplit", {str: "a,b,,c"}, {str: ","}]
// ➔ ["a", "b", "", "c"]
An empty separator splits into grapheme clusters — the same
segmentation Characters uses — but the parts are
one-character strings, not character values:
["StringSplit", {str: "abc"}, {str: ""}]
// ➔ ["a", "b", "c"] (a list<string>)
To split into characters, use Characters, or simply
iterate or index the string.
See also: Characters.
Searching for a substring
Substring search is contiguous-subsequence search, and it is generic: the same four operators work on any indexed collection, and a string is just the case where the elements are characters. They are documented here because strings are where they are reached for most; the collections reference covers the list cases.
Two things distinguish this family from Contains
and IndexOf, which search
for one element: here the second argument is always read as a sequence
of elements, and matching is character-wise on both sides.
Character-wise matching is what makes the family grapheme-safe without a special rule: a needle can never match across a cluster boundary, because the comparison is between whole characters.
["RangeOf", {str: "x́y"}, {str: "x"}]
// ➔ "Nothing" — the subject's characters are [x́, y], and x ≠ x́
["RangeOf", {str: "👨👩👧"}, {str: "👩"}]
// ➔ "Nothing" — the subject is ONE character (a ZWJ family cluster)
["RangeOf", {str: "ée"}, {str: "e"}]
// ➔ ["Range", 2, 2] — the leading `e` is inside the `é` cluster
(The first example is x + U+0301 COMBINING ACUTE, which has no precomposed
NFC form; the third is e + U+0301, which does. A code-unit search would find
a match in all three.)
RangeOf(xs:indexed_collection, needle:indexed_collection) -> range | nothing
RangeOf(xs:indexed_collection, needle:indexed_collection, from:integer) -> range | nothing
The span of the first occurrence of needle as a contiguous subsequence
of xs, as a 1-based inclusive index range,
or Nothing when the needle does not occur.
["RangeOf", {str: "hello world"}, {str: "o w"}]
// ➔ ["Range", 5, 7]
["RangeOf", {str: "abc"}, {str: "b"}]
// ➔ ["Range", 2, 2]
["RangeOf", {str: "abc"}, {str: "z"}]
// ➔ "Nothing"
A span rather than a start index, because a span feeds slicing and replacement directly:
["Slice", {str: "hello world"}, ["RangeOf", {str: "hello world"}, {str: "o w"}]]
// ➔ "o w"
The optional from is the index to start searching at (default 1). The
returned span is always in the original subject's indices, so scanning for
the next occurrence is RangeOf(xs, needle, Last(r) + 1) for non-overlapping
matches (or First(r) + 1 to allow overlaps), and finding every occurrence is
that loop run until it answers Nothing:
["RangeOf", {str: "abcabc"}, {str: "bc"}]
// ➔ ["Range", 2, 3]
["RangeOf", {str: "abcabc"}, {str: "bc"}, 4]
// ➔ ["Range", 5, 6]
["RangeOf", {str: "abcabc"}, {str: "bc"}, 6]
// ➔ "Nothing"
Domain rules, chosen to make that loop terminate cleanly:
| Case | Result |
|---|---|
| Needle absent | Nothing |
| from past the end of xs | Nothing — never an error, since a match at the very end legitimately produces Length(xs) + 1 |
| from less than 1, or not an integer | An error value |
| Empty needle | An error value — an empty span is not representable (["Range", 1, 0] is the descending range [1, 0], not an empty one) |
| Infinite or unknown-length subject or needle | The expression stays symbolic |
["RangeOf", {str: "abc"}, {str: ""}]
// ➔ Error: out-of-range — expected a non-empty needle
["RangeOf", {str: "abc"}, {str: "a"}, 0]
// ➔ Error: out-of-range — expected an index of 1 or more
The needle may be a sibling kind of the subject: searching a string with a
list<character> needle is well-typed, and the span is still in the string's
character indices.
["RangeOf", {str: "abc"}, ["Characters", {str: "bc"}]]
// ➔ ["Range", 2, 3]
The defining law, stated element-wise: when the needle is found,
Slice(xs, RangeOf(xs, needle)) has the same element sequence as the needle.
It is deliberately not spelled == needle, because Slice is
kind-preserving: with a list<character> needle over a string subject the two
sides are a string and a list<character>, which are equal element by
element but are never == (the two types are disjoint siblings). When needle
and subject are the same kind, the stronger == does hold.
See also: ContainsSequence,
StringReplace,
IndexOf for element
search.
ContainsSequence(xs:indexed_collection, needle:indexed_collection) -> boolean
Whether needle occurs in xs as a contiguous subsequence. For a non-empty
needle this is RangeOf(xs, needle) not being Nothing.
["ContainsSequence", {str: "abc"}, {str: "bc"}]
// ➔ "True"
["ContainsSequence", {str: "abc"}, {str: "ac"}]
// ➔ "False"
Distinct from Contains,
which is element membership: Contains("abc", "ab") is False because
"ab" is not one of the string's characters.
An empty needle is True — the empty sequence is a subsequence of
everything. This is the one edge rule that deliberately diverges from
RangeOf's, which must reject an empty needle because it has to
return a span.
["ContainsSequence", {str: "abc"}, {str: ""}]
// ➔ "True"
A non-finite subject or needle leaves the expression symbolic.
StartsWith(xs:indexed_collection, prefix:indexed_collection) -> boolean
Whether xs begins with prefix as a contiguous subsequence.
["StartsWith", {str: "hello"}, {str: "he"}]
// ➔ "True"
["StartsWith", {str: "hello"}, {str: "el"}]
// ➔ "False"
["StartsWith", {str: "hello"}, {str: ""}]
// ➔ "True"
A prefix that would end in the middle of a grapheme cluster does not match:
StartsWith("👨👩👧", "👨") is False, because the subject's first (and only)
character is the whole family cluster.
EndsWith(xs:indexed_collection, suffix:indexed_collection) -> boolean
Whether xs ends with suffix as a contiguous subsequence.
["EndsWith", {str: "hello"}, {str: "lo"}]
// ➔ "True"
["EndsWith", {str: "hello"}, {str: ""}]
// ➔ "True"
EndsWith has to inspect the tail, so in addition to the finiteness rule it
needs a known length: over a collection whose length is not known the
expression stays symbolic.
Transforming a string
StringReplace(s:string, target:string, replacement:string) -> string
StringReplace(s:string, target:string, replacement:string, count:integer) -> string
Replace occurrences of target in s with replacement. Occurrences are
found by the same character-wise matching RangeOf uses, scanning
left to right, non-overlapping.
["StringReplace", {str: "a-b-c"}, {str: "-"}, {str: "+"}]
// ➔ "a+b+c"
["StringReplace", {str: "a-b-c"}, {str: "-"}, {str: "+"}, 1]
// ➔ "a+b-c"
["StringReplace", {str: "banana"}, {str: "na"}, {str: ""}]
// ➔ "ba"
The scan walks the original subject and skips past each match's span, so a replacement's own content is never re-matched:
["StringReplace", {str: "aa"}, {str: "a"}, {str: "aa"}]
// ➔ "aaaa" (not an infinite expansion)
Matching is character-wise, so a target cannot match part of a cluster:
["StringReplace", {str: "x́y"}, {str: "x"}, {str: "z"}]
// ➔ "x́y" (unchanged: the first character is x́, not x)
All occurrences are replaced by default; count limits the replacements from
the left and must be a positive integer. An empty target is an error
value: the host replaceAll("", x) behavior of inserting at every boundary is
a well-known surprise and is deliberately not inherited. An empty
replacement is legal and means deletion. A non-string operand leaves the
expression unevaluated.
["StringReplace", {str: "abc"}, {str: ""}, {str: "x"}]
// ➔ Error: unexpected-argument — the target must not be empty
["StringReplace", {str: "abc"}, {str: "a"}, {str: "x"}, 0]
// ➔ Error: unexpected-argument — count must be a positive integer
Regular-expression matching is not available; target is always a literal sequence of characters.
Trim(s:string, chars:(character | string | collection<character | string>)?) -> string
TrimStart(s:string, chars:(character | string | collection<character | string>)?) -> string
TrimEnd(s:string, chars:(character | string | collection<character | string>)?) -> string
Remove leading and/or trailing characters. Trim strips both ends,
TrimStart the beginning only, TrimEnd the end only.
With no chars, the characters removed are the Unicode White_Space set —
the same definition StringSplit uses, so it does not depend
on the host's interpretation of \s.
["Trim", {str: " hi "}]
// ➔ "hi"
["TrimStart", {str: " hi "}]
// ➔ "hi "
["TrimEnd", {str: " hi "}]
// ➔ " hi"
chars is a set of characters to strip, never a literal affix to remove. A string argument means "the set of this string's characters":
["Trim", {str: "xxhixx"}, {str: "x"}]
// ➔ "hi"
["Trim", {str: "abcba"}, {str: "ab"}]
// ➔ "c"
To remove a literal prefix or suffix instead, test it with
StartsWith / EndsWith and slice.
StringRepeat(s:string, n:integer) -> string
n copies of s, concatenated.
["StringRepeat", {str: "ab"}, 3]
// ➔ "ababab"
["StringRepeat", {str: "ab"}, 0]
// ➔ ""
n must be a non-negative integer; a negative or fractional n is an error value. Like every concatenation the result is re-segmented, so repeating a lone combining mark does not necessarily multiply the character count.
The name is StringRepeat rather than Repeat because
Repeat is the infinite lazy
collection constructor.
PadStart(s:string, n:integer, pad:string?) -> string
PadEnd(s:string, n:integer, pad:string?) -> string
Pad s to n characters by prepending (PadStart) or appending
(PadEnd) copies of pad. If s already has n or more characters it is
returned unchanged.
["PadStart", {str: "7"}, 3, {str: "0"}]
// ➔ "007"
["PadEnd", {str: "7"}, 3, {str: "0"}]
// ➔ "700"
["PadStart", {str: "abc"}, 2, {str: "0"}]
// ➔ "abc"
pad defaults to a single space. A multi-character pad repeats, and the final copy is truncated on a character boundary to fit exactly:
["PadStart", {str: "ab"}, 3]
// ➔ " ab"
["PadEnd", {str: "ab"}, 7, {str: "123"}]
// ➔ "ab12312"
n must be a non-negative integer, and an empty pad is an error value: there is no way to reach length n with it, and silently returning s unchanged would hide the caller's bug.
Padding counts characters, not display columns; aligning to a terminal or proportional-font width is an explicit non-goal.
Case
ToUpperCase(s:string) -> string
ToLowerCase(s:string) -> string
The Unicode default (locale-independent) upper- and lower-case mappings of s.
["ToUpperCase", {str: "hello"}]
// ➔ "HELLO"
["ToLowerCase", {str: "HELLO"}]
// ➔ "hello"
These are whole-string operations, not per-character maps, because case mapping is contextual. Two consequences worth knowing:
-
Case mapping can change the character count. German
ßuppercases to two characters:["ToUpperCase", {str: "straße"}]// ➔ "STRASSE"["Length", ["ToUpperCase", {str: "straße"}]]// ➔ 7 (the input has 6 characters) -
The Greek final sigma is chosen by position. A
Σat the end of a word lowercases toς, notσ:["ToLowerCase", {str: "ΟΔΟΣ"}]// ➔ "οδος" (final sigma U+03C2)
There is no locale argument in v1, so the Turkish dotless-i mapping is not
available; ToLowerCase("I") is "i" on every host, which is what makes
canonical forms and dedup keys identical everywhere. For case-insensitive
comparison, use CaseFold, not ToLowerCase.
CaseFold(s:string) -> string
The case-folded form of s — the right primitive for case-insensitive
comparison, which is CaseFold(a) == CaseFold(b) rather than a comparison of
ToLowerCase results.
["CaseFold", {str: "Straße"}]
// ➔ "strasse"
["CaseFold", {str: "STRASSE"}]
// ➔ "strasse"
["Equal", ["CaseFold", {str: "ΟΔΟΣ"}], ["CaseFold", {str: "οδοσ"}]]
// ➔ "True"
The folded form is not meant to be displayed — it is a comparison key. Note
that CaseFold(s) and ToLowerCase(s) differ exactly where lowercasing is
contextual: ToLowerCase("ΟΔΟΣ") is "οδος" (final sigma) while
CaseFold("ΟΔΟΣ") is "οδοσ" (medial sigma), which is what makes the fold
agree on both spellings.
Implementation note (v1 approximation). The host platform offers no
case-folding primitive, so the fold is computed as uppercase-then-lowercase
with the Greek final sigma restored to its medial form (U+03C2 → U+03C3). This
agrees with Unicode full case folding on Latin, Greek and Cyrillic text,
including the ß/SS case above; it deviates for a small number of
characters that Unicode's CaseFolding.txt maps specially, notably Cherokee
and some Turkic and Lithuanian sequences.
StringCompare(a:string, b:string) -> integer
Order two strings: -1 when a sorts before b, 0 when they are equal,
1 when a sorts after b.
["StringCompare", {str: "a"}, {str: "b"}]
// ➔ -1
["StringCompare", {str: "b"}, {str: "a"}]
// ➔ 1
["StringCompare", {str: "a"}, {str: "a"}]
// ➔ 0
["StringCompare", {str: "ab"}, {str: "abc"}]
// ➔ -1 (a proper prefix sorts first)
The order is the Unicode code-point order of the two NFC scalar sequences, compared position by position. It is deliberately not locale-aware: engine identity, dedup keys and match plans must be deterministic on every host. A locale-aware collation, if it ever ships, will arrive as an explicit trailing argument, never as a change to this order.
Where this differs from <. The relational operators on two multi-character
strings compare the host's UTF-16 code units, which places the astral
characters (U+10000 and above, encoded with surrogates starting at U+D800)
below the range U+E000–U+FFFF. StringCompare puts them above, where their
code points say they belong. For all other text — everything below U+D800, so
all of Latin, Greek, Cyrillic, CJK — the two orders agree. Use
StringCompare when the ordering must be by code point.
ce.box(['StringCompare', ce.string('\uE000'), ce.string('\u{1F600}')]).evaluate();
// ➔ -1 (U+E000 < U+1F600 by code point)
ce.box(['Less', ce.string('\uE000'), ce.string('\u{1F600}')]).evaluate();
// ➔ "False" (by UTF-16 code unit, U+1F600 starts at 0xD83D)
IntegerString(n:integer) -> string
IntegerString(n:integer, base:integer) -> string
A string representation of the integer n in the given base (2 to 36, default 10). The sign is preserved.
["IntegerString", 42]
// ➔ "42"
["IntegerString", 255, 16]
// ➔ "ff"
["IntegerString", -42]
// ➔ "-42"
IntegerString is broadcastable: applied to a list of integers it returns a
list of strings.
See also: DigitsFrom, BaseForm.
DigitsFrom(s:string) -> integer
DigitsFrom(s:string, base:integer) -> integer
The integer denoted by the digits of the string s, read in the given base
(2 to 36, default 10). Leading and trailing whitespace is ignored, and a
leading sign is honored. A 0x or 0b prefix selects base 16 or 2
regardless of the base argument.
["DigitsFrom", {str: "42"}]
// ➔ 42
["DigitsFrom", {str: "-42"}]
// ➔ -42
["DigitsFrom", {str: "0xff"}]
// ➔ 255
A digit that is not valid in the base produces an unexpected-digit error
value.
See also: IntegerString, BaseForm,
NumberFrom.
NumberFrom(s:string) -> number
NumberFrom(s:string, base:integer) -> number
The number denoted by the string s. Unlike
DigitsFrom, which is integer-only, NumberFrom accepts
fractions, exponents and the non-finite spellings.
["NumberFrom", {str: "42"}]
// ➔ 42
["NumberFrom", {str: "3.14"}]
// ➔ 3.14
["NumberFrom", {str: "-1.5e2"}]
// ➔ -150
The accepted grammar is fixed, so that different hosts cannot drift: any
amount of leading and trailing Unicode White_Space, an optional + or -
sign, then either a decimal numeral — ASCII digits, with an optional .
fraction and an optional e/E exponent — or one of the exact spellings
oo, +oo, -oo, NaN.
| Input | Result | Why |
|---|---|---|
"42", "-42", "+7", " 42 " | 42, -42, 7, 42 | Integer numeral, sign and surrounding whitespace allowed |
"3.14", "1e-3", "1.5e3" | 3.14, 0.001, 1500 | Fraction and exponent |
".5" | 0.5 | A leading . needs no integer part |
"oo", "+oo", "-oo", "NaN" | +oo, +oo, -oo, NaN | The engine's own spellings for the non-finite values |
"5." | Error invalid-number | A trailing . with no fraction digits is not a numeral |
"" | Error invalid-number | The empty string denotes no number |
"abc", "12abc" | Error invalid-number | The whole string must be a numeral — a numeric prefix is not enough |
"1/3" | Error invalid-number | Not a numeral; build the fraction with arithmetic |
"0x1f", "1_000", "Infinity", "nan" | Error invalid-number | Not in the grammar (spellings are exact and case-sensitive) |
"٣" (Arabic-Indic three) | Error invalid-number | ASCII digits only, so that homoglyph digits cannot slip through |
Failure is always an error value, never NaN: NaN is a legitimate
parse result for the literal "NaN", so it cannot double as the failure
signal.
["NumberFrom", {str: "12abc"}]
// ➔ Error: invalid-number
Exactness follows the engine's evaluate/N contract: a numeral with no
fraction and no exponent parses to an exact integer, and a fractional or
exponent numeral parses to an exact decimal, numericized only by .N().
The optional base (2 to 36) mirrors DigitsFrom's and
accepts integer numerals only:
["NumberFrom", {str: "ff"}, 16]
// ➔ 255
["NumberFrom", {str: "1010"}, 2]
// ➔ 10
["NumberFrom", {str: "1.5"}, 16]
// ➔ Error: invalid-number
["NumberFrom", {str: "11"}, 37]
// ➔ Error: unexpected-base
See also: DigitsFrom,
IntegerString, String.
BaseForm(value:integer) -> number
BaseForm(value:integer, base:integer) -> number
Represent an integer in a specific base, such as hexadecimal or binary.
If no base is specified, use base-10. base should be an integer from 2 to 36.
BaseForm evaluates to the numeric value it represents, so based numerals
participate in arithmetic:
["BaseForm", 23, 2]
// ➔ 23
Parsing. A numeral with an integer-literal subscript base, e.g. 10111_2 or
2748_{16}, parses to BaseForm, provided every digit is valid for the base
(otherwise it stays an inert Subscript):
ce.parse('10111_2').json;
// ➔ ["BaseForm", 23, 2]
ce.parse('1011_2 \\cdot 101_2').evaluate();
// ➔ 55
ce.parse('11_8 - 3_8 = 6_8').evaluate();
// ➔ "True"
ce.parse('19_2').json;
// ➔ ["Subscript", 19, 2] (9 is not a valid base-2 digit)
A symbol subscript base, e.g. 161_b, parses to BaseForm of the digit
polynomial in that base, so base equations can be solved symbolically:
ce.parse('161_b').json;
// ➔ ["BaseForm", ["Add", ["Power", "b", 2], ["Multiply", 6, "b"], 1], "b"]
ce.parse('161_b + 134_b = 315_b').solve('b');
// ➔ [0, 8]
Serialization. BaseForm round-trips through LaTeX as value_{base}:
ce.box(['BaseForm', 23, 2]).latex;
// ➔ "10111_{2}"
ce.box(['BaseForm', 42, 16]).latex;
// ➔ "\mathrm{2a}_{16}"
Delimiter(expr)
Delimiter(expr, delim)
Visually group expressions with an open delimiter, a close delimiter and separators between elements of the expression.
When serializing to LaTeX, render expr wrapped in delimiters.
The Delimiter function is inert and the value of a ["Delimiter", _expr_] expression is expr.
expr is a function expression, usually a ["Sequence"]. It should
not be a symbol or a number.
delim is an optional string:
- when it is a single character it is a separator
- when it is two characters, the first is the opening delimiter and the second is the closing delimiter
- when it is three characters, the first is the opening delimiter, the second is the separator, and the third is the closing delimiter
The delimiters are rendered to LaTeX.
The open and close delimiters are a single character, one of: ()[]{}<>|‖⌈⌉⌊⌋⌜⌝⌞⌟⎰⎱". The open and close delimiters do not have to match.
For example, "')]'" is a valid delimiter.
If an open or close delimiter is ., it is ignored.
The separator delimiter is also a single character, one of ,;.&:|- or U+00B7 (middle dot), U+2022 (bullet) or U+2026 (ellipsis).
If no delim is provided, a default delimiter is used based on the type of expr:
["Sequence"]->(,)["Tuple"],["Single"],["Pair"],["Triple"]->(,)["List"]->[,]["Set"]->{,}
Spacing(width)
When serializing to LaTeX, width is the dimension of the spacing, in 1/18 em.
The Spacing function is inert and the value of a ["Spacing", _expr_] expression is expr.
Regular expressions
A regular expression is a compiled pattern, built with RegExp and used by
IsMatch, StringMatch, StringMatchAll, and the pattern forms of
StringSplit and StringReplace.
Patterns are most readable as a raw string literal, which performs no escape processing, so a backslash means a backslash:
RegExp(#"[0-9]+(\.[0-9]+)?"#)
The dialect is JavaScript's, in full. Backreferences, lookahead and lookbehind all work, and there is no restricted subset. Matching is always code-point aware. Compiled JavaScript uses the same engine, so a compiled expression and an interpreted one agree.
Regular-expression matching backtracks. Some ordinary-looking patterns take
time exponential in the length of the subject — the classic example is
RegExp(#"(a+)+$"#) against a non-matching string of about thirty as.
The engine cannot stop this. Deadlines are checked between evaluation
steps, and a single match is one step, so no timeout, withTimeLimit span or
abort signal will end it: the call runs to completion or hangs.
Matching a pattern you wrote against data you control is fine. If either the pattern or the subject can come from somewhere you do not control, that is a denial-of-service path, and the engine gives you no protection from it.
RegExp(pattern:string) -> regexp
RegExp(pattern:string, flags:string) -> regexp
Compile pattern into a value of type regexp.
["RegExp", "'[0-9]+'"]
The type regexp is disjoint from string: a plain string cannot be
passed where a pattern is expected. That is deliberate — otherwise every
string argument would become silently pattern-sensitive, and "a.c" would
stop meaning what it says.
An invalid pattern is an error value, reported where it was written rather than at the first match.
flags is a string of single letters: i (case-insensitive), m
(multi-line ^/$), s (. matches a newline), d (record capture
indices), u/v (Unicode mode). The global and sticky flags g and y are
rejected: they carry a mutable scan position, so a value carrying one
would answer differently depending on what it matched last. Use
StringMatchAll for every match.
Two patterns are the same value when their pattern text and flags are equal.
IsMatch(subject:string, pattern:regexp) -> boolean
Whether subject contains a match for pattern.
["IsMatch", "'abc123'", ["RegExp", "'[0-9]+'"]]
// ➔ "True"
StringMatch(subject:string, pattern:regexp) -> record
The first match of pattern in subject, as a record — or Nothing when
there is no match.
The record has these fields:
| Field | Meaning |
|---|---|
match | the matched text |
range | its span in the subject, as a range of character positions |
groups | the numbered captures, in order (Nothing for one that did not participate) |
names | the named captures, as a dictionary |
range is what lets a match compose with the rest of the string operators:
["Slice", "'abc123'", ["At", ["StringMatch", "'abc123'", ["RegExp", "'[0-9]+'"]], "'range'"]]
// ➔ "'123'"
Positions count characters (grapheme clusters), like every other string operation — not UTF-16 code units.
range is not always presentA pattern can match part of a character. 👨👩👧 is one character built
from several code points, and a pattern can match just the 👩 inside it.
There is no span of whole characters that names exactly that text, so range
is absent for such a match and reading it gives Missing. match still
holds the exact text. Test range before slicing with it.
StringMatchAll(subject:string, pattern:regexp) -> list<record>
Every non-overlapping match, as a list of records shaped exactly like
StringMatch's.
["StringMatchAll", "'a1b22c'", ["RegExp", "'[0-9]+'"]]
// ➔ two records, matching "1" and "22"
Patterns with StringSplit and StringReplace
Both take a regexp where they take a literal separator or target, with the
host's own semantics — including splitting at a zero-width match:
["StringSplit", "'a1b22c'", ["RegExp", "'[0-9]+'"]]
// ➔ ["'a'", "'b'", "'c'"]
["StringReplace", "'a1b22c'", ["RegExp", "'[0-9]+'"], "'#'"]
// ➔ "'a#b#c'"
StringReplace also accepts a function replacement, called with the match
record, so each replacement can be computed from its captures:
["StringReplace", "'ab cd'", ["RegExp", "'[a-z]+'"],
["Function", ["ToUpperCase", ["At", "m", "'match'"]], "m"]]
// ➔ "'AB CD'"
Captures are not interleaved into StringSplit's result; use
StringMatchAll when you want them.
Compilation. IsMatch and StringReplace with a literal pattern and a
string replacement compile to JavaScript. StringMatch, StringMatchAll, a
function replacement and a computed pattern do not — they report character
positions or records that compiled code has no representation for — and fail
closed rather than answering differently from the interpreter. No regular
expression compiles to Python or to the shader targets.
Annotated(expr:expression, dictionary) -> expression
Annotated(expr, attributes) is an expression that behaves exactly like expr,
but carries visual or semantic metadata as an attribute dictionary.
The attributes have no effect on evaluation. This function is inert — it evaluates to its first argument.
The attributes dictionary may include:
- Visual style hints (e.g.
weight: "bold",color: "blue") - Semantic metadata (e.g.
tooltip,language,link)
Use Annotated when you want to attach presentational or semantic
information to an expression without affecting its evaluation or identity.
This is useful for rendering, tooltips, highlighting, etc.
The following keys are applicable to math expressions:
mathStyle="compact"or"normal". The"compact"style is used for inline math expressions, while the"normal"style is used for display math expressions.scriptLevel=0,1, or-1,+1. The script level is used to determine the size of the expression in relation to the surrounding text. A script level of0is normal size,1is smaller, and2is even smaller.
The following keys are applicable to text content:
weighta string, one of"normal","bold","bolder","light"stylea string, one of"normal","italic","oblique"languagea string indicating the language of the expression, e.g."en"(English),"fr"(French),"es"(Spanish)
The following keys are applicable to both math expressions and text content:
colora color name or hex codebackgroundColora color name or hex code for the background colortooltipa string to be displayed as a tooltip when the expression is hovered overlinka URL to be followed when the expression is clickedcssClassa string indicating the CSS class to be applied to the expressioncssIda string indicating the CSS id of the expression
The Annotated function is inert and the value of a ["Annotated", expr] expression is expr.
Text and LaTeX
Text builds a formatted text expression (see
Text Expressions above); LatexString carries a literal
LaTeX fragment and is documented with the other core operators.