use std::collections::{HashMap, HashSet};
use docling_core::{Table, TableCell};
use super::iwork::{
decode_iwa, first_bytes, first_varint, grid_table, parse_archives, reference, text_cell,
Archive, Fields, Value,
};
use super::ooxml::Package;
use super::pages::{
authored, runs_for, script_of, split_paragraphs, unique_paragraphs, value_at, Block, Comment,
Content, Formatting, ListStyle, Paragraph, Picture, StorageRuns,
};
use crate::error::ConversionError;
const MAX_REFERENCE_DEPTH: usize = 4;
const REFERENCE_MAX_BYTES: usize = 6;
const TSWP_CHARACTER_STYLE: u32 = 2021;
const STORAGE_CHARACTER_STYLE_FIELD: u32 = 8;
const STYLE_PROPERTIES_FIELD: u32 = 11;
const STYLE_SCRIPT_FIELD: u32 = 10;
const PROPERTY_BOLD: u32 = 1;
const PROPERTY_ITALIC: u32 = 2;
const PROPERTY_UNDERLINE: u32 = 11;
const PROPERTY_STRIKETHROUGH: u32 = 12;
const TSWP_SHAPE_INFO: u32 = 2011;
const DOCUMENT_DRAWABLES_FIELD: u32 = 20;
const TP_DOCUMENT_ARCHIVE: u32 = 10000;
const TSWP_STORAGE_ARCHIVE: u32 = 2001;
const TSWP_PARAGRAPH_STYLE: u32 = 2022;
const DOCUMENT_BODY_FIELD: u32 = 4;
const STORAGE_TEXT_FIELD: u32 = 3;
const STYLE_SUPER_FIELD: u32 = 1;
const STYLE_NAME_FIELD: u32 = 1;
const TST_TABLE_MODEL: u32 = 6001;
const TST_TABULAR_INFO: u32 = 6000;
const TABULAR_INFO_MODEL_FIELD: u32 = 2;
const TSD_IMAGE: u32 = 3005;
const TSD_GROUP: u32 = 3008;
const GROUP_CHILDREN_FIELD: u32 = 2;
const IMAGE_DATA_FIELDS: [u32; 4] = [15, 13, 11, 12];
const TSWP_DRAWABLE_ATTACHMENT: u32 = 2003;
const ATTACHMENT_DRAWABLE_FIELD: u32 = 1;
const STORAGE_ATTACHMENT_FIELD: u32 = 9;
const STORAGE_COMMENT_FIELD: u32 = 23;
const TSWP_COMMENT_FIELD: u32 = 2013;
const COMMENT_FIELD_STORAGE_FIELD: u32 = 1;
const TSD_COMMENT_STORAGE: u32 = 3056;
const COMMENT_TEXT_FIELD: u32 = 1;
const COMMENT_AUTHOR_FIELD: u32 = 3;
const COMMENT_REPLIES_FIELD: u32 = 4;
const TSK_ANNOTATION_AUTHOR: u32 = 212;
const AUTHOR_NAME_FIELD: u32 = 1;
const TSWP_NOTE: u32 = 2008;
const NOTE_STORAGE_FIELD: u32 = 2;
const STORAGE_FOOTNOTE_FIELD: u32 = 16;
const STORAGE_PAGE_MASTER_FIELD: u32 = 17;
const TP_PAGE_MASTER: u32 = 10011;
const PAGE_MASTER_HEADER_FOOTER_FIELDS: [u32; 3] = [23, 24, 25];
const TP_HEADERS_AND_FOOTERS: u32 = 10143;
const HEADERS_FIELD: u32 = 1;
const FOOTERS_FIELD: u32 = 2;
const TSP_PACKAGE_METADATA: u32 = 11006;
const PACKAGE_DATAS_FIELD: u32 = 4;
const DATA_INFO_IDENTIFIER_FIELD: u32 = 1;
const DATA_INFO_PREFERRED_NAME_FIELD: u32 = 3;
const DATA_INFO_NAME_FIELD: u32 = 4;
const DATA_MEMBER_PREFIX: &str = "Data/";
const TST_TILE: u32 = 6002;
const TST_DATA_LIST: u32 = 6005;
const TABLE_ROWS_FIELD: u32 = 6;
const TABLE_COLS_FIELD: u32 = 7;
const TABLE_HEADER_ROWS_FIELD: u32 = 9;
const TABLE_DATA_STORE_FIELD: u32 = 4;
const STORE_TILES_FIELD: u32 = 3;
const STORE_STRINGS_FIELD: u32 = 4;
const STORE_RICH_TEXT_FIELD: u32 = 17;
const LIST_ENTRIES_FIELD: u32 = 3;
const LIST_SEGMENTS_FIELD: u32 = 4;
const ENTRY_KEY_FIELD: u32 = 1;
const ENTRY_STRING_FIELD: u32 = 3;
const ENTRY_RICH_TEXT_FIELD: u32 = 9;
const TST_TEXT_REF: u32 = 6218;
const TILE_ROWS_FIELD: u32 = 5;
const ROW_INDEX_FIELD: u32 = 1;
const ROW_STORAGE_FIELD: u32 = 3;
const ROW_OFFSETS_FIELD: u32 = 4;
const ROW_WIDE_STORAGE_FIELD: u32 = 6;
const ROW_WIDE_OFFSETS_FIELD: u32 = 7;
const ROW_WIDE_OFFSETS_FLAG: u32 = 8;
const CELL_VERSION_LEGACY: u8 = 4;
const CELL_VERSION_CURRENT: u8 = 5;
const CELL_TYPE_TEXT: u8 = 3;
const CELL_TYPE_RICH_TEXT: u8 = 9;
const CELL_KEY_OFFSET: usize = 16;
const CELL_FLAGS_OFFSET: usize = 8;
const CELL_VALUES_OFFSET: usize = 12;
const CELL_FLAG_STRING: u32 = 0x8;
const CELL_FLAG_RICH_TEXT: u32 = 0x10;
const CELL_VALUE_WIDTHS: [(u32, usize); 5] = [
(0x1, 16),
(0x2, 8),
(0x4, 8),
(CELL_FLAG_STRING, 4),
(CELL_FLAG_RICH_TEXT, 4),
];
const STORAGE_PARAGRAPH_STYLE_FIELD: u32 = 5;
const TSWP_LINK_FIELD: u32 = 2032;
const LINK_URL_FIELD: u32 = 2;
const STORAGE_SMART_FIELD: u32 = 11;
const TSWP_LIST_STYLE: u32 = 2023;
const STORAGE_LIST_DEPTH_FIELD: u32 = 6;
const STORAGE_LIST_STYLE_FIELD: u32 = 7;
const LIST_LABEL_TYPES_FIELD: u32 = 11;
const LIST_STRINGS_FIELD: u32 = 16;
type Objects<'a> = HashMap<u64, &'a Archive>;
fn fail(msg: &str) -> ConversionError {
ConversionError::Parse(format!("iwork: {msg}"))
}
pub(crate) fn read_content(pkg: &mut Package) -> Result<Content, ConversionError> {
let names: Vec<String> = pkg
.names()
.filter(|n| n.ends_with(".iwa"))
.map(str::to_string)
.collect();
let mut archives: Vec<Archive> = Vec::new();
for name in &names {
let Some(bytes) = pkg.read_bytes(name) else {
continue;
};
let stream = decode_iwa(&bytes)?;
parse_archives(&stream, &mut archives, true);
}
let mut order: Vec<u64> = Vec::new();
let mut objects: Objects = HashMap::new();
for a in &archives {
if objects.insert(a.id, a).is_none() {
order.push(a.id);
}
}
let document = order
.iter()
.filter_map(|id| objects.get(id).copied())
.find(|a| a.ty == TP_DOCUMENT_ARCHIVE)
.ok_or_else(|| {
fail(
"the Pages document has no TP.DocumentArchive; the container may be corrupt \
or password-protected",
)
})?;
let storage = first_bytes(&document.payload, DOCUMENT_BODY_FIELD)
.and_then(reference)
.and_then(|id| objects.get(&id).copied())
.filter(|a| a.ty == TSWP_STORAGE_ARCHIVE)
.ok_or_else(|| fail("the Pages document does not reference a body text storage"))?;
let mut reader = Reader {
objects: &objects,
order: &order,
pkg,
data_files: HashMap::new(),
emitted: HashSet::new(),
};
reader.data_files = reader.data_files();
let mut blocks = reader.storage_blocks(storage);
blocks.extend(reader.floating_blocks(document));
let (headers, footers) = reader.page_furniture(storage);
Ok(Content {
blocks,
headers,
footers,
footnotes: reader.footnotes(storage),
comments: reader.comments(storage),
})
}
fn style_name(payload: &[u8]) -> Option<String> {
let super_message = first_bytes(payload, STYLE_SUPER_FIELD)?;
let name = first_bytes(super_message, STYLE_NAME_FIELD)?;
std::str::from_utf8(name).ok().map(str::to_string)
}
fn storage_text(payload: &[u8]) -> Vec<char> {
let mut text = String::new();
for (f, v) in Fields::new(payload) {
if let (STORAGE_TEXT_FIELD, Value::Bytes(b)) = (f, v) {
text.push_str(&String::from_utf8_lossy(b));
}
}
text.chars().collect()
}
fn object_runs<T>(
payload: &[u8],
field: u32,
objects: &Objects,
message_type: u32,
decode: impl Fn(&[u8]) -> Option<T>,
) -> Vec<(usize, Option<T>)> {
let Some(table) = first_bytes(payload, field) else {
return Vec::new();
};
let mut runs: Vec<(usize, Option<T>)> = Vec::new();
for (f, v) in Fields::new(table) {
let (1, Value::Bytes(entry)) = (f, v) else {
continue;
};
let Some(index) = first_varint(entry, 1) else {
continue;
};
let value = first_bytes(entry, 2)
.and_then(reference)
.and_then(|id| objects.get(&id))
.filter(|a| a.ty == message_type)
.and_then(|a| decode(&a.payload));
runs.push((index as usize, value));
}
runs.sort_by_key(|run| run.0);
runs
}
fn depth_runs(payload: &[u8]) -> Vec<(usize, usize)> {
let Some(table) = first_bytes(payload, STORAGE_LIST_DEPTH_FIELD) else {
return Vec::new();
};
let mut runs = Vec::new();
for (f, v) in Fields::new(table) {
let (1, Value::Bytes(entry)) = (f, v) else {
continue;
};
if let (Some(index), Some(depth)) = (first_varint(entry, 1), first_varint(entry, 2)) {
runs.push((index as usize, depth as usize));
}
}
runs.sort_by_key(|run| run.0);
runs
}
fn attachment_runs(payload: &[u8], field: u32) -> Vec<(usize, u64)> {
let Some(table) = first_bytes(payload, field) else {
return Vec::new();
};
let mut runs = Vec::new();
for (f, v) in Fields::new(table) {
let (1, Value::Bytes(entry)) = (f, v) else {
continue;
};
if let (Some(index), Some(target)) = (
first_varint(entry, 1),
first_bytes(entry, 2).and_then(reference),
) {
runs.push((index as usize, target));
}
}
runs.sort_by_key(|run| run.0);
runs
}
fn link(payload: &[u8]) -> Option<String> {
let url = first_bytes(payload, LINK_URL_FIELD)?;
let url = String::from_utf8_lossy(url).trim().to_string();
(!url.is_empty()).then_some(url)
}
fn list_style(payload: &[u8]) -> Option<ListStyle> {
let mut style = ListStyle::default();
for (f, v) in Fields::new(payload) {
match (f, v) {
(LIST_LABEL_TYPES_FIELD, Value::Varint(n)) => style.label_types.push(n),
(LIST_STRINGS_FIELD, Value::Bytes(b)) => {
style.strings.push(String::from_utf8_lossy(b).into_owned())
}
_ => {}
}
}
Some(style)
}
fn formatting(payload: &[u8]) -> Option<Formatting> {
let properties = first_bytes(payload, STYLE_PROPERTIES_FIELD)?;
let (mut bold, mut italic, mut underline, mut strike) = (false, false, false, false);
let mut script = None;
for (f, v) in Fields::new(properties) {
let Value::Varint(n) = v else {
continue;
};
match f {
PROPERTY_BOLD if n != 0 => bold = true,
PROPERTY_ITALIC if n != 0 => italic = true,
PROPERTY_UNDERLINE if n != 0 => underline = true,
PROPERTY_STRIKETHROUGH if n != 0 => strike = true,
STYLE_SCRIPT_FIELD if script.is_none() => script = Some(n),
_ => {}
}
}
Formatting::build(bold, italic, underline, strike, script.and_then(script_of))
}
fn storage_runs(payload: &[u8], objects: &Objects) -> StorageRuns {
StorageRuns {
styles: object_runs(
payload,
STORAGE_PARAGRAPH_STYLE_FIELD,
objects,
TSWP_PARAGRAPH_STYLE,
style_name,
),
characters: object_runs(
payload,
STORAGE_CHARACTER_STYLE_FIELD,
objects,
TSWP_CHARACTER_STYLE,
formatting,
),
lists: object_runs(
payload,
STORAGE_LIST_STYLE_FIELD,
objects,
TSWP_LIST_STYLE,
list_style,
),
depths: depth_runs(payload),
links: object_runs(payload, STORAGE_SMART_FIELD, objects, TSWP_LINK_FIELD, link),
}
}
struct CellValues {
strings: HashMap<u32, String>,
rich_text: HashMap<u32, String>,
}
fn table(model: &Archive, objects: &Objects) -> Option<Table> {
let num_rows = first_varint(&model.payload, TABLE_ROWS_FIELD)? as usize;
let num_cols = first_varint(&model.payload, TABLE_COLS_FIELD)? as usize;
let store = first_bytes(&model.payload, TABLE_DATA_STORE_FIELD)?;
if num_rows == 0 || num_cols == 0 {
return None;
}
let header_rows = first_varint(&model.payload, TABLE_HEADER_ROWS_FIELD).unwrap_or(0) as usize;
let values = CellValues {
strings: value_list(store, STORE_STRINGS_FIELD, objects, entry_string),
rich_text: value_list(store, STORE_RICH_TEXT_FIELD, objects, entry_rich_text),
};
let mut cells: Vec<TableCell> = Vec::new();
for tile in tiles(store, objects) {
tile_cells(tile, &values, num_cols, header_rows, &mut cells);
}
if cells.is_empty() {
return None;
}
Some(grid_table(num_rows, num_cols, cells))
}
fn value_list(
store: &[u8],
field: u32,
objects: &Objects,
decode: impl Fn(&[u8], &Objects) -> Option<String>,
) -> HashMap<u32, String> {
let mut values = HashMap::new();
let Some(list) = first_bytes(store, field)
.and_then(reference)
.and_then(|id| objects.get(&id))
.filter(|a| a.ty == TST_DATA_LIST)
else {
return values;
};
let mut payloads: Vec<&[u8]> = vec![&list.payload];
for segment in reference_list(&list.payload, LIST_SEGMENTS_FIELD) {
if let Some(spilled) = objects.get(&segment) {
payloads.push(&spilled.payload);
}
}
for payload in payloads {
for (f, v) in Fields::new(payload) {
let (LIST_ENTRIES_FIELD, Value::Bytes(entry)) = (f, v) else {
continue;
};
if let (Some(key), Some(value)) =
(first_varint(entry, ENTRY_KEY_FIELD), decode(entry, objects))
{
values.insert(key as u32, value);
}
}
}
values
}
fn entry_string(entry: &[u8], _objects: &Objects) -> Option<String> {
first_bytes(entry, ENTRY_STRING_FIELD).map(|b| String::from_utf8_lossy(b).into_owned())
}
fn entry_rich_text(entry: &[u8], objects: &Objects) -> Option<String> {
let indirection = first_bytes(entry, ENTRY_RICH_TEXT_FIELD)
.and_then(reference)
.and_then(|id| objects.get(&id))
.filter(|a| a.ty == TST_TEXT_REF)?;
let storage = reference_field(&indirection.payload, 1)
.and_then(|id| objects.get(&id))
.filter(|a| a.ty == TSWP_STORAGE_ARCHIVE)?;
let text: String = storage_text(&storage.payload).into_iter().collect();
let text = text.trim().to_string();
(!text.is_empty()).then_some(text)
}
fn tiles<'a>(store: &[u8], objects: &Objects<'a>) -> Vec<&'a Archive> {
let Some(container) = first_bytes(store, STORE_TILES_FIELD) else {
return Vec::new();
};
Fields::new(container)
.filter_map(|(f, v)| match (f, v) {
(1, Value::Bytes(entry)) => first_bytes(entry, 2)
.and_then(reference)
.and_then(|id| objects.get(&id).copied())
.filter(|a| a.ty == TST_TILE),
_ => None,
})
.collect()
}
fn tile_cells(
tile: &Archive,
values: &CellValues,
num_cols: usize,
header_rows: usize,
out: &mut Vec<TableCell>,
) {
for (f, v) in Fields::new(&tile.payload) {
let (TILE_ROWS_FIELD, Value::Bytes(row)) = (f, v) else {
continue;
};
let Some(row_index) = first_varint(row, ROW_INDEX_FIELD) else {
continue;
};
let wide = (
first_bytes(row, ROW_WIDE_STORAGE_FIELD),
first_bytes(row, ROW_WIDE_OFFSETS_FIELD),
);
let (storage, offsets, scale) = match wide {
(Some(s), Some(o)) => {
let scale = if first_varint(row, ROW_WIDE_OFFSETS_FLAG).unwrap_or(0) != 0 {
4
} else {
1
};
(s, o, scale)
}
_ => match (
first_bytes(row, ROW_STORAGE_FIELD),
first_bytes(row, ROW_OFFSETS_FIELD),
) {
(Some(s), Some(o)) => (s, o, 1),
_ => continue,
},
};
let row_index = row_index as usize;
for column in 0..num_cols.min(offsets.len() / 2) {
let start = i16::from_le_bytes([offsets[2 * column], offsets[2 * column + 1]]);
let Some(text) = cell_text(storage, i64::from(start) * scale, values) else {
continue;
};
out.push(text_cell(text, row_index, column, header_rows));
}
}
}
fn cell_text(storage: &[u8], start: i64, values: &CellValues) -> Option<String> {
if start < 0 {
return None;
}
let start = start as usize;
if start.checked_add(CELL_VALUES_OFFSET)? > storage.len() {
return None;
}
let version = storage[start];
if version == CELL_VERSION_LEGACY {
if storage[start + 1] != CELL_TYPE_TEXT {
return None;
}
let key = read_u32(storage, start + CELL_KEY_OFFSET)?;
return values.strings.get(&key).cloned();
}
if version != CELL_VERSION_CURRENT {
return None;
}
if !matches!(storage[start + 1], CELL_TYPE_TEXT | CELL_TYPE_RICH_TEXT) {
return None;
}
let flags = read_u32(storage, start + CELL_FLAGS_OFFSET)?;
let mut offset = start + CELL_VALUES_OFFSET;
for (flag, width) in CELL_VALUE_WIDTHS {
if flags & flag == 0 {
continue;
}
if offset + width > storage.len() {
return None;
}
if flag == CELL_FLAG_STRING {
return values.strings.get(&read_u32(storage, offset)?).cloned();
}
if flag == CELL_FLAG_RICH_TEXT {
return values.rich_text.get(&read_u32(storage, offset)?).cloned();
}
offset += width;
}
None
}
fn read_u32(buffer: &[u8], at: usize) -> Option<u32> {
Some(u32::from_le_bytes(buffer.get(at..at + 4)?.try_into().ok()?))
}
fn reference_field(payload: &[u8], field: u32) -> Option<u64> {
first_bytes(payload, field).and_then(reference)
}
fn reference_list(payload: &[u8], field: u32) -> Vec<u64> {
Fields::new(payload)
.filter_map(|(f, v)| match v {
Value::Bytes(b) if f == field => reference(b),
_ => None,
})
.collect()
}
fn referenced_ids(payload: &[u8], depth: usize, found: &mut HashSet<u64>) {
if depth > MAX_REFERENCE_DEPTH {
return;
}
for (_, v) in Fields::new(payload) {
let Value::Bytes(b) = v else {
continue;
};
if b.len() <= REFERENCE_MAX_BYTES {
if let Some(target) = reference(b) {
found.insert(target);
}
} else {
referenced_ids(b, depth + 1, found);
}
}
}
struct Reader<'a, 'p> {
objects: &'a Objects<'a>,
order: &'a [u64],
pkg: &'p mut Package,
data_files: HashMap<u64, String>,
emitted: HashSet<u64>,
}
impl<'a> Reader<'a, '_> {
fn object(&self, id: u64) -> Option<&'a Archive> {
self.objects.get(&id).copied()
}
fn typed(&self, id: u64, ty: u32) -> Option<&'a Archive> {
self.object(id).filter(|a| a.ty == ty)
}
fn storage_blocks(&mut self, storage: &Archive) -> Vec<Block> {
let text = storage_text(&storage.payload);
let runs = storage_runs(&storage.payload, self.objects);
let attachments = attachment_runs(&storage.payload, STORAGE_ATTACHMENT_FIELD);
let comments = attachment_runs(&storage.payload, STORAGE_COMMENT_FIELD);
let mut blocks = Vec::new();
let mut offset = 0usize;
for line in text.split(|&c| c == '\n') {
let end = offset + line.len() + 1;
let pieces = runs_for(line, offset, &runs);
if !pieces.is_empty() {
let label = super::pages::label_for_style(
value_at(&runs.styles, offset).flatten().as_deref(),
);
blocks.push(Block::Paragraph(Paragraph {
runs: pieces,
label,
list: super::pages::list_label_at(&runs, offset),
anchors: comments
.iter()
.filter(|(index, _)| offset <= *index && *index < end)
.map(|(_, field)| field.to_string())
.collect(),
}));
}
for (index, identifier) in &attachments {
if offset <= *index && *index < end {
blocks.extend(self.drawable_blocks(*identifier));
}
}
offset = end;
}
blocks
}
fn floating_blocks(&mut self, document: &Archive) -> Vec<Block> {
let Some(root) = reference_field(&document.payload, DOCUMENT_DRAWABLES_FIELD)
.and_then(|id| self.object(id))
else {
return Vec::new();
};
let mut ids = HashSet::new();
referenced_ids(&root.payload, 0, &mut ids);
let mut ids: Vec<u64> = ids.into_iter().collect();
ids.sort_unstable();
let mut blocks = Vec::new();
for id in ids {
blocks.extend(self.drawable_blocks(id));
}
blocks
}
fn footnotes(&self, storage: &Archive) -> Vec<Paragraph> {
let mut paragraphs = Vec::new();
for (_, identifier) in attachment_runs(&storage.payload, STORAGE_FOOTNOTE_FIELD) {
let Some(note) = self.typed(identifier, TSWP_NOTE) else {
continue;
};
paragraphs.extend(
self.storage_paragraphs(reference_field(¬e.payload, NOTE_STORAGE_FIELD)),
);
}
paragraphs
}
fn page_furniture(&self, storage: &Archive) -> (Vec<Paragraph>, Vec<Paragraph>) {
let mut headers = Vec::new();
let mut footers = Vec::new();
for (_, identifier) in attachment_runs(&storage.payload, STORAGE_PAGE_MASTER_FIELD) {
let Some(master) = self.typed(identifier, TP_PAGE_MASTER) else {
continue;
};
for field in PAGE_MASTER_HEADER_FOOTER_FIELDS {
let Some(bundle) = reference_field(&master.payload, field)
.and_then(|id| self.typed(id, TP_HEADERS_AND_FOOTERS))
else {
continue;
};
for text_id in reference_list(&bundle.payload, HEADERS_FIELD) {
headers.extend(self.storage_paragraphs(Some(text_id)));
}
for text_id in reference_list(&bundle.payload, FOOTERS_FIELD) {
footers.extend(self.storage_paragraphs(Some(text_id)));
}
}
}
(unique_paragraphs(headers), unique_paragraphs(footers))
}
fn comments(&self, storage: &Archive) -> Vec<Comment> {
let mut comments = Vec::new();
for (_, identifier) in attachment_runs(&storage.payload, STORAGE_COMMENT_FIELD) {
let Some(field) = self.typed(identifier, TSWP_COMMENT_FIELD) else {
continue;
};
let head = reference_field(&field.payload, COMMENT_FIELD_STORAGE_FIELD);
comments.extend(self.thread(head).into_iter().map(|text| Comment {
text,
anchor: identifier.to_string(),
}));
}
comments
}
fn thread(&self, head: Option<u64>) -> Vec<String> {
let mut texts = Vec::new();
let mut pending: std::collections::VecDeque<u64> = head.into_iter().collect();
let mut seen = HashSet::new();
while let Some(current) = pending.pop_front() {
if !seen.insert(current) {
continue;
}
let Some(comment) = self.typed(current, TSD_COMMENT_STORAGE) else {
continue;
};
if let Some(raw) = first_bytes(&comment.payload, COMMENT_TEXT_FIELD) {
let text = String::from_utf8_lossy(raw).trim().to_string();
if !text.is_empty() {
texts.push(authored(self.author(&comment.payload).as_deref(), &text));
}
}
pending.extend(reference_list(&comment.payload, COMMENT_REPLIES_FIELD));
}
texts
}
fn author(&self, payload: &[u8]) -> Option<String> {
let author = reference_field(payload, COMMENT_AUTHOR_FIELD)
.and_then(|id| self.typed(id, TSK_ANNOTATION_AUTHOR))?;
let name = first_bytes(&author.payload, AUTHOR_NAME_FIELD)?;
let name = String::from_utf8_lossy(name).trim().to_string();
(!name.is_empty()).then_some(name)
}
fn storage_paragraphs(&self, identifier: Option<u64>) -> Vec<Paragraph> {
let Some(storage) = identifier.and_then(|id| self.typed(id, TSWP_STORAGE_ARCHIVE)) else {
return Vec::new();
};
split_paragraphs(
&storage_text(&storage.payload),
&storage_runs(&storage.payload, self.objects),
)
}
fn drawable_blocks(&mut self, identifier: u64) -> Vec<Block> {
if !self.emitted.insert(identifier) {
return Vec::new();
}
let Some(drawable) = self.object(identifier) else {
return Vec::new();
};
match drawable.ty {
TSWP_DRAWABLE_ATTACHMENT => {
match reference_field(&drawable.payload, ATTACHMENT_DRAWABLE_FIELD) {
Some(anchored) => self.drawable_blocks(anchored),
None => Vec::new(),
}
}
TSD_IMAGE => vec![Block::Picture(self.picture(drawable))],
TST_TABULAR_INFO => {
let Some(model) = reference_field(&drawable.payload, TABULAR_INFO_MODEL_FIELD)
.and_then(|id| self.typed(id, TST_TABLE_MODEL))
else {
return Vec::new();
};
table(model, self.objects)
.map(Block::Table)
.into_iter()
.collect()
}
TSD_GROUP => {
let mut blocks = Vec::new();
for child in reference_list(&drawable.payload, GROUP_CHILDREN_FIELD) {
blocks.extend(self.drawable_blocks(child));
}
blocks
}
TSWP_SHAPE_INFO => {
let mut ids = HashSet::new();
referenced_ids(&drawable.payload, 0, &mut ids);
let mut ids: Vec<u64> = ids.into_iter().collect();
ids.sort_unstable();
let mut blocks = Vec::new();
for id in ids {
if let Some(storage) = self.typed(id, TSWP_STORAGE_ARCHIVE) {
blocks.extend(self.storage_blocks(storage));
}
}
blocks
}
_ => Vec::new(),
}
}
fn picture(&mut self, image: &Archive) -> Picture {
let mut named = String::new();
for field in IMAGE_DATA_FIELDS {
let Some(member) = reference_field(&image.payload, field)
.and_then(|id| self.data_files.get(&id))
.cloned()
else {
continue;
};
if named.is_empty() {
named = member.clone();
}
if let Some(data) = self.pkg.read_bytes(&member) {
return Picture {
data: Some(data),
name: member,
};
}
}
Picture {
data: None,
name: named,
}
}
fn data_files(&self) -> HashMap<u64, String> {
let mut files = HashMap::new();
let Some(metadata) = self
.order
.iter()
.filter_map(|id| self.object(*id))
.find(|a| a.ty == TSP_PACKAGE_METADATA)
else {
return files;
};
for (f, v) in Fields::new(&metadata.payload) {
let (PACKAGE_DATAS_FIELD, Value::Bytes(entry)) = (f, v) else {
continue;
};
let name = first_bytes(entry, DATA_INFO_NAME_FIELD)
.or_else(|| first_bytes(entry, DATA_INFO_PREFERRED_NAME_FIELD));
if let (Some(identifier), Some(name)) =
(first_varint(entry, DATA_INFO_IDENTIFIER_FIELD), name)
{
files.insert(
identifier,
format!("{DATA_MEMBER_PREFIX}{}", String::from_utf8_lossy(name)),
);
}
}
files
}
}