use std::collections::HashSet; use y_octo::{Any, Map, Value}; use super::{text_content, text_content_for_summary}; use crate::doc_parser::{ blocksuite::{DocContext, collect_child_ids, get_string}, markdown::{ DeltaToMdOptions, InlineReferencePayload, delta_value_to_inline_markdown, extract_inline_references_from_value, text_to_inline_markdown, }, table::{MarkdownTableOptions, render_markdown_table}, value::{any_as_string, value_to_string}, }; pub(super) struct DatabaseTable { pub(super) columns: Vec, pub(super) rows: Vec>, } pub(super) struct DatabaseOption { id: Option, value: Option, color: Option, } pub(super) struct DatabaseColumn { pub(super) id: String, pub(super) name: Option, pub(super) col_type: String, pub(super) options: Vec, } pub(super) fn build_database_table( block: &Map, context: &DocContext, md_options: &DeltaToMdOptions, ) -> Option { let columns = parse_database_columns(block)?; let cells_map = block.get("prop:cells").and_then(|v| v.to_map())?; let child_ids = collect_child_ids(block); let mut rows = Vec::new(); for child_id in child_ids { let row_cells = cells_map.get(&child_id).and_then(|v| v.to_map()); let mut row = Vec::new(); for column in columns.iter() { let mut cell_text = String::new(); if column.col_type == "title" { if let Some(child_block) = context.block_pool.get(&child_id) { if let Some(text_md) = text_to_inline_markdown(child_block, "prop:text", md_options) { cell_text = text_md; } else if let Some((text, _)) = text_content(child_block, "prop:text") { cell_text = text; } else if let Some((text, _)) = text_content_for_summary(child_block, "prop:text") { cell_text = text; } } } else if let Some(row_cells) = &row_cells && let Some(cell_val) = row_cells.get(&column.id).and_then(|v| v.to_map()) && let Some(value) = cell_val.get("value") { if let Some(text_md) = delta_value_to_inline_markdown(&value, md_options) { cell_text = text_md; } else { cell_text = format_cell_value(&value, column); } } row.push(cell_text); } if row.iter().any(|cell| !cell.is_empty()) { rows.push(row); } } Some(DatabaseTable { columns, rows }) } pub(super) fn database_table_markdown(table: DatabaseTable) -> Option { let mut rows = Vec::with_capacity(table.rows.len() + 1); let header = table .columns .iter() .map(|column| column.name.as_deref().unwrap_or_default().to_string()) .collect::>(); rows.push(header); rows.extend(table.rows); let options = MarkdownTableOptions::new(false, "
", true); render_markdown_table(&rows, options) } pub(super) fn database_summary_text(block: &Map, context: &DocContext) -> Option { let md_options = DeltaToMdOptions::new(None); let table = build_database_table(block, context, &md_options)?; let mut summary = String::new(); if let Some(title) = get_string(block, "prop:title") && !title.is_empty() { summary.push_str(&title); summary.push('|'); } for column in table.columns.iter() { if let Some(name) = column.name.as_ref() && !name.is_empty() { summary.push_str(name); summary.push('|'); } for option in column.options.iter() { if let Some(value) = option.value.as_ref() && !value.is_empty() { summary.push_str(value); summary.push('|'); } } } for row in table.rows.iter() { for cell_text in row.iter() { if !cell_text.is_empty() { summary.push_str(cell_text); summary.push('|'); } } } if summary.is_empty() { None } else { Some(summary) } } pub(super) fn gather_database_texts(block: &Map) -> (Vec, Option) { let mut texts = Vec::new(); let database_title = get_string(block, "prop:title"); if let Some(title) = &database_title { texts.push(title.clone()); } if let Some(columns) = parse_database_columns(block) { for column in columns.iter() { if let Some(name) = column.name.as_ref() { texts.push(name.clone()); } for option in column.options.iter() { if let Some(value) = option.value.as_ref() { texts.push(value.clone()); } } } } (texts, database_title) } pub(super) fn collect_database_cell_references(block: &Map) -> Vec { let cells_map = match block.get("prop:cells").and_then(|value| value.to_map()) { Some(map) => map, None => return Vec::new(), }; let mut refs = Vec::new(); let mut seen: HashSet<(String, String)> = HashSet::new(); for row in cells_map.values() { let Some(row_map) = row.to_map() else { continue; }; for cell in row_map.values() { let Some(cell_map) = cell.to_map() else { continue; }; let Some(value) = cell_map.get("value") else { continue; }; for reference in extract_inline_references_from_value(&value) { let key = (reference.doc_id.clone(), reference.payload.clone()); if seen.insert(key) { refs.push(reference); } } } } refs } fn parse_database_columns(block: &Map) -> Option> { let columns = block.get("prop:columns").and_then(|value| value.to_array())?; let mut parsed = Vec::new(); for column_value in columns.iter() { if let Some(column) = column_value.to_map() { let id = column .get("id") .and_then(|value| value_to_string(&value)) .unwrap_or_default(); let name = column.get("name").and_then(|value| value_to_string(&value)); let col_type = column .get("type") .and_then(|value| value_to_string(&value)) .unwrap_or_default(); let options = parse_database_options(&column); parsed.push(DatabaseColumn { id, name, col_type, options, }); } } Some(parsed) } fn parse_database_options(column: &Map) -> Vec { let Some(data) = column.get("data").and_then(|value| value.to_map()) else { return Vec::new(); }; let Some(options) = data.get("options").and_then(|value| value.to_array()) else { return Vec::new(); }; let mut parsed = Vec::new(); for option_value in options.iter() { if let Some(option) = option_value.to_map() { parsed.push(DatabaseOption { id: option.get("id").and_then(|value| value_to_string(&value)), value: option.get("value").and_then(|value| value_to_string(&value)), color: option.get("color").and_then(|value| value_to_string(&value)), }); } } parsed } fn format_option_tag(option: &DatabaseOption) -> String { let id = option.id.as_deref().unwrap_or_default(); let value = option.value.as_deref().unwrap_or_default(); let color = option.color.as_deref().unwrap_or_default(); format!("{value}") } fn format_cell_value(value: &Value, column: &DatabaseColumn) -> String { match column.col_type.as_str() { "select" => { let id = match value { Value::Any(any) => any_as_string(any).map(str::to_string), Value::Text(text) => Some(text.to_string()), _ => None, }; if let Some(id) = id { for option in column.options.iter() { if option.id.as_deref() == Some(id.as_str()) { return format_option_tag(option); } } } String::new() } "multi-select" => { let ids: Vec = match value { Value::Any(Any::Array(ids)) => ids.iter().filter_map(any_as_string).map(str::to_string).collect(), Value::Array(array) => array.iter().filter_map(|id_val| value_to_string(&id_val)).collect(), _ => Vec::new(), }; if ids.is_empty() { return String::new(); } let mut selected = Vec::new(); for id in ids.iter() { for option in column.options.iter() { if option.id.as_deref() == Some(id.as_str()) { selected.push(format_option_tag(option)); } } } selected.join("") } _ => value_to_string(value).unwrap_or_default(), } }