///|
pub fn Diagnostic::hint(self : Diagnostic) -> String {
match self.code {
"missing_header" | "invalid_version" | "header_order" =>
"Use an RTF file whose root group begins with \\rtf1; DOC and DOCX are different formats."
"text_outside_root" | "extra_root" | "control_outside_root" =>
"Remove concatenated documents or bytes outside the single root group."
"unexpected_close" =>
"Balance group braces. Literal braces in text must be escaped."
"unclosed_group" =>
"Check whether the file was truncated during transfer; recovery can expose partial text."
"truncated_escape" | "truncated_hex" | "invalid_hex" =>
"Check the backslash escape and ensure hexadecimal escapes contain exactly two hex digits."
"parameter_overflow" | "missing_parameter" =>
"Check the numeric argument attached to the control word."
"control_word_too_long" =>
"RTF control word names may contain at most 32 ASCII letters."
"invalid_binary_length" | "truncated_binary" =>
"The binary length must match the bytes following the control, including any braces."
"unicode_range" | "unpaired_surrogate" =>
"Check Unicode values and surrogate pairs; ensure fallback characters are counted correctly."
"fallback_range" =>
"Use a nonnegative \\uc count and check its group scope."
"truncated_utf8" | "invalid_utf8" =>
"Check UTF-8 bytes under code page 65001; a multibyte sequence cannot cross a formatting boundary."
"undefined_codepoint" =>
"This byte is undefined in Windows-1252; verify the declared code page."
"invalid_codepage" =>
"Use a positive code-page identifier; Unicode escapes avoid legacy encoding ambiguity."
"input_limit" | "scan_limit" =>
"Use a smaller input or explicitly raise the input limit for trusted data."
"depth_limit" =>
"Inspect deeply nested groups before increasing the nesting limit."
"node_limit" =>
"Reduce fragmentation or increase the node limit after checking the file size."
"output_limit" =>
"Increase the decoded-text limit only if the expected document size is known."
"invalid_limits" => "All parser limits must be positive integers."
"duplicate_font" | "duplicate_font_id" =>
"Ensure each font identifier has one consistent declaration."
"undefined_font" =>
"Add the missing font declaration or select an existing font identifier."
"color_range" | "color_index_range" | "undefined_color" =>
"Check color-table indices and RGB channel values from 0 through 255."
"font_size_range" =>
"RTF font sizes use positive half-points; 24 represents 12 pt."
"invalid_timestamp" =>
"Provide a valid year, month and day; omitted time components default to zero."
"complex_table" | "table_cell_order" | "incomplete_table_address" =>
"Export a simple single-level table or inspect the structured output for lost layout."
"unsafe_link" =>
"Only http, https, mailto and fragment links are activated by HTML and Markdown export."
"embedded_content" =>
"Images and objects remain metadata; use an external renderer for their visual content."
"list_numbering" =>
"Automatic list labels are not reconstructed; use explicit text labels if required."
"page_layout" =>
"Page and section layout is intentionally reduced to logical paragraph boundaries."
"unknown_destination" =>
"Inspect the original file if the omitted destination contains important custom data."
"unsupported_drawing" =>
"The file contains drawing or equation content outside the supported extraction subset."
"nul_text" =>
"An embedded NUL was discarded; check the producer's text encoding."
"invalid_paragraph_span" | "invalid_run_span" =>
"Source ranges refer to the original bytes; do not reuse them after editing the input."
_ =>
if self.code.has_prefix("unsupported_codepage_") {
"Convert this document to Unicode RTF or a supported code page before extracting it."
} else if self.code.has_prefix("unknown_control_") {
"The control was ignored; check the support matrix if formatting or text is missing."
} else if self.code.has_prefix("unknown_symbol_") {
"Check whether this symbol is a typo or a producer-specific extension."
} else {
"Inspect the source range and the project's support matrix."
}
}
}
///|
pub(all) struct DiagnosticSummary {
errors : Int
warnings : Int
distinct_codes : Int
first_error : Int?
} derive(Eq, Debug, ToJson)
///|
pub fn summarize_diagnostics(
diagnostics : Array[Diagnostic],
) -> DiagnosticSummary {
let codes : Map[String, Bool] = Map([])
let mut errors = 0
let mut warnings = 0
let mut first_error : Int? = None
for diagnostic in diagnostics {
codes[diagnostic.code] = true
if diagnostic.is_error() {
errors += 1
if first_error is None {
first_error = Some(diagnostic.span.start)
}
} else {
warnings += 1
}
}
{ errors, warnings, distinct_codes: codes.length(), first_error, }
}
///|
/// Terminal-safe context; input control bytes are escaped before display.
pub fn explain_diagnostic(
input : Bytes,
diagnostic : Diagnostic,
context_bytes? : Int = 24,
) -> String {
let out = StringBuilder()
out.write_string(format_diagnostic(input, diagnostic))
out.write_char('\n')
out.write_string(" source: ")
out.write_string(source_excerpt(input, diagnostic.span, radius=context_bytes))
out.write_char('\n')
out.write_string(" hint: ")
out.write_string(diagnostic.hint())
out.to_string()
}
///|
pub fn explain_diagnostics(
input : Bytes,
diagnostics : Array[Diagnostic],
max_messages? : Int = 20,
) -> String {
let out = StringBuilder()
let count = max_messages.max(0).min(diagnostics.length())
for i in 0.. 0 {
out.write_string("\n\n")
}
out.write_string(explain_diagnostic(input, diagnostics[i]))
}
if diagnostics.length() > count {
out.write_string(
"\n\{diagnostics.length() - count} additional diagnostics omitted",
)
}
out.to_string()
}