Ui parser

primitive UIParser
  """
  Stateless parser for the UI DSL. Handles comment stripping,
  line tokenization, and size literal parsing.
  """

  fun strip_comments(line: String): String =>
    """
    Remove // line comments and /* ... */ inline block comments.
    If a block comment is opened but not closed, strip from /* to end of line.
    """
    recover val
      var result: String ref = line.clone()

      // Strip block comments (/* ... */ or unclosed /*)
      try
        while true do
          let open_idx = result.find("/*")?
          try
            let close_idx = result.find("*/", open_idx.isize() + 2)?
            // Remove from /* through */
            let before = result.substring(0, open_idx.isize())
            let after = result.substring(close_idx.isize() + 2)
            result = before.clone()
            result.append(consume after)
          else
            // Unclosed block comment: strip from /* to end
            result = result.substring(0, open_idx.isize()).clone()
          end
        end
      end

      // Strip line comments (//)
      try
        let idx = result.find("//")?
        result = result.substring(0, idx.isize()).clone()
      end

      result.rstrip()
      result
    end

  fun tokenize_line(line: String, line_num: USize)
    : (ParsedLine | BuilderError)
  =>
    """
    Count leading spaces for indent level (must be multiple of 2),
    then tokenize the remaining content.
    """
    // Count leading spaces
    var spaces: USize = 0
    try
      while spaces < line.size() do
        if line(spaces)? == ' ' then
          spaces = spaces + 1
        else
          break
        end
      end
    end

    if (spaces % 2) != 0 then
      return BuilderError(line_num,
        "indentation must be a multiple of 2 spaces (found " + spaces.string()
          + ")")
    end

    let indent = spaces / 2

    let content: String val =
      recover val
        let c: String ref = line.substring(spaces.isize())
        c.strip()
        c
      end

    if content.size() == 0 then
      return ParsedLine(indent, recover val Array[Token](0) end)
    end

    // Tokenize content
    let tokens: Array[Token] iso = recover iso Array[Token] end

    var i: USize = 0
    while i < content.size() do
      try
        // Skip whitespace
        if content(i)? == ' ' then
          i = i + 1
          continue
        end

        // Quoted string
        if content(i)? == '"' then
          let start = i + 1
          var end_idx = start
          try
            while end_idx < content.size() do
              if content(end_idx)? == '"' then break end
              end_idx = end_idx + 1
            end
          end
          let inner = content.substring(start.isize(), end_idx.isize())
          tokens.push(Token(TokQuotedString, consume inner))
          i = end_idx + 1
          continue
        end

        // ID (#name)
        if content(i)? == '#' then
          let start = i + 1
          var end_idx = start
          try
            while end_idx < content.size() do
              if content(end_idx)? == ' ' then break end
              end_idx = end_idx + 1
            end
          end
          let name = content.substring(start.isize(), end_idx.isize())
          tokens.push(Token(TokId, consume name))
          i = end_idx
          continue
        end

        // Word: read until space
        let start = i
        var end_idx = start
        try
          while end_idx < content.size() do
            if content(end_idx)? == ' ' then break end
            end_idx = end_idx + 1
          end
        end
        let word: String val = content.substring(start.isize(), end_idx.isize())
        tokens.push(_classify_word(word))
        i = end_idx
      else
        break
      end
    end

    let final_tokens: Array[Token] val = consume tokens
    ParsedLine(indent, final_tokens)

  fun _classify_word(word: String val): Token =>
    """
    Classify a plain word into the appropriate token kind.
    """
    match word
    | "pack-start" => Token(TokPackStart, word)
    | "pack-end" => Token(TokPackEnd, word)
    | "add" => Token(TokAdd, word)
    else
      if _is_mode(word) then
        Token(TokMode, word)
      elseif _is_size(word) then
        Token(TokSize, word)
      elseif word.contains("=") then
        try
          let eq_idx = word.find("=")?
          let key = word.substring(0, eq_idx.isize())
          let value = word.substring(eq_idx.isize() + 1)
          Token(TokKeyValue, consume value, consume key)
        else
          Token(TokWord, word)
        end
      else
        Token(TokWord, word)
      end
    end

  fun _is_size(word: String val): Bool =>
    """
    A size is bare '*' or 'WxH' where W and H are digits or '*'.
    Must not match words like 'vbox' that happen to contain 'x'.
    """
    if word == "*" then return true end
    if not word.contains("x") then return false end
    try
      let x_pos = word.find("x")?
      let w_part: String val = word.substring(0, x_pos.isize())
      let h_part: String val = word.substring((x_pos + 1).isize())
      _is_size_part(w_part) and _is_size_part(h_part)
    else
      false
    end

  fun _is_size_part(s: String val): Bool =>
    """
    A size part is '*' or all digits.
    """
    if s == "*" then return true end
    if s.size() == 0 then return false end
    var i: USize = 0
    try
      while i < s.size() do
        let c = s(i)?
        if (c < '0') or (c > '9') then return false end
        i = i + 1
      end
    end
    true

  fun _is_mode(word: String val): Bool =>
    match word
    | "fill" => true
    | "expand" => true
    | "fixed" => true
    else
      false
    end

  fun parse_size(size_str: String, line_num: USize)
    : ((USize, USize) | BuilderError)
  =>
    """
    Parse a size literal: "*" → (0, 0), "WxH" → (W, H).
    An asterisk in either position means 0 (unconstrained).
    """
    if size_str == "*" then
      return (0, 0)
    end

    if size_str.contains("x") then
      let parts = size_str.split_by("x")
      try
        if parts.size() != 2 then error end
        let w_str: String val = parts(0)?
        let h_str: String val = parts(1)?
        let w: USize = if w_str == "*" then 0 else w_str.usize()? end
        let h: USize = if h_str == "*" then 0 else h_str.usize()? end
        (w, h)
      else
        BuilderError(line_num,
          "invalid size literal: " + size_str)
      end
    else
      BuilderError(line_num,
        "invalid size literal: " + size_str)
    end