1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
|
//// The object-centric document abstraction, as Gleam types.
////
//// A deliberate mirror of the D library's `ocda.model`, field for field.
//// The two are meant to agree, and the cheapest way to make that testable
//// is for them to hold the same things under the same names: the dump of a
//// loaded document is then identical by construction rather than by
//// negotiation.
////
//// Written against specs/sisudoc-abstraction-format: `ocda-model.md` for
//// the model, the two format specifications for what each serialisation
//// says about it.
import gleam/dict.{type Dict}
import gleam/list
import gleam/string
/// The format major version this library knows.
///
/// The `.ssp` and the `.ocda.db` carry one version between them, declared
/// in band. The minor part rises when a property is added and a reader of
/// the newer format still reads the older, so only the major is checked.
pub const format_major_read = "2"
/// The nine sections, in the order the format gives them, which is not the
/// order a sorted key list or a `group by` would give.
pub const section_names = [
"head", "toc", "body", "endnotes", "glossary", "bibliography", "bookindex",
"blurb", "tail",
]
/// The four blocks that are document preamble rather than sections of
/// objects.
pub const preamble_blocks = ["source", "meta", "make", "doc_has"]
/// A repeatable `.image` record: the file as the abstraction found it.
pub type Image {
Image(
/// filename as the object refers to it
name: String,
/// hex, uppercase
sha256: String,
bytes: Int,
/// the file's own pixel dimensions, not any display width
width: Int,
height: Int,
/// absent or unreadable, so no digest and no size
missing: Bool,
/// any further key:value fields, kept rather than dropped
fields: Dict(String, String),
)
}
pub fn new_image() -> Image {
Image(
name: "",
sha256: "",
bytes: 0,
width: 0,
height: 0,
missing: False,
fields: dict.new(),
)
}
/// One object of the abstraction.
///
/// `text` is one string with the lines joined by newline, as in the D
/// library and for the same reason: an object whose only text line is
/// blank is then indistinguishable from an object with no text, in both
/// libraries, in the same place. A difference that exists in one language
/// and not the other would be worse than a loss both share.
pub type Object {
Object(
ocn: Int,
/// one of the sixteen kinds
is_a: String,
/// the section this object was found in
section: String,
/// headings: A B C D 1 2 3 4
heading_level: String,
identifier: String,
part: String,
/// how the text behaves: para, block, comment
is_of_type: String,
/// the object's own section, when it differs from the block it sits in
own_section: String,
segment_html_is: String,
segment_epub: String,
segment_epub_is: String,
table_aligns: String,
stow_link: List(String),
anchor_tags: List(String),
parent_ocn: Int,
last_descendant_ocn: Int,
ocn_range_first: Int,
ocn_range_last: Int,
children: List(Int),
ancestors: List(Int),
dummy_heading: Bool,
ocn_off: Bool,
indent_base: Int,
indent_hang: Int,
bullet: Bool,
lang: String,
has_links: Bool,
has_notes_reg: Bool,
has_notes_star: Bool,
has_images: Bool,
segment: String,
segment_prev: String,
segment_next: String,
anchor: String,
heading_lev_anchor: String,
table_cols: Int,
table_widths: String,
table_header: Bool,
code_syntax: String,
code_linenumbers: Bool,
heading_ancestors_text: String,
sha256: String,
images: List(Image),
text: String,
)
}
pub fn new_object() -> Object {
Object(
ocn: 0,
is_a: "",
section: "",
heading_level: "",
identifier: "",
part: "",
is_of_type: "",
own_section: "",
segment_html_is: "",
segment_epub: "",
segment_epub_is: "",
table_aligns: "",
stow_link: [],
anchor_tags: [],
parent_ocn: 0,
last_descendant_ocn: 0,
ocn_range_first: 0,
ocn_range_last: 0,
children: [],
ancestors: [],
dummy_heading: False,
ocn_off: False,
indent_base: 0,
indent_hang: 0,
bullet: False,
lang: "",
has_links: False,
has_notes_reg: False,
has_notes_star: False,
has_images: False,
segment: "",
segment_prev: "",
segment_next: "",
anchor: "",
heading_lev_anchor: "",
table_cols: 0,
table_widths: "",
table_header: False,
code_syntax: "",
code_linenumbers: False,
heading_ancestors_text: "",
sha256: "",
images: [],
text: "",
)
}
/// Whether this object is rendered at all.
///
/// `is_of_type` "comment" is markup commentary: carried by the abstraction
/// so that nothing is lost, and not put into output. An output that
/// renders it leaks the author's notes into the document, which is what
/// spine itself does not do.
pub fn renders(object: Object) -> Bool {
object.is_of_type != "comment"
}
/// The section this object belongs to: the block it sits in, unless it
/// says otherwise.
pub fn section_of(object: Object) -> String {
case object.own_section {
"" -> object.section
own -> own
}
}
/// What the loader met and did not understand.
///
/// A loader that silently drops what it does not know cannot say whether
/// it understood the document, and so cannot be used to check a
/// specification against an implementation: silence and understanding look
/// alike.
pub type Unknown {
Unknown(
/// `.name` -> times seen
property: Dict(String, Int),
/// `@name` -> times seen
block: Dict(String, Int),
/// first character -> times seen, for lines of no known shape
line: Dict(String, Int),
)
}
pub fn new_unknown() -> Unknown {
Unknown(property: dict.new(), block: dict.new(), line: dict.new())
}
pub fn any_unknown(unknown: Unknown) -> Bool {
dict.size(unknown.property) > 0
|| dict.size(unknown.block) > 0
|| dict.size(unknown.line) > 0
}
pub type Document {
Document(
/// as declared on line 1 of a `.ssp` or in `schema.version`, "" when
/// absent
format_version: String,
source_filename: String,
/// language, languages, digest
source: Dict(String, String),
unknown: Unknown,
metadata: Dict(String, String),
make: Dict(String, String),
doc_has: Dict(String, String),
/// section name -> objects, in document order
sections: Dict(String, List(Object)),
)
}
pub fn new_document() -> Document {
Document(
format_version: "",
source_filename: "",
source: dict.new(),
unknown: new_unknown(),
metadata: dict.new(),
make: dict.new(),
doc_has: dict.new(),
sections: dict.new(),
)
}
/// The major part of the declared version, "" when none was declared.
pub fn version_major(document: Document) -> String {
case string.split_once(document.format_version, ".") {
Ok(#(major, _)) -> major
Error(_) -> document.format_version
}
}
pub fn meta(document: Document, key: String) -> String {
case dict.get(document.metadata, key) {
Ok(value) -> value
Error(_) -> ""
}
}
/// The objects of a named section, empty when the document has none.
pub fn section(document: Document, name: String) -> List(Object) {
case dict.get(document.sections, name) {
Ok(objects) -> objects
Error(_) -> []
}
}
/// Is this one of the nine sections of the format?
pub fn is_section(name: String) -> Bool {
list.contains(section_names, name)
}
/// Is this one of the four preamble blocks?
pub fn is_preamble_block(name: String) -> Bool {
list.contains(preamble_blocks, name)
}
|