DDirectorWikiDirector & Lingo Encyclopedia

Strings and Chunk Expressions

Lingo string handling - chunk expressions (char, word, item, line), itemDelimiter, searching, mutation, and parsing binary-delimited network data.

String fundamentals

lingo
s = "Hello, World"
put s.length              -- 12       (also: the length of s, length(s))
put s & "!"               -- concatenation
put "a" && "b"            -- "a b"    (concatenate with a space)
put EMPTY                 -- ""
  • Strings are 1-indexed sequences of single-byte characters (pre-Director 11).
  • Comparison operators (=, <, contains, starts) are case-insensitive.
  • offset("lo", s) returns the 1-based position of a substring (0 if absent) and IS case-insensitive like the rest.
lingo
put "HELLO" = "hello"          -- 1 (TRUE)
put s contains "WORLD"         -- 1
put s starts "hel"             -- 1
put offset("world", s)         -- 8

Chunk expressions

Chunks are Lingo's signature feature: addressing substrings structurally.

ChunkSeparator rule
char n of sSingle characters.
word n of sRuns separated by spaces, tabs, and returns (any run of whitespace counts as one separator).
item n of sSeparated by the itemDelimiter (default comma).
line n of sSeparated by RETURN (carriage return, char 13).
lingo
t = "red,green,blue"
put item 2 of t                     -- "green"
put char 1 of t                     -- "r"
put char 1 to 3 of t                -- "red"
put word 2 of "the quick fox"       -- "quick"
put line 2 of ("a" & RETURN & "b")  -- "b"

put the number of chars in t        -- 14
put the number of items in t        -- 3
put the number of words in t        -- 1
put the number of lines in t        -- 1
put t.item[2]                       -- dot-syntax chunk access (D7+)
put t.item[1..2]                    -- "red,green" (range keeps delimiters)

Chunks nest arbitrarily:

lingo
put word 1 of item 2 of line 3 of bigText

Out-of-range chunk reads return EMPTY, never an error. the last word of s, the last char of s etc. are supported, as is counting via the number of words in s.

the itemDelimiter

lingo
the itemDelimiter = TAB
fields = the number of items in row
name = item 2 of row
the itemDelimiter = ","        -- restore!
  • It is a single character, global to the movie, and anything that parses items while you have it changed will use your value. Classic bugs come from a handler changing it and not restoring it.
  • MX 2004 documents only a single-character delimiter.
  • Habbo-era network code sets it to control characters like numToChar(2) to split server packets.

Chunk mutation

Chunks are assignable targets, and put ... into/before/after works on them:

lingo
s = "red,green,blue"
put "GREEN" into item 2 of s     -- "red,GREEN,blue"
put ">" before word 1 of s
delete item 3 of s               -- removes the chunk AND its delimiter

Mutating chunk expressions on field members edits the visible text directly (put "hi" into line 2 of field "chat"), which is how classic UIs updated text.

Related field/text utilities:

lingo
hilite word 2 of field "doc"     -- select text in a field
put chars(s, 2, 4)               -- "edg" (substring function form)

Search and split idioms

Lingo has no split(); real code splits with chunks:

lingo
on splitString input, delim
  saved = the itemDelimiter
  the itemDelimiter = delim
  parts = []
  cnt = the number of items in input
  repeat with i = 1 to cnt
    add parts, item i of input
  end repeat
  the itemDelimiter = saved
  return parts
end

offset() powers streaming parsers:

lingo
pos = offset(delim, buffer)
if pos > 0 then
  packet = char 1 to pos - 1 of buffer
  delete char 1 to pos of buffer
end if

Case conversion and formatting

Classic Lingo (through MX 2004) has no built-in case conversion; code ships its own toUpper/toLower via charToNum/numToChar loops. Number formatting goes through string(), the floatPrecision, and manual padding.

Parsing binary-delimited protocols

Multiplayer-era Shockwave games (Habbo Hotel being the canonical example) transported data as text with control-character separators:

  • chr(1) / numToChar(1) between logical records,
  • chr(2) between fields,
  • chr(13) (RETURN) as an older record separator, which conveniently makes records addressable as line n of packet.

Rules that emulators must match exactly:

  • word splitting treats space, TAB, and RETURN as separators. Control characters like chr(1)/chr(2) are not word separators; they stay glued to adjacent characters, so word 1 of ("a" & numToChar(2) & "b") is the whole 3-character string.
  • line splits only on RETURN (char 13); chr(10) (linefeed) is not a line separator in classic Mac-heritage Lingo, though Windows CRLF text yields a stray chr(10) at line starts that robust code strips.
  • Numeric coercion of a field that still carries a trailing delimiter (integer("42" & numToChar(2))) returns VOID under MX 2004's strict rule; game code therefore isolates the digits first (chunk off the delimiter, then coerce). When emulating older players, test this exact case against the target version before assuming leniency.
  • value() on delimited text is even stricter and also side-effect-prone; avoid it in protocol paths.
lingo
-- typical packet field extraction
the itemDelimiter = numToChar(2)
userId = integer(item 1 of packet)
userName = item 2 of packet

String performance

Strings are immutable values; put ... after bigString inside a tight loop is O(n^2). Old well-written Lingo accumulates into fields or lists and joins once; emulators should still expect and tolerate the slow pattern.