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}