Skip to main content

setzer_import/
lib.rs

1//! Manuscript importers. Every importer returns a `Book` plus warnings and
2//! never fails on odd input.
3//! Only input that is not a manuscript at all (empty, not a .docx) is an
4//! error.
5
6mod assets;
7pub mod docx;
8mod file;
9mod heading;
10mod lang;
11pub mod markdown;
12pub mod scrub;
13mod vocab;
14
15pub use assets::{MAX_IMAGE_BYTES, unique_id};
16pub use file::{ReadError, read_image, read_manuscript};
17
18use serde::Serialize;
19
20/// A non-fatal problem found while importing, shown to the author.
21#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
22pub struct ImportWarning {
23    pub kind: WarningKind,
24    /// 1-based line in the source (Markdown) or paragraph number (docx);
25    /// 0 when the warning is about the file as a whole.
26    pub location: usize,
27    /// Index into `Book::sections` of the section the warning is about.
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub section: Option<usize>,
30    pub message: String,
31    /// The thing the warning is about, when it has a name, so the UI can
32    /// say which image or footnote it means.
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub detail: Option<WarningDetail>,
35}
36
37/// What a warning names. Tagged by `kind` with kebab-case values, fields in
38/// snake_case, like the façade's other enums.
39#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
40#[serde(tag = "kind", rename_all = "kebab-case")]
41pub enum WarningDetail {
42    /// An image that is missing, cannot be read or is in a format books
43    /// cannot use: its path in the manuscript (Markdown) or in the file
44    /// (.docx), or none when the file does not say.
45    Image {
46        #[serde(skip_serializing_if = "Option::is_none")]
47        name: Option<String>,
48    },
49    /// A footnote label (Markdown).
50    Footnote { label: String },
51    /// A front-matter key (Markdown YAML).
52    FrontMatterKey { key: String },
53    /// The name of a `:::` fence that was not closed (Markdown).
54    Fence { name: String },
55}
56
57/// What a warning is about, so the UI can group and link warnings and
58/// tests can assert on them without matching prose.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
60#[serde(rename_all = "kebab-case")]
61pub enum WarningKind {
62    NoTitle,
63    NoChapters,
64    TextBeforeFirstChapter,
65    TitlePageReplaced,
66    TableOfContentsRemoved,
67    /// A table that stayed paragraphs, in reading order: a one-column table
68    /// (a boxed note), a table with a picture in a cell, a table with a
69    /// single cell. Every other table becomes a table block.
70    TableFlattened,
71    /// A table became a table block, or was dropped because it was empty,
72    /// and something in it could not be kept: a nested table, rotated text,
73    /// a list or several paragraphs in a cell, a cell past the header's
74    /// count (Markdown), a merge Word wrote inconsistently (.docx), a table
75    /// with cells but no rows or no text (dropped). Formatting a theme
76    /// decides anyway (shading, borders, fonts) gives no warning. One per
77    /// table.
78    TableSimplified,
79    TextBoxSkipped,
80    ImageUnsupported,
81    ImageMissing,
82    TrackedChangesAccepted,
83    LineBreaksJoined,
84    PageBreaksIgnored,
85    SceneBreaksFromBlankLines,
86    DamagedFile,
87    /// A footnote mark without a definition (Markdown).
88    FootnoteMissing,
89    /// A footnote definition no mark uses, or a second one for a label
90    /// (Markdown); its text is not in the book.
91    FootnoteUnused,
92    /// A `:::verse` fence without its closing `:::` (Markdown).
93    VerseNotClosed,
94    /// A front-matter key Kernsetzer does not read, or a value or line it
95    /// could not read (Markdown YAML); left out of the book.
96    FrontMatterKeyIgnored,
97    /// A numbered list that does not start at 1, kept as paragraphs with
98    /// their numbers (Markdown; the model numbers lists from 1).
99    ListFlattened,
100    /// A closing "END" / "Ende" under the manuscript's last paragraph,
101    /// left out of the book (.docx).
102    EndMarkRemoved,
103    /// A picture in front of or behind the text (.docx, `wp:wrapNone`): it
104    /// is set in the text where it was anchored, its position is not kept.
105    /// One per book.
106    ImagePositionDropped,
107}
108
109/// The attribution in an epigraph line that starts with a dash (`—`, `–`,
110/// `--`, `-`), or `None`. Shared by the Markdown and .docx importers.
111pub(crate) fn attribution_text(line: &str) -> Option<&str> {
112    let text = line
113        .strip_prefix(['—', '―', '–', '-'])?
114        .trim_start_matches(['—', '―', '–', '-'])
115        .trim();
116    (!text.is_empty()).then_some(text)
117}
118
119/// Whether a front-matter paragraph starts like a dedication ("For …",
120/// "Für …", "In memory of …", "Gewidmet …"). Shared by the Markdown and
121/// .docx importers; each adds its own limits on length.
122pub(crate) fn is_dedication(text: &str) -> bool {
123    let l = text.to_lowercase();
124    vocab::DEDICATION_STARTS.iter().any(|p| l.starts_with(p))
125}
126
127/// An image file found in a manuscript, referenced by `Block::Image::asset`.
128pub use setzer_model::asset::Asset;
129
130#[derive(Debug, Clone, PartialEq)]
131pub struct Imported {
132    pub book: setzer_model::Book,
133    pub assets: Vec<Asset>,
134    pub warnings: Vec<ImportWarning>,
135}
136
137#[derive(Debug, thiserror::Error)]
138pub enum ImportError {
139    #[error("the file is empty")]
140    Empty,
141    #[error("this is not a Word document (.docx): {0}")]
142    NotDocx(String),
143}