///|
/// Incremental ECMA-48 parser. It accepts arbitrary chunk boundaries, including
/// a split between ESC and `\\` in an ST terminator.
pub(all) struct Parser {
mut mode : ParserMode
mut text : Array[Char]
mut sequence : Array[Char]
mut payload : Array[Char]
mut sequence_start : Int
mut offset : Int
}
///|
pub fn Parser::new() -> Parser {
{
mode: Ground,
text: [],
sequence: [],
payload: [],
sequence_start: 0,
offset: 0,
}
}
///|
pub fn Parser::current_mode(self : Parser) -> ParserMode {
self.mode
}
///|
pub fn Parser::consumed(self : Parser) -> Int {
self.offset
}
///|
fn is_c0(c : Char) -> Bool {
let n = c.to_int()
n < 0x20 || n == 0x7f
}
///|
fn is_c1(c : Char) -> Bool {
let n = c.to_int()
n >= 0x80 && n <= 0x9f
}
///|
fn is_parameter_byte(c : Char) -> Bool {
let n = c.to_int()
n >= 0x30 && n <= 0x3f
}
///|
fn is_intermediate_byte(c : Char) -> Bool {
let n = c.to_int()
n >= 0x20 && n <= 0x2f
}
///|
fn is_final_byte(c : Char) -> Bool {
let n = c.to_int()
n >= 0x40 && n <= 0x7e
}
///|
fn chars_to_string(chars : Array[Char]) -> String {
String::from_array(chars)
}
///|
fn Parser::flush_text(self : Parser, out : Array[Event]) -> Unit {
if self.text.length() > 0 {
let text = chars_to_string(self.text)
out.push({
kind: Text,
raw: text,
payload: text,
final_byte: None,
offset: self.offset - self.text.length(),
complete: true,
})
self.text = []
}
}
///|
fn Parser::begin_sequence(self : Parser, mode : ParserMode, c : Char) -> Unit {
self.mode = mode
self.sequence = [c]
self.payload = []
self.sequence_start = self.offset
}
///|
fn Parser::emit_sequence(
self : Parser,
out : Array[Event],
kind : EventKind,
final_byte : Char?,
complete : Bool,
) -> Unit {
out.push({
kind,
raw: chars_to_string(self.sequence),
payload: chars_to_string(self.payload),
final_byte,
offset: self.sequence_start,
complete,
})
self.mode = Ground
self.sequence = []
self.payload = []
}
///|
fn Parser::emit_single(self : Parser, out : Array[Event], c : Char) -> Unit {
out.push({
kind: if is_c1(c) {
C1
} else {
C0
},
raw: String::from_array([c]),
payload: "",
final_byte: Some(c),
offset: self.offset,
complete: true,
})
}
///|
fn Parser::consume_ground(self : Parser, out : Array[Event], c : Char) -> Unit {
if c == '\u{1b}' {
self.flush_text(out)
self.begin_sequence(EscapeStart, c)
} else if c == '\u{9b}' {
self.flush_text(out)
self.begin_sequence(CsiEntry, c)
} else if c == '\u{9d}' {
self.flush_text(out)
self.begin_sequence(OscString, c)
} else if c == '\u{90}' {
self.flush_text(out)
self.begin_sequence(DcsString, c)
} else if c == '\u{98}' {
self.flush_text(out)
self.begin_sequence(SosString, c)
} else if c == '\u{9e}' {
self.flush_text(out)
self.begin_sequence(PmString, c)
} else if c == '\u{9f}' {
self.flush_text(out)
self.begin_sequence(ApcString, c)
} else if is_c0(c) || is_c1(c) {
if c == '\n' || c == '\t' {
self.text.push(c)
} else {
self.flush_text(out)
self.emit_single(out, c)
}
} else {
self.text.push(c)
}
}
///|
fn Parser::consume_escape(self : Parser, out : Array[Event], c : Char) -> Unit {
self.sequence.push(c)
if c == '[' {
self.mode = CsiEntry
} else if c == ']' {
self.mode = OscString
} else if c == 'P' {
self.mode = DcsString
} else if c == 'X' {
self.mode = SosString
} else if c == '^' {
self.mode = PmString
} else if c == '_' {
self.mode = ApcString
} else if is_intermediate_byte(c) {
self.payload.push(c)
self.mode = EscapeIntermediate
} else if is_final_byte(c) {
self.emit_sequence(out, Escape, Some(c), true)
} else {
self.payload.push(c)
self.emit_sequence(out, Invalid, None, false)
}
}
///|
fn Parser::consume_escape_intermediate(
self : Parser,
out : Array[Event],
c : Char,
) -> Unit {
self.sequence.push(c)
if is_intermediate_byte(c) {
self.payload.push(c)
} else if is_final_byte(c) {
self.emit_sequence(out, Escape, Some(c), true)
} else {
self.payload.push(c)
self.emit_sequence(out, Invalid, None, false)
}
}
///|
fn Parser::consume_csi(self : Parser, out : Array[Event], c : Char) -> Unit {
self.sequence.push(c)
match self.mode {
CsiEntry | CsiParam =>
if is_parameter_byte(c) {
self.payload.push(c)
self.mode = CsiParam
} else if is_intermediate_byte(c) {
self.payload.push(c)
self.mode = CsiIntermediate
} else if is_final_byte(c) {
self.emit_sequence(out, Csi, Some(c), true)
} else if is_c0(c) {
// Embedded format effectors do not terminate a CSI. Retaining them in
// `raw` makes the event lossless while omitting them from parameters.
()
} else {
self.payload.push(c)
self.emit_sequence(out, Invalid, None, false)
}
CsiIntermediate =>
if is_intermediate_byte(c) {
self.payload.push(c)
} else if is_final_byte(c) {
self.emit_sequence(out, Csi, Some(c), true)
} else {
self.payload.push(c)
self.emit_sequence(out, Invalid, None, false)
}
_ => ()
}
}
///|
fn Parser::consume_osc(self : Parser, out : Array[Event], c : Char) -> Unit {
self.sequence.push(c)
if c == '\u{7}' {
self.emit_sequence(out, Osc, None, true)
} else if c == '\u{9c}' {
self.emit_sequence(out, Osc, None, true)
} else if c == '\u{1b}' {
self.mode = OscEscape
} else {
self.payload.push(c)
}
}
///|
fn Parser::consume_osc_escape(
self : Parser,
out : Array[Event],
c : Char,
) -> Unit {
self.sequence.push(c)
if c == '\\' {
self.emit_sequence(out, Osc, None, true)
} else {
self.payload.push('\u{1b}')
self.payload.push(c)
self.mode = OscString
}
}
///|
fn string_kind(mode : ParserMode) -> EventKind {
match mode {
DcsString | DcsEscape => Dcs
SosString | SosEscape => Sos
PmString | PmEscape => Pm
ApcString | ApcEscape => Apc
_ => Invalid
}
}
///|
fn string_escape_mode(mode : ParserMode) -> ParserMode {
match mode {
DcsString => DcsEscape
SosString => SosEscape
PmString => PmEscape
ApcString => ApcEscape
_ => mode
}
}
///|
fn string_body_mode(mode : ParserMode) -> ParserMode {
match mode {
DcsEscape => DcsString
SosEscape => SosString
PmEscape => PmString
ApcEscape => ApcString
_ => mode
}
}
///|
fn Parser::consume_control_string(
self : Parser,
out : Array[Event],
c : Char,
) -> Unit {
let old_mode = self.mode
self.sequence.push(c)
match old_mode {
DcsEscape | SosEscape | PmEscape | ApcEscape =>
if c == '\\' {
self.emit_sequence(out, string_kind(old_mode), None, true)
} else {
self.payload.push('\u{1b}')
self.payload.push(c)
self.mode = string_body_mode(old_mode)
}
_ =>
if c == '\u{9c}' {
self.emit_sequence(out, string_kind(old_mode), None, true)
} else if c == '\u{1b}' {
self.mode = string_escape_mode(old_mode)
} else {
self.payload.push(c)
}
}
}
///|
/// Feed a chunk and return all events completed by that chunk. Text is held
/// until a control boundary or `finish`, so adjacent chunks coalesce naturally.
pub fn Parser::feed(self : Parser, chunk : String) -> Array[Event] {
let out : Array[Event] = []
for c in chunk {
match self.mode {
Ground => self.consume_ground(out, c)
EscapeStart => self.consume_escape(out, c)
EscapeIntermediate => self.consume_escape_intermediate(out, c)
CsiEntry | CsiParam | CsiIntermediate => self.consume_csi(out, c)
OscString => self.consume_osc(out, c)
OscEscape => self.consume_osc_escape(out, c)
DcsString
| DcsEscape
| SosString
| SosEscape
| PmString
| PmEscape
| ApcString
| ApcEscape => self.consume_control_string(out, c)
}
self.offset += 1
}
out
}
///|
/// Flush remaining text and turn an unfinished control string into a Truncated
/// event. Calling `finish` resets the parser to Ground and is idempotent.
pub fn Parser::finish(self : Parser) -> Array[Event] {
let out : Array[Event] = []
if self.mode == Ground {
self.flush_text(out)
} else {
self.emit_sequence(out, Truncated, None, false)
}
out
}
///|
/// Convenience batch parser.
pub fn parse(input : String) -> Array[Event] {
let parser = Parser::new()
let events = parser.feed(input)
for event in parser.finish() {
events.push(event)
}
events
}