///|
/// Parse a bounded RTF byte buffer. Recovery never bypasses resource limits.
pub fn parse(
input : Bytes,
recover? : Bool = false,
limits? : Limits = Limits::default(),
) -> ParseResult {
let parser = Parser::new(input, limits, recover)
if !limits.valid() {
parser.report(
"invalid_limits",
Error,
"All resource limits must be positive",
{ start: 0, end: 0, },
)
parser.stopped = true
} else if input.length() > limits.input_bytes {
parser.report("input_limit", Error, "Input byte limit exceeded", {
start: 0,
end: 0,
})
parser.stopped = true
}
while !parser.stopped {
let token = parser.lexer.next()
if token.kind == End {
break
}
parser.consume(token)
}
parser.flush_utf8()
parser.flush_surrogate()
parser.finish_paragraph(parser.lexer.pos, false)
let end : Span = { start: parser.lexer.pos, end: parser.lexer.pos, }
if !parser.stopped {
if !parser.stack.is_empty() {
parser.report(
"unclosed_group",
Error,
"Input ended before all groups were closed",
end,
)
}
if !parser.seen_version {
parser.report(
"missing_header",
Error,
"Input does not contain an RTF version header",
end,
)
}
}
{
document: {
paragraphs: parser.paragraphs,
fonts: parser.fonts,
colors: parser.colors,
metadata: parser.metadata,
attachments: parser.attachments,
input_bytes: input.length(),
},
diagnostics: parser.diagnostics,
complete: parser.complete,
}
}
///|
fn Parser::consume(self : Parser, token : Token) -> Unit {
let span = token.span
if self.stack.is_empty() {
match token.kind {
Open | Close | Text | End => ()
_ => {
self.report(
"control_outside_root",
Error,
"Escapes and controls must occur inside the root group",
span,
)
return
}
}
}
if !self.seen_version {
match token.kind {
Word(name, _) if name != "rtf" => {
self.report(
"header_order",
Error,
"The first control word must be the RTF version header",
span,
)
if self.stopped {
return
}
}
Hex(_) | Symbol(_) | Binary => {
self.report(
"header_order",
Error,
"Escapes must follow the RTF header",
span,
)
return
}
Open if !self.stack.is_empty() => {
self.report(
"header_order",
Error,
"Nested groups must follow the RTF header",
span,
)
if self.stopped {
return
}
}
_ => ()
}
}
match token.kind {
Open => {
self.flush_utf8()
self.fallback = 0
self.flush_surrogate()
self.flush_run()
if self.closed_root {
self.report(
"extra_root",
Error,
"Only one root group is permitted",
span,
)
self.stopped = true
return
}
if self.stack.length() >= self.limits.depth {
self.report("depth_limit", Error, "Group nesting limit exceeded", span)
self.stopped = true
return
}
self.stack.push(self.state)
self.state = self.state.child(span.start)
}
Close => {
self.flush_utf8()
self.fallback = 0
self.flush_surrogate()
self.flush_run()
if self.stack.pop() is Some(parent) {
self.close_destination(span)
if self.stack.is_empty() {
self.finish_paragraph(span.end, false)
self.closed_root = true
}
self.state = parent
} else {
self.report(
"unexpected_close",
Error,
"Closing brace has no matching open group",
span,
)
}
}
Bad(code) => self.report(code, Error, "Malformed RTF token: \{code}", span)
Text =>
for i in span.start.. self.emit_byte(b, span)
Word(name, value) =>
if self.stack.is_empty() {
self.report(
"control_outside_root",
Error,
"Control word outside the root group",
span,
)
} else if self.fallback > 0 {
self.fallback -= 1
} else {
self.control(name, value, span)
}
Symbol(symbol) =>
if self.fallback > 0 {
self.fallback -= 1
} else {
self.symbol(symbol, span)
}
Binary => ()
End => ()
}
}
///|
fn Parser::symbol(self : Parser, symbol : Int, span : Span) -> Unit {
self.flush_utf8()
if self.state.destination == "skip" {
return
}
if symbol == 42 {
self.state.starred = true
return
}
self.flush_surrogate()
match symbol {
92 | 123 | 125 => self.append_scalar(symbol, span)
126 => self.append_scalar(0xA0, span)
95 => self.append_scalar(0x2011, span)
45 => self.append_scalar(0xAD, span)
10 | 13 => self.finish_paragraph(span.end, true)
_ =>
self.warn_once(
"unknown_symbol_\{symbol}",
"Unrecognized control symbol was omitted",
span,
)
}
}
///|
pub fn extract_text(
input : Bytes,
recover? : Bool = false,
include_hidden? : Bool = false,
) -> String {
let parsed = parse(input, recover~)
let out = StringBuilder()
let mut first = true
for paragraph in parsed.document.paragraphs {
if paragraph.region != Body {
continue
}
if !first {
out.write_char('\n')
}
first = false
out.write_string(paragraph.text(include_hidden~))
}
out.to_string()
}