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
|
import gleam/list
import gleam/string
import gleeunit
import gleeunit/should
import ocda/load_ssp
import ocda/model
import gleam/bit_array
import write/epub
import write/html
import write/text
pub fn main() {
gleeunit.main()
}
/// A document small enough to reason about, carrying the things the writer
/// has to get right: a heading, an object with a note inside it, an object
/// with no ocn, and a trailing section with only its own heading.
const minimal = "% SiSU Document Abstraction v2.0
% Source: example.sst
@meta {
title.full: An Example
creator.author: A Writer
date.published: 2026-09-25
}
@head {
[0] heading :A the_title
.part: frontmatter
| An Example
}
@toc {
[0] toc
.indent: 0 4
| ┥Chapter One┝┤2├
}
@body {
[1] heading :1 c1
.part: body
| Chapter One
[2] para
.part: body
.parent: 1
| A paragraph with a note【1 The note itself.】in it.
}
@glossary {
[0] heading :A glossary
.part: backmatter
| Glossary
}
"
fn render() -> String {
text.write(load_ssp.parse(minimal))
}
/// head, toc, body. Not glossary: it holds its own heading and nothing else,
/// and spine omits a trailing section of one object.
pub fn sections_test() {
let doc = load_ssp.parse(minimal)
text.text_sections(doc) |> should.equal(["head", "toc", "body"])
render() |> string.contains(contain: "Glossary") |> should.be_false
}
/// A heading is preceded by a blank line; an object with ocn 0 gets no mark.
pub fn heading_test() {
render() |> string.contains(contain: "\nAn Example\n\n") |> should.be_true
render() |> string.contains(contain: "「0」") |> should.be_false
}
/// The ocn follows the object, in its own brackets.
pub fn ocn_test() {
render() |> string.contains(contain: "「1」") |> should.be_true
render() |> string.contains(contain: "「2」") |> should.be_true
}
/// A note is carried inside the object that refers to it: the reference
/// becomes [n] in place and the note is appended after the object.
pub fn note_test() {
let out = render()
out |> string.contains(contain: "a note[1]in it.") |> should.be_true
out |> string.contains(contain: "1. The note itself.") |> should.be_true
}
/// A toc entry's link becomes its text and, its target being an object,
/// the ocn it points at; one level in use, so no indent.
pub fn toc_test() {
render()
|> string.contains(contain: "\nChapter One ≫2\n")
|> should.be_true
}
/// The footer names the document and then the program.
pub fn tail_test() {
let out = render()
out |> string.contains(contain: "Title: An Example") |> should.be_true
out |> string.contains(contain: "Author: A Writer") |> should.be_true
out |> string.contains(contain: "Published: 2026-09-25") |> should.be_true
// no copyright or licence in this document, so no line for either
out |> string.contains(contain: "Copyright:") |> should.be_false
out |> string.ends_with("spine\nhttps://sisudoc.org\n") |> should.be_true
}
/// The writer is given the model and nothing else, so an object it has no
/// case for writes nothing rather than failing.
pub fn unknown_kind_writes_nothing_test() {
let doc = load_ssp.parse(minimal)
let body = model.section(doc, "body")
body |> list.length |> should.equal(2)
}
// ---- the html writer -----------------------------------------------------
fn render_html() -> String {
html.write(load_ssp.parse(minimal))
}
/// A level A heading is h1. Spine writes <h0>, which is not an element.
pub fn heading_element_test() {
let out = render_html()
out |> string.contains(contain: "<h1 id=\"the_title\">") |> should.be_true
out |> string.contains(contain: "<h0") |> should.be_false
}
/// One charset declaration, and no placeholder metadata: a document with no
/// publisher gets no dc.publisher rather than the word "Publisher".
pub fn head_test() {
let out = render_html()
out |> string.contains(contain: "<meta charset=\"utf-8\" />") |> should.be_true
out |> string.contains(contain: "text/plain") |> should.be_false
out |> string.contains(contain: "dc.publisher") |> should.be_false
out |> string.contains(contain: "content=\"year\"") |> should.be_false
}
/// The object number is a span, not a label, and links to itself.
pub fn ocn_mark_test() {
render_html()
|> string.contains(contain: "<span class=\"ocn\"><a href=\"#2\" class=\"lnkocn\">2</a></span>")
|> should.be_true
}
/// A note reference points at the gathered note and carries the dpub role.
pub fn noteref_test() {
render_html()
|> string.contains(
contain: "<a id=\"noteref_1\" href=\"#note_1\" role=\"doc-noteref\">1</a>",
)
|> should.be_true
}
/// A section carries its dpub-aria role and no invented id.
pub fn section_test() {
let out = render_html()
out |> string.contains(contain: "<section class=\"toc\" role=\"doc-toc\">") |> should.be_true
out |> string.contains(contain: "id=\"section_toc\"") |> should.be_false
}
/// It is self-contained: the stylesheet is in the file, not linked out of it.
pub fn self_contained_test() {
let out = render_html()
out |> string.contains(contain: "<style>") |> should.be_true
out |> string.contains(contain: "html_scroll.css") |> should.be_false
}
/// An ampersand in the text becomes &, and a quotation mark in an
/// attribute becomes ". Erlang's re:replace reads "&" in a replacement
/// *string* as the whole match, which turned "&" into the match itself;
/// every replacement here goes through the function form instead.
pub fn escaping_test() {
let doc =
load_ssp.parse(
"% SiSU Document Abstraction v2.0
@body {
[1] para
.part: body
| Smith & Jones said \"hello\" <here>
}
",
)
let out = html.write(doc)
out |> string.contains(contain: "Smith & Jones") |> should.be_true
out |> string.contains(contain: "<here>") |> should.be_true
out |> string.contains(contain: "\"quot;") |> should.be_false
}
// ---- the epub writer -----------------------------------------------------
/// The container is first and stored: a reading system identifies an epub
/// from the first bytes of the file, so "mimetype" has to be there and
/// uncompressed.
pub fn epub_mimetype_first_test() {
let bytes = epub.write(load_ssp.parse(minimal), "test/nothing.ssp")
// local file header, then the name at offset 30
let assert Ok(head) = bit_array.slice(bytes, 0, 4)
head |> should.equal(<<0x50, 0x4b, 0x03, 0x04>>)
let assert Ok(name) = bit_array.slice(bytes, 30, 8)
name |> should.equal(<<"mimetype":utf8>>)
let assert Ok(kind) = bit_array.slice(bytes, 38, 20)
kind |> should.equal(<<"application/epub+zip":utf8>>)
}
/// Every entry has the same date, so that the same document gives the same
/// epub twice.
pub fn epub_is_reproducible_test() {
let doc = load_ssp.parse(minimal)
epub.write(doc, "test/nothing.ssp")
|> should.equal(epub.write(doc, "test/nothing.ssp"))
}
/// The segments are grouped by name, not by run: a section's objects are not
/// always contiguous, and two zip entries of one name is an epubcheck error.
pub fn epub_segments_are_unique_test() {
let segs = epub.segments(load_ssp.parse(minimal))
let names = list.map(segs, fn(s) { s.file })
list.length(names) |> should.equal(list.length(list.unique(names)))
}
/// A `.ssp` outside a pod cannot give images, and the writer asks before it
/// emits one: an `<img>` whose file is not in the manifest is an error.
pub fn epub_images_test() {
epub.images_named(load_ssp.parse(minimal)) |> should.equal([])
}
|