local U = {} function U.trim(s) return (s:gsub("^%s+", ""):gsub("%s+$", "")) end -- Reads a { ... } group, handling nesting and \{ \} escapes. -- `open` points at the opening brace; returns the inner content and the -- position just after the closing brace. function U.read_group(s, open) local depth, i, n = 0, open, #s while i <= n do local c = s:sub(i, i) if c == "\\" then i = i + 2 elseif c == "{" then depth = depth + 1; i = i + 1 elseif c == "}" then depth = depth - 1 if depth == 0 then return s:sub(open + 1, i - 1), i + 1 end i = i + 1 else i = i + 1 end end error("scholatex: missing closing brace from position " .. open) end -- Raw { minus } balance on a line (escapes ignored). -- Used to track depth inside for/if/while bodies. function U.raw_brace_delta(line) local delta, i, n = 0, 1, #line while i <= n do local c = line:sub(i, i) if c == "\\" then i = i + 2 else if c == "{" then delta = delta + 1 elseif c == "}" then delta = delta - 1 end i = i + 1 end end return delta end -- Splits a top-level comma-separated list, honouring nested {} [] and -- escapes. Used by the for-in-list loop. function U.split_commas(s) local items, depth, start, i, n = {}, 0, 1, 1, #s while i <= n do local c = s:sub(i, i) if c == "\\" then i = i + 2 else if c == "{" or c == "[" then depth = depth + 1 elseif c == "}" or c == "]" then depth = depth - 1 elseif c == "," and depth == 0 then items[#items + 1] = U.trim(s:sub(start, i - 1)) start = i + 1 end i = i + 1 end end items[#items + 1] = U.trim(s:sub(start)) return items end -- Splits text into paragraphs at top-level newlines, i.e. newlines that -- are not inside a { } group or $ $ math span. Escapes are honoured. -- A newline nested in a sub-group stays within its paragraph. function U.split_top_newlines(s) local paras, depth, inmath, start, i, n = {}, 0, false, 1, 1, #s while i <= n do local c = s:sub(i, i) if c == "\\" then i = i + 2 elseif c == "$" then inmath = not inmath; i = i + 1 elseif c == "{" and not inmath then depth = depth + 1; i = i + 1 elseif c == "}" and not inmath then depth = depth - 1; i = i + 1 elseif c == "\n" and depth == 0 and not inmath then paras[#paras + 1] = s:sub(start, i - 1) start = i + 1; i = i + 1 else i = i + 1 end end paras[#paras + 1] = s:sub(start) return paras end local PLACE_V = {t = "top", m = "center", b = "bottom"} local PLACE_H = {l = "left", c = "center", r = "right"} function U.place_code(w) if type(w) ~= "string" or #w ~= 2 then return nil end local v, h = PLACE_V[w:sub(1, 1)], PLACE_H[w:sub(2, 2)] if v and h then return v, h end return nil end function U.split_opts(s) local toks, i, n = {}, 1, #s while i <= n do while i <= n and s:sub(i, i):match("%s") do i = i + 1 end if i > n then break end local start = i while i <= n and not s:sub(i, i):match("%s") do if s:sub(i, i) == "{" then local _, after = U.read_group(s, i) i = after else i = i + 1 end end toks[#toks + 1] = s:sub(start, i - 1) end return toks end return U