common/parser_tools/text_options.rs
1//! Opt-ins for the two *flowing* export formats.
2//!
3//! Plain text and Markdown have no page geometry and no typography, so unlike
4//! [`DocxExportOptions`](super::DocxExportOptions) and its siblings these carry nothing but a
5//! handful of presentation choices — the things a **file being written out** wants and a
6//! string being computed against does not.
7//!
8//! Both default to *off*, and that default is load-bearing rather than merely conservative:
9//! `to_plain_text()` is pinned character-for-character to the document's addressable text, the
10//! text `find_all`/`replace_text` compute offsets against. Anything that inserts or indents
11//! shifts every offset after it and silently desynchronises search from the document. So the
12//! plain view stays plain, and only a caller writing a file asks for the rest.
13
14use serde::{Deserialize, Serialize};
15
16/// The CSS3 property and its CSS2 predecessor, both, because reading systems and print
17/// engines are still split between them. Shared with the HTML writer so the two formats
18/// cannot drift apart on what a page break looks like.
19pub const HTML_PAGE_BREAK_STYLE: &str = "break-before: page; page-break-before: always;";
20
21/// A raw HTML block standing in for a page break in Markdown.
22///
23/// Markdown has no page-break construct — not in CommonMark, not in any widely-implemented
24/// extension. Of the three conventions in the wild (a bare `U+000C`, an HTML-comment sentinel
25/// a toolchain greps for, and this) only this one *does* anything without a bespoke
26/// processor: it survives Pandoc's reader as raw HTML, reaches HTML output intact, and is
27/// honoured by browser and EPUB print engines. It renders as nothing on screen.
28pub fn markdown_page_break() -> String {
29 format!("<div style=\"{HTML_PAGE_BREAK_STYLE}\"></div>")
30}
31
32/// U+000C FORM FEED — what a page break has meant in a text file since line printers, and
33/// still what `pr`, `less` and most terminal pagers act on.
34pub const FORM_FEED: char = '\u{000C}';
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
37pub struct PlainTextExportOptions {
38 /// Indent blockquoted blocks four spaces per level, so quoted matter still reads as
39 /// set-off in a format with no markup to say so.
40 pub quote_indent: bool,
41 /// Emit [`FORM_FEED`] before a block that asks to start a new page.
42 pub page_breaks: bool,
43 /// Drop the `U+FFFC` that stands for an inline image.
44 ///
45 /// **Presentation only.** A written-out `.txt` has no way to show a picture,
46 /// and the raw sentinel renders as an unmarked box — but it is also one
47 /// character of the document, so removing it shifts every offset after it.
48 /// Anything that addresses the text by position (a cursor, a search hit, a
49 /// comment's anchor) must therefore leave it in, which is exactly the
50 /// difference between [`Self::addressable`] and [`Self::presentation`].
51 pub strip_images: bool,
52 /// Lift footnotes out of the flow and list them, numbered, at the end.
53 ///
54 /// **Presentation only**, and for exactly the reason `strip_images` is. A
55 /// note's body is real content in the document, sitting in the rope where
56 /// its definition was written, and the addressable view has to agree with
57 /// the document character for character. Moving those blocks to the end —
58 /// or worse, printing them twice — puts every offset after them wrong,
59 /// which is every search hit and every comment anchor below the note.
60 ///
61 /// A written-out `.txt` has no page to put a footnote at the foot of, so
62 /// the endnote list is what a manuscript printed without markup has always
63 /// done.
64 pub endnote_footnotes: bool,
65}
66
67impl PlainTextExportOptions {
68 /// Nothing added — the addressable view, pinned to the document's own search
69 /// text. Offsets into it are offsets into the document, character for
70 /// character.
71 pub const fn addressable() -> Self {
72 Self {
73 quote_indent: false,
74 page_breaks: false,
75 strip_images: false,
76 endnote_footnotes: false,
77 }
78 }
79
80 /// Everything a written-out `.txt` wants.
81 pub const fn presentation() -> Self {
82 Self {
83 quote_indent: true,
84 page_breaks: true,
85 strip_images: true,
86 endnote_footnotes: true,
87 }
88 }
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
92pub struct MarkdownExportOptions {
93 /// Emit [`markdown_page_break`] before a block that asks to start a new page. Off by
94 /// default: raw HTML is not Markdown, and an export nobody is going to paginate is
95 /// better off clean.
96 pub page_breaks: bool,
97 /// Drop inline images instead of emitting ``.
98 ///
99 /// A Markdown export references its images by path and cannot carry their
100 /// bytes, so a caller that will not place those files beside the output is
101 /// choosing between a dangling reference and no image at all. Off by
102 /// default, because the reference is what a caller who *does* write the
103 /// files needs — see [`crate::parser_tools::HtmlImageMode`] for the same
104 /// choice in the one text format that can also inline the bytes.
105 #[serde(default)]
106 pub omit_images: bool,
107}