java.lang.Object
io.github.brantunger.unruly.api.language.MessageText

public final class MessageText extends Object
Shortens and escapes text a language didn't write, such as a fact name, an option value, an import or its library's own error text, for a message, as the engine does in its own messages. Such text can hold line breaks, bidi controls or lone surrogates, and a message the engine or a language logs mustn't let it start a log line of its own or hide part of the line. It delegates to the engine's own escaping rather than keeping a copy of it.

Use it for text the engine shows as it came: a message a language logs itself, the message of an exception its own library sees first, and the issues it throws in an InvalidExpressionException, which the engine copies untouched into the RuleCompilationException it reports. Use quote(java.lang.String) for a name and escape(truncate(text)) for other text. To end such text with a note of the language's own, such as " (caused by ...)", cut it with escapeWithin(String, int), escapeWithin(String, int, int) or quoteWithin(String, int), giving it the room the rest of the message leaves: the escaped result, its ... (N more characters) included, fits in that room, so the note stays whole. To end text it shortened its own way with the engine's note, use leftOut(int).

The engine shortens to 1,000 characters and escapes the message of an InvalidExpressionException, the message of a warning's issue when it logs it, and the message of an exception from ExpressionCompiler.checkFactName(java.lang.String) when it logs it or, at load(), reports it for a declared fact. Leave a message the engine shortens raw: the engine escapes it itself, and the count of what was left out of escaped text counts escaped characters. Every cut the engine and this class make, but truncate(java.lang.String)'s, falls between code points and never inside an escape the engine writes or inside raw text that reads as one (a backslash and n, r or t, or a backslash, u and four lowercase hex digits): such text is left out whole, so up to 5 fewer characters show. A language may instead escape such a message itself, when the escaped message fits in 1,000 characters, as escapeWithin(text, MAX_TEXT_LENGTH) makes it; the engine then never shortens it.

The escapes, the note ... (N more characters) and the limits, MAX_NAME_LENGTH (200 characters, UTF-16 units) for a name and MAX_TEXT_LENGTH (1,000) for other text, are fixed for 2.x. javac inlines the two constants into the classes that read them. Which characters above U+00FF are escaped follows Unicode 15.0 to 17.0, as JDK 21 to 26 have it, from a table of the engine's own, whatever the running JDK's Unicode version, so a later JDK escapes the same characters until a later 2.x release updates the table; below U+0100 the running JDK decides, the same way on JDK 21 to 26. A later 2.x release may also escape more kinds of character.

On Linux and macOS, with no engine built, the JVM's first read of a character of a string holding one above U+00FF, such as "π", initializes the JDK's java.lang.StringUTF16, and a first read deep in a stack can break that class for the life of the JVM (#1295). So make a first escape of such text near the top of a stack, or after an engine is built: the first build initializes the class.

  • Field Details

    • MAX_NAME_LENGTH

      public static final int MAX_NAME_LENGTH
      The most characters (UTF-16 units) of a name quote(java.lang.String) shows: 200, fixed for 2.x. javac inlines it into the classes that read it.
      See Also:
    • MAX_TEXT_LENGTH

      public static final int MAX_TEXT_LENGTH
      The most characters (UTF-16 units) of other text truncate(java.lang.String) keeps, and the length above which the engine shortens the messages the class description lists: 1,000, fixed for 2.x. javac inlines it into the classes that read it.
      See Also:
  • Method Details

    • leftOut

      public static String leftOut(int count)
      Writes the note that follows shortened text, as truncate(java.lang.String), quote(java.lang.String) and the engine's own messages write it: ... (N more characters), N being how many characters of the text were left out. For a language that shortens text its own way, such as to the room a message leaves after it, and ends it with the engine's note. The count is written as given, whatever it is. It allocates the note.
      Parameters:
      count - How many characters were left out
      Returns:
      ... ( + count + more characters)
    • escape

      public static String escape(String text)
      Escapes text for a message, without shortening it. \n, \r and \t become those two-character escapes. Every other ISO control character, line or paragraph separator, space separator (category Zs) other than U+0020, format character (category Cf), other Default_Ignorable_Code_Point character and lone surrogate becomes a backslash, u and four lowercase hex digits, one for each UTF-16 unit, so an escaped character outside the Basic Multilingual Plane becomes two escapes, and each escaped unit shows as 2 or 6 characters. Everything else is kept, a valid surrogate pair such as an emoji included. A backslash isn't escaped, so escaping text twice gives the same text.

      Space separators and format characters above U+00FF are those of Unicode 15.0 to 17.0, as JDK 21 to 26 have them, from a table of the engine's own rather than from the running JDK's Character.getType(int), which above U+00FF reads a table the JVM builds on its first use (#1286); below U+0100 the running JDK decides, the same way on JDK 21 to 26. The space separators are U+00A0, U+1680, U+2000 to U+200A, U+202F, U+205F and U+3000 besides U+0020. The other default-ignorable characters are U+034F, U+115F to U+1160, U+17B4 to U+17B5, U+180B to U+180F, U+2065, U+3164, U+FE00 to U+FE0F, U+FFA0, U+FFF0 to U+FFF8 and U+E0000 to U+E0FFF.

      To put long text in a message, shorten it first and escape it after, escape(truncate(text)), so a cut never falls inside an escape this writes. The result can be up to six times longer than the 1,000 characters kept, since a character escapes to at most 6, so use it only for text the engine won't shorten again. Leave a message the engine shortens raw: the engine escapes it itself (see the class description). To fit escaped text into a room, use escapeWithin(String, int).

      Parameters:
      text - The text
      Returns:
      The text, escaped
      Throws:
      NullPointerException - if text is null
    • truncate

      public static String truncate(String text)
      Shortens text for a message to at most 1,000 characters (UTF-16 units), without escaping it. Text of at most 1,000 characters is returned as it is. Longer text keeps its first 1,000 characters, or 999 when the 1,000th is a high surrogate, so the text never ends in half a character, followed by ... (N more characters), where N counts the text's own characters left out. 1,000 is also the length above which the engine shortens the messages the class description lists, such as that of an InvalidExpressionException.

      Shorten first and escape after, escape(truncate(text)), so a cut never falls inside an escape and N counts the original text. Don't shorten text that is already escaped.

      Parameters:
      text - The text, not escaped
      Returns:
      The text, shortened if it was longer
      Throws:
      NullPointerException - if text is null
    • quote

      public static String quote(String name)
      Shortens a name for a message to at most 200 characters (MAX_NAME_LENGTH, UTF-16 units), then escapes it. The cut falls between code points and before raw text that reads as an escape, which is left out whole, so up to 5 fewer characters show; ... (N more characters) follows, as truncate(java.lang.String) writes it, N counting the name's own characters left out. It adds no quotes: write them around it, as in "fact '" + MessageText.quote(name) + "'".
      Parameters:
      name - The name, such as a fact's, an option's or a class's
      Returns:
      The name, shortened if it was longer, then escaped
      Throws:
      NullPointerException - if name is null
    • escapeWithin

      public static String escapeWithin(String text, int room)
      Escapes text for a message as escape(java.lang.String) does, shortened first if its escaped form is longer than room characters: to as many of its first characters as fit in room with ... (N more characters) after them, as truncate(java.lang.String) writes it. Use it to keep what a message says after the text, such as a closing note, whole: give it the room the rest of the message leaves.

      N counts the text's own characters left out, as given, not escaped ones. The cut falls between code points, so never inside a surrogate pair or an escape this writes, and never inside an escape the text already holds, or inside raw text that reads as one (\n, \r, \t, or a backslash, u and four lowercase hex digits): such text is left out whole. escape(java.lang.String) leaves an escape the text holds as it is, so a cut inside one would show it cut in half.

      The result is at most room characters, unless room is too small for the note alone: then the result is the note alone, and nothing is thrown. A negative room is taken as 0. Empty text is returned empty, whatever the room.

      Parameters:
      text - The text, raw, or with escapes the engine already wrote
      room - The most characters the escaped text may take, the note included
      Returns:
      The text, shortened if it didn't fit, then escaped
      Throws:
      NullPointerException - if text is null
    • escapeWithin

      public static String escapeWithin(String text, int most, int room)
      Escapes text within a room as escapeWithin(String, int) does, showing at most its first most characters (UTF-16 units). Where most falls inside a surrogate pair, or inside an escape the text holds or raw text that reads as one, that is left out whole, as quote(java.lang.String) leaves it out at 200, so up to 5 fewer characters show. A most of 0 or less shows none of a non-empty text, only the note. With a room of Integer.MAX_VALUE, the text is shortened to most characters alone.
      Parameters:
      text - The text, raw, or with escapes the engine already wrote
      most - The most of its characters to show
      room - The most characters the escaped text may take, the note included
      Returns:
      The text, shortened if it didn't fit or is longer than most, then escaped
      Throws:
      NullPointerException - if text is null
    • quoteWithin

      public static String quoteWithin(String name, int room)
      Quotes a name for a message as quote(java.lang.String) does, or, if that is longer than room characters, escapes it within the room as escapeWithin(String, int) does. Either way it shows no more of the name than quote(java.lang.String) does. An empty name is quoted as empty, whatever the room. It adds no quotes.
      Parameters:
      name - The name, such as a fact's, an option's or a class's
      room - The most characters the quoted name may take, the note included
      Returns:
      The name, shortened if it was longer or didn't fit, then escaped
      Throws:
      NullPointerException - if name is null