use serde_json::{json, Map, Value};
use crate::escape::{
decode_entities, is_xml_whitespace_only, push_escaped_attr, push_escaped_text,
};
use crate::types::{ElementData, ElementRef, TextSegments};
use crate::Markdown;
fn text_with_trivia<'a>(
raw: &'a str,
el: &'a ElementData,
trivia: &'a [core::ops::Range<usize>],
) -> TextSegments<'a> {
TextSegments::new_with_trivia(raw, el, trivia)
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct SerializeOpts {
pub indent: Option<String>,
pub self_close_empty: bool,
}
impl SerializeOpts {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn pretty() -> Self {
Self {
indent: Some(" ".to_string()),
self_close_empty: true,
}
}
#[must_use]
pub fn with_indent(mut self, indent: impl Into<String>) -> Self {
self.indent = Some(indent.into());
self
}
#[must_use]
pub fn compact(mut self) -> Self {
self.indent = None;
self
}
#[must_use]
pub fn self_close_empty(mut self) -> Self {
self.self_close_empty = true;
self
}
#[must_use]
pub fn expand_empty(mut self) -> Self {
self.self_close_empty = false;
self
}
}
pub(crate) fn to_xml(doc: &Markdown, opts: &SerializeOpts) -> String {
let mut out = String::new();
for (i, root) in doc.roots_internal().iter().enumerate() {
if i > 0 && opts.indent.is_some() {
out.push('\n');
}
emit_element(root, doc.raw(), doc.trivia(), opts, 0, &mut out);
}
out
}
pub(crate) fn to_json(doc: &Markdown) -> Value {
Value::Array(
doc.roots_internal()
.iter()
.map(|root| element_json(root, doc.raw(), doc.trivia()))
.collect(),
)
}
fn emit_element(
el: &ElementData,
raw: &str,
trivia: &[core::ops::Range<usize>],
opts: &SerializeOpts,
depth: usize,
out: &mut String,
) {
indent_for(opts, depth, out);
out.push('<');
out.push_str(&el.tag);
for (k, v) in &el.attrs {
out.push(' ');
out.push_str(k);
out.push_str("=\"");
push_escaped_attr(out, v);
out.push('"');
}
let has_text = text_with_trivia(raw, el, trivia).any(|s| !is_xml_whitespace_only(s));
let is_empty = el.children.is_empty() && !has_text;
if is_empty && (el.self_closing || opts.self_close_empty) {
out.push_str("/>");
return;
}
out.push('>');
if el.children.is_empty() {
for segment in text_with_trivia(raw, el, trivia) {
push_escaped_text(out, segment);
}
} else if opts.indent.is_some() {
emit_pretty_children(el, raw, trivia, opts, depth, out);
} else {
emit_tight_children(el, raw, trivia, opts, depth, out);
}
out.push_str("</");
out.push_str(&el.tag);
out.push('>');
}
fn emit_tight_children(
el: &ElementData,
raw: &str,
trivia: &[core::ops::Range<usize>],
opts: &SerializeOpts,
depth: usize,
out: &mut String,
) {
let body_start = el.content_range.start;
let body_end = el.content_range.end;
let mut cursor = body_start;
let mut trivia_idx = trivia.partition_point(|r| r.end <= body_start);
for child in &el.children {
let child_start = child.span.start.offset_usize();
let segment_end = child_start.min(body_end);
push_escaped_text_skipping_trivia(raw, cursor, segment_end, trivia, &mut trivia_idx, out);
emit_element(child, raw, trivia, opts, depth + 1, out);
cursor = child.span.end.offset_usize();
}
if cursor < body_end {
push_escaped_text_skipping_trivia(raw, cursor, body_end, trivia, &mut trivia_idx, out);
}
}
fn push_escaped_text_skipping_trivia(
raw: &str,
from: usize,
to: usize,
trivia: &[core::ops::Range<usize>],
trivia_idx: &mut usize,
out: &mut String,
) {
let mut cursor = from;
while cursor < to {
while *trivia_idx < trivia.len() && trivia[*trivia_idx].end <= cursor {
*trivia_idx += 1;
}
let tr = trivia.get(*trivia_idx);
let plain_end = match tr {
Some(r) if r.start < to => r.start.max(cursor),
_ => to,
};
if cursor < plain_end {
escape_into(&raw[cursor..plain_end], out);
}
match tr {
Some(r) if r.start < to => {
cursor = r.end.min(to).max(cursor);
if r.end <= to {
*trivia_idx += 1;
}
}
_ => break,
}
}
}
#[inline]
fn escape_into(slice: &str, out: &mut String) {
push_escaped_text(out, slice);
}
fn emit_pretty_children(
el: &ElementData,
raw: &str,
trivia: &[core::ops::Range<usize>],
opts: &SerializeOpts,
depth: usize,
out: &mut String,
) {
let has_inline_text = text_with_trivia(raw, el, trivia).any(|s| !is_xml_whitespace_only(s));
if has_inline_text {
let tight = SerializeOpts {
indent: None,
self_close_empty: opts.self_close_empty,
};
emit_tight_children(el, raw, trivia, &tight, depth, out);
return;
}
for child in &el.children {
out.push('\n');
emit_element(child, raw, trivia, opts, depth + 1, out);
}
out.push('\n');
indent_for(opts, depth, out);
}
fn indent_for(opts: &SerializeOpts, depth: usize, out: &mut String) {
if let Some(indent) = &opts.indent {
for _ in 0..depth {
out.push_str(indent);
}
}
}
fn element_json(el: &ElementData, raw: &str, trivia: &[core::ops::Range<usize>]) -> Value {
let attrs: Map<String, Value> = el
.attrs
.iter()
.map(|(k, v)| (k.clone(), Value::String(v.clone())))
.collect();
let children: Vec<Value> = el
.children
.iter()
.map(|c| element_json(c, raw, trivia))
.collect();
let mut text = String::new();
for segment in text_with_trivia(raw, el, trivia) {
text.push_str(&decode_entities(segment));
}
json!({
"tag": el.tag.clone(),
"attrs": Value::Object(attrs),
"text": text,
"children": Value::Array(children),
"selfClosing": el.self_closing,
"location": {
"start": { "line": el.span.start.line, "offset": el.span.start.offset },
"end": { "line": el.span.end.line, "offset": el.span.end.offset },
},
})
}
impl std::fmt::Display for Markdown {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.raw())
}
}
impl std::fmt::Display for ElementRef<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let span = self.location();
f.write_str(&self.raw[span.start.offset_usize()..span.end.offset_usize()])
}
}