use rayon::prelude::*;
use serde_json::Value;
use crate::config::PARALLEL;
use crate::ui::UI_STRINGS;
use super::column_metadata;
use super::consts::TABLE_GAP;
use super::format;
use super::walk;
pub struct KvSection {
pub title: Option<String>,
pub rows: Vec<(String, String)>,
pub sub_title: bool,
}
pub struct ContentsSection {
pub title: String,
pub columns: Vec<String>,
pub column_keys: Vec<String>,
pub entries: Vec<Value>,
pub sub_title: bool,
}
pub struct SingleColumnListSection {
pub title: String,
pub values: Vec<String>,
}
pub enum Section {
KeyValue(KvSection),
Contents(ContentsSection),
SingleColumnList(SingleColumnListSection),
}
impl Section {
#[must_use]
pub fn title_str(&self) -> Option<&str> {
match self {
Section::KeyValue(kv) => kv.title.as_deref(),
Section::Contents(c) => Some(c.title.as_str()),
Section::SingleColumnList(list) => Some(list.title.as_str()),
}
}
#[must_use]
pub fn line_metrics(&self) -> (bool, u16, usize) {
match self {
Section::KeyValue(kv) => (kv.title.is_some(), 1, kv.rows.len()),
Section::Contents(c) => (true, 1, c.entries.len()),
Section::SingleColumnList(list) => (true, 0, list.values.len()),
}
}
#[must_use]
pub fn sub_title_style(&self) -> bool {
match self {
Section::KeyValue(kv) => kv.sub_title,
Section::Contents(c) => c.sub_title,
Section::SingleColumnList(_) => false,
}
}
}
fn parse_one_blob(blob: &str, max_array_inline: usize) -> Vec<Section> {
let value: Value = match serde_json::from_str(blob.trim()) {
Ok(v) => v,
Err(_) => return vec![],
};
let Some(map) = value.as_object() else {
return vec![];
};
let map = map
.get("_metadata")
.and_then(Value::as_object)
.unwrap_or(map);
if column_metadata::is_compact_column_metadata(map) {
column_metadata::sections_from_column_metadata_root(map, max_array_inline)
} else if column_metadata::is_legacy_parallel_column_metadata(map) {
column_metadata::sections_from_legacy_column_metadata_root(map)
} else {
walk::root_parts_sections(map, max_array_inline)
}
}
#[must_use]
pub fn parse_json_sections(json: &str) -> Vec<Section> {
parse_json_sections_with(json, format::DEFAULT_MAX_ARRAY_INLINE)
}
#[must_use]
pub fn parse_json_sections_with(json: &str, max_array_inline: usize) -> Vec<Section> {
let blobs: Vec<&str> = json
.split("\n\n")
.filter(|s| !s.trim().is_empty())
.collect();
let mut sections: Vec<Section> = if blobs.len() >= PARALLEL.json_sections_blobs {
blobs
.par_iter()
.flat_map(|blob| parse_one_blob(blob, max_array_inline))
.collect()
} else {
blobs
.iter()
.flat_map(|blob| parse_one_blob(blob, max_array_inline))
.collect()
};
if let Some(Section::KeyValue(kv)) = sections.first_mut() {
kv.title = Some(UI_STRINGS.tables.first_title.to_string());
}
sections
}
#[must_use]
pub fn visual_lines_from_sections(sections: &[Section], max_array_inline: usize) -> Vec<String> {
let mut lines: Vec<String> = Vec::new();
for (i, section) in sections.iter().enumerate() {
if i > 0 {
for _ in 0..TABLE_GAP {
lines.push(String::new());
}
}
match section {
Section::KeyValue(kv) => {
if let Some(t) = &kv.title {
lines.push(t.clone());
}
lines.push(format!(
"{} {}",
UI_STRINGS.tables.header_key, UI_STRINGS.tables.header_value
));
for (k, v) in &kv.rows {
lines.push(format!("{k} {v}"));
}
}
Section::Contents(c) => {
lines.push(c.title.clone());
lines.push(c.columns.join(" "));
for entry in &c.entries {
if let Some(obj) = entry.as_object() {
let row: Vec<String> = c
.column_keys
.iter()
.map(|k| {
obj.get(k).map_or_else(
|| "—".to_string(),
|v| format::format_value(v, k, max_array_inline),
)
})
.collect();
lines.push(row.join(" "));
}
}
}
Section::SingleColumnList(list) => {
lines.push(list.title.clone());
for s in &list.values {
lines.push(s.clone());
}
}
}
}
lines
}
#[must_use]
pub fn line_byte_starts(s: &str) -> Vec<usize> {
let mut v = vec![0];
for (i, b) in s.bytes().enumerate() {
if b == b'\n' {
v.push(i + 1);
}
}
v
}
#[must_use]
pub fn searchable_text_from_json(json: &str) -> String {
searchable_text_from_json_with(json, format::DEFAULT_MAX_ARRAY_INLINE)
}
#[must_use]
pub fn searchable_text_from_json_with(json: &str, max_array_inline: usize) -> String {
let sections = parse_json_sections_with(json, max_array_inline);
if sections.is_empty() {
return json.to_string();
}
let lines = visual_lines_from_sections(§ions, max_array_inline);
lines.join("\n")
}
#[must_use]
pub fn content_height(json: &str) -> u16 {
let sections = parse_json_sections(json);
if sections.is_empty() {
return 0;
}
let mut lines: u16 = 0;
for (i, section) in sections.iter().enumerate() {
if i > 0 {
lines += TABLE_GAP;
}
let (has_title, header_lines, num_rows) = section.line_metrics();
lines += u16::from(has_title);
lines += header_lines;
lines += num_rows as u16;
}
lines
}