use pdfboss_core::Stream;
use serde_json::Value;
use crate::input::{use_color, Input};
use crate::q::value::{build_tree, TreeFlags};
pub fn cmd_json(
input_spec: &str,
flags: &TreeFlags,
layout: bool,
password: &str,
) -> Result<(), String> {
let input = Input::open_with_password(input_spec, password)?;
let opts = flags.element_opts()?;
let elements = input.collect_elements(opts);
let mut decode = |s: &Stream| input.decode_stream(s);
let mut tree = build_tree(
&elements,
flags.stream_data(),
flags.content_ops,
&mut decode,
);
if layout {
let value = layout_value(&input, &flags.pages)?;
if let Value::Object(map) = &mut tree {
map.insert("layout".to_string(), value);
}
}
let mut text = String::new();
write_json_pretty(&mut text, &tree, 0, use_color());
println!("{text}");
Ok(())
}
fn layout_value(input: &Input, pages: &Option<Vec<usize>>) -> Result<Value, String> {
let count = input.page_count();
let layouts = match pages {
Some(numbers) => {
let mut numbers: Vec<usize> = numbers
.iter()
.copied()
.filter(|&n| n >= 1 && n <= count)
.collect();
numbers.sort_unstable();
numbers.dedup();
let mut layouts = Vec::with_capacity(numbers.len());
for n in numbers {
let (spans, rulings) = input.page_spans_and_rulings(n - 1)?;
layouts.push(pdfboss_output::page_layout_with_rulings(&spans, &rulings));
}
layouts
}
None => {
let mut pages = Vec::with_capacity(count);
for index in 0..count {
pages.push(input.page_spans_and_rulings(index)?);
}
pdfboss_output::document_layout_with_rulings(&pages)
}
};
serde_json::to_value(&layouts).map_err(|e| e.to_string())
}
pub fn write_json_pretty(out: &mut String, v: &Value, indent: usize, color: bool) {
const KEY: &str = "\x1b[36m";
const STR: &str = "\x1b[32m";
const NUM: &str = "\x1b[33m";
const LIT: &str = "\x1b[35m";
const RESET: &str = "\x1b[0m";
let paint = |out: &mut String, code: &str, text: &str| {
if color {
out.push_str(code);
out.push_str(text);
out.push_str(RESET);
} else {
out.push_str(text);
}
};
match v {
Value::Null => paint(out, LIT, "null"),
Value::Bool(b) => paint(out, LIT, if *b { "true" } else { "false" }),
Value::Number(n) => paint(out, NUM, &n.to_string()),
Value::String(s) => {
let quoted = serde_json::to_string(s).expect("strings always serialize");
paint(out, STR, "ed);
}
Value::Array(items) => {
if items.is_empty() {
out.push_str("[]");
return;
}
out.push_str("[\n");
for (i, item) in items.iter().enumerate() {
push_indent(out, indent + 1);
write_json_pretty(out, item, indent + 1, color);
if i + 1 < items.len() {
out.push(',');
}
out.push('\n');
}
push_indent(out, indent);
out.push(']');
}
Value::Object(map) => {
if map.is_empty() {
out.push_str("{}");
return;
}
out.push_str("{\n");
for (i, (key, value)) in map.iter().enumerate() {
push_indent(out, indent + 1);
let quoted = serde_json::to_string(key).expect("strings always serialize");
paint(out, KEY, "ed);
out.push_str(": ");
write_json_pretty(out, value, indent + 1, color);
if i + 1 < map.len() {
out.push(',');
}
out.push('\n');
}
push_indent(out, indent);
out.push('}');
}
}
}
fn push_indent(out: &mut String, level: usize) {
for _ in 0..level {
out.push_str(" ");
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn plain(v: &Value) -> String {
let mut out = String::new();
write_json_pretty(&mut out, v, 0, false);
out
}
#[test]
fn scalars_print_bare() {
assert_eq!(plain(&json!(null)), "null");
assert_eq!(plain(&json!(true)), "true");
assert_eq!(plain(&json!(42)), "42");
assert_eq!(plain(&json!(1.5)), "1.5");
assert_eq!(plain(&json!("a\"b")), r#""a\"b""#);
}
#[test]
fn empty_containers_stay_inline() {
assert_eq!(plain(&json!([])), "[]");
assert_eq!(plain(&json!({})), "{}");
}
#[test]
fn pretty_prints_nested_containers_two_space_indented() {
let v = json!({"a": [1, 2], "b": {"c": "x"}});
assert_eq!(
plain(&v),
"{\n \"a\": [\n 1,\n 2\n ],\n \"b\": {\n \"c\": \"x\"\n }\n}"
);
}
#[test]
fn color_paints_keys_strings_numbers_and_literals() {
let v = json!({"k": ["s", 7, true, null]});
let mut out = String::new();
write_json_pretty(&mut out, &v, 0, true);
assert!(
out.contains("\x1b[36m\"k\"\x1b[0m"),
"key not cyan: {out:?}"
);
assert!(
out.contains("\x1b[32m\"s\"\x1b[0m"),
"string not green: {out:?}"
);
assert!(
out.contains("\x1b[33m7\x1b[0m"),
"number not yellow: {out:?}"
);
assert!(
out.contains("\x1b[35mtrue\x1b[0m"),
"bool not magenta: {out:?}"
);
assert!(
out.contains("\x1b[35mnull\x1b[0m"),
"null not magenta: {out:?}"
);
}
#[test]
fn uncolored_output_carries_no_escapes() {
let v = json!({"k": [1, "s"]});
assert!(!plain(&v).contains('\x1b'));
}
}