use crate::config::ImageMode;
use crate::markdown_layout::{
PositionedBlock, build_heading_map, classify_page_with_filters, compute_body_size,
compute_header_footer_set, detect_single_page_chrome, render_blocks, splice_soft_hyphens,
};
use crate::types::{OutlineTarget, ParsedPage};
pub fn format_markdown(
pages: &[ParsedPage],
outline: &[OutlineTarget],
image_mode: ImageMode,
) -> String {
format_markdown_pages(pages, outline, image_mode, false).join("\n\n-----\n\n")
}
pub fn format_markdown_pages(
pages: &[ParsedPage],
outline: &[OutlineTarget],
image_mode: ImageMode,
keep_headers_footers: bool,
) -> Vec<String> {
let classified = classify_document(pages, outline, image_mode, keep_headers_footers);
render_classified(pages, &classified)
}
pub fn classify_document(
pages: &[ParsedPage],
outline: &[OutlineTarget],
image_mode: ImageMode,
keep_headers_footers: bool,
) -> Vec<Option<Vec<PositionedBlock>>> {
if pages.is_empty() {
return Vec::new();
}
let body_size = compute_body_size(pages);
let heading_map = build_heading_map(pages, body_size);
let header_footer = if keep_headers_footers {
Default::default()
} else {
compute_header_footer_set(pages)
};
pages
.iter()
.map(|page| {
if page.projected_lines.is_empty() {
return None;
}
let target_index = (page.page_number as i32).saturating_sub(1);
let page_outline: Vec<OutlineTarget> = outline
.iter()
.filter(|e| e.page_index == target_index)
.cloned()
.collect();
let chrome_indices = if keep_headers_footers {
Default::default()
} else {
detect_single_page_chrome(page, body_size)
};
let mut blocks = classify_page_with_filters(
page,
&heading_map,
&header_footer,
&page_outline,
image_mode,
&chrome_indices,
);
let has_content = blocks.iter().any(|b| {
!matches!(
b.block,
crate::markdown_layout::Block::HorizontalRule
| crate::markdown_layout::Block::Figure { .. }
)
});
if !has_content {
blocks
.retain(|b| !matches!(b.block, crate::markdown_layout::Block::HorizontalRule));
}
dedupe_rules(&mut blocks);
Some(splice_soft_hyphens(blocks))
})
.collect()
}
pub fn render_classified(
pages: &[ParsedPage],
classified: &[Option<Vec<PositionedBlock>>],
) -> Vec<String> {
pages
.iter()
.zip(classified)
.map(|(page, blocks)| match blocks {
Some(blocks) => render_blocks(blocks),
None => {
let mut out = String::from("```text\n");
out.push_str(&page.text);
if !page.text.ends_with('\n') {
out.push('\n');
}
out.push_str("```");
out
}
})
.collect()
}
fn dedupe_rules(blocks: &mut Vec<crate::markdown_layout::PositionedBlock>) {
use crate::markdown_layout::Block::HorizontalRule;
while matches!(blocks.first().map(|b| &b.block), Some(HorizontalRule)) {
blocks.remove(0);
}
while matches!(blocks.last().map(|b| &b.block), Some(HorizontalRule)) {
blocks.pop();
}
blocks.dedup_by(|a, b| matches!((&a.block, &b.block), (HorizontalRule, HorizontalRule)));
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{Anchor, ProjectedLine, Rect, TextItem};
fn line(text: &str, x: f32, y: f32, h: f32, size: f32) -> ProjectedLine {
ProjectedLine {
text: text.into(),
rtl: crate::bidi::is_rtl_text(text),
bbox: Rect {
x,
y,
width: text.chars().count() as f32 * (size * 0.5),
height: h,
},
anchor: Anchor::Left,
indent_x: x,
dominant_font_size: size,
font_size_is_estimated: false,
heading_font_size: None,
dominant_font_name: Some("Arial".into()),
all_bold: false,
all_italic: false,
all_mono: false,
all_strike: false,
spans: vec![TextItem::default()],
region_path: Vec::new(),
mcid: None,
in_figure: false,
}
}
fn page_with(n: usize, lines: Vec<ProjectedLine>) -> ParsedPage {
ParsedPage {
page_number: n,
page_width: 612.0,
page_height: 792.0,
content_bounds: None,
text: "fallback".into(),
markdown: String::new(),
text_items: vec![],
projected_lines: lines,
regions: crate::types::Region::default(),
graphics: vec![],
vector_graphics: None,
figures: vec![],
struct_nodes: vec![],
image_refs: vec![],
complexity: None,
annotations: None,
form_fields: None,
structure_tree: None,
blocks: None,
}
}
#[test]
fn test_empty() {
assert_eq!(format_markdown(&[], &[], ImageMode::Placeholder), "");
}
#[test]
fn dedupe_rules_drops_edges_and_collapses_runs() {
use crate::markdown_layout::Block::{self, HorizontalRule, Paragraph};
let p = |t: &str| Paragraph {
text: t.into(),
bold: false,
italic: false,
};
let mut blocks: Vec<crate::markdown_layout::PositionedBlock> = [
HorizontalRule,
p("a"),
HorizontalRule,
HorizontalRule,
p("b"),
HorizontalRule,
]
.into_iter()
.map(crate::markdown_layout::PositionedBlock::unlocated)
.collect();
dedupe_rules(&mut blocks);
let kinds: Vec<bool> = blocks
.iter()
.map(|b| matches!(b.block, Block::HorizontalRule))
.collect();
assert_eq!(kinds, vec![false, true, false]);
}
#[test]
fn test_fallback_when_no_projected_lines() {
let p = ParsedPage {
page_number: 1,
page_width: 0.0,
page_height: 0.0,
content_bounds: None,
text: "hello".into(),
markdown: String::new(),
text_items: vec![],
projected_lines: vec![],
regions: crate::types::Region::default(),
graphics: vec![],
vector_graphics: None,
figures: vec![],
struct_nodes: vec![],
image_refs: vec![],
complexity: None,
annotations: None,
form_fields: None,
structure_tree: None,
blocks: None,
};
let out = format_markdown(&[p], &[], ImageMode::Placeholder);
assert!(out.contains("```text"));
assert!(out.contains("hello"));
}
#[test]
fn test_heading_and_paragraph() {
let p = page_with(
1,
vec![
line("My Title For This Test Document", 50.0, 50.0, 18.0, 18.0),
line("First sentence of body prose here.", 50.0, 80.0, 10.0, 10.0),
line(
"Second sentence of body prose here.",
50.0,
92.0,
10.0,
10.0,
),
line(
"Third sentence of body prose here.",
50.0,
104.0,
10.0,
10.0,
),
],
);
let out = format_markdown(&[p], &[], ImageMode::Placeholder);
assert!(out.contains("# My Title For This Test Document"));
assert!(out.contains("First sentence of body prose here."));
}
#[test]
fn test_multi_page_separator() {
let a = page_with(1, vec![line("A page.", 50.0, 80.0, 10.0, 10.0)]);
let b = page_with(2, vec![line("B page.", 50.0, 80.0, 10.0, 10.0)]);
let out = format_markdown(&[a, b], &[], ImageMode::Placeholder);
assert!(out.contains("-----"));
assert!(out.find("A page.").unwrap() < out.find("B page.").unwrap());
}
#[test]
fn test_per_page_matches_joined_document() {
let a = page_with(1, vec![line("A page.", 50.0, 80.0, 10.0, 10.0)]);
let b = page_with(2, vec![line("B page.", 50.0, 80.0, 10.0, 10.0)]);
let pages = [a, b];
let per_page = format_markdown_pages(&pages, &[], ImageMode::Placeholder, false);
assert_eq!(per_page.len(), 2);
assert!(per_page[0].contains("A page."));
assert!(per_page[1].contains("B page."));
assert!(!per_page[0].contains("-----"));
assert_eq!(
format_markdown(&pages, &[], ImageMode::Placeholder),
per_page.join("\n\n-----\n\n")
);
}
}