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
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
|
module write.html;
/+ ↓ the scroll html writer: one file, self-contained, valid html5.
.
Not a reproduction of spine's html. The text writer is spine's output
byte for byte because the format is plain and there was nothing to
improve; html is different, and reproducing it exactly would mean
carrying forward things that are wrong rather than merely unusual. So
this writer is held to a different standard, and a stricter one in the
places that matter:
.
- the D and the Gleam writer produce the *same bytes*, which is the
agreement that says the model is enough;
- the visible text equals spine's, normalised the same way on both
sides, so nothing is dropped, added or reordered;
- every internal link resolves to an id in the same file;
- it parses.
.
What is deliberately different from spine, each with the reason:
.
1. *headings are h1..h6*. Spine writes <h0> for a level A heading,
which is not an html element: the level is its markup depth and 0
is passed through. A document title is h1.
2. *one charset declaration*. Spine writes
Content-Type: text/plain for an html document and then a second
Content-Type saying text/html.
3. *no empty or placeholder metadata*. Spine writes
dc.publisher="Publisher" and dc.date.issued="year" literally where
it has no value, and generator="" when asked not to name itself. A
meta with nothing in it is left out.
4. *span, not label, for the object number*. <label> is for form
controls.
5. *section with a dpub-aria role*, rather than div with a class that
means something to one stylesheet. Spine's roles are right and are
kept; the element is the improvement.
6. *self-contained*: a small embedded stylesheet, so the file renders
on its own. Spine links ../../css/html_scroll.css, which resolves
only inside spine's own output tree.
7. *no site furniture*. The headband of sisudoc.org links is not in
the abstraction and is not the document.
+/
import std.algorithm : canFind, endsWith, startsWith;
import std.array : appender, array, join, replace;
import std.array : asplit = split;
import std.conv : to;
import std.format : format;
import std.regex : matchAll, matchFirst, replaceAll, replaceFirst, split;
import std.string : strip;
import std.uri : encodeComponent;
import ocda.model;
import write.rgx;
static Rgx rgx_h;
/+ ↓ the whole document as one html file +/
string writeHtmlScroll(ref OcdaDocument doc) {
auto out_ = appender!string;
useLinkResolver((string t) => t);
auto ids = AnchorIds();
reserveIdentifiers(doc, ids);
out_ ~= head(doc);
string open_section = "";
foreach (name; htmlSections(doc)) {
foreach (obj; *doc.sectionByName(name)) {
if (!obj.renders) { continue; }
if (name != open_section) {
if (open_section.length > 0) { out_ ~= "</section>\n"; }
out_ ~= sectionOpen(name);
open_section = name;
}
out_ ~= writeObj(obj, doc, ids);
}
}
if (open_section.length > 0) { out_ ~= "</section>\n"; }
out_ ~= tail(doc);
return out_.data;
}
/+ ↓ an id must be unique, so for each anchor tag the *first* occurrence
becomes the id and the rest become markers.
.
An anchor tag is a named target the markup placed on an object, and a
book index entry puts the same marker on every object the entry names:
"loc" 296 times in one document. An id repeated 296 times is not a
target, it is a defect, and a link to it arrives nowhere in particular.
The first occurrence is a definite place and is where a link should land.
.
Spine decides by kind instead - an endnote's tag becomes an id and
nothing else does - which is right about endnotes and silent about the
rest, so a link to any other tag has nothing to arrive at. Its own
output has such links.
.
The state is carried rather than computed up front, because "first"
is a property of the traversal.
+/
struct AnchorIds {
bool[string] taken;
bool claim(string tag) {
if (tag in taken) { return false; }
taken[tag] = true;
return true;
}
}
/+ ↓ every object's identifier is an id, so those names are taken before any
anchor can ask for one.
.
They collide in practice: a heading's level anchor may be named "1",
and "1" is also the identifier of the object whose ocn is 1. Claiming
the identifiers first means the collision costs an anchor rather than
making the same id twice, and an id that appears twice is not a target.
+/
void reserveIdentifiers(ref OcdaDocument doc, ref AnchorIds ids) {
foreach (name; htmlSections(doc)) {
auto sec = doc.sectionByName(name);
if (sec is null) { continue; }
foreach (obj; *sec) {
if (obj.identifier.length > 0) { ids.claim(obj.identifier); }
/+ ↓ and the ocn, which is the id of every numbered object +/
if (obj.ocn != 0) { ids.claim(obj.ocn.to!string); }
}
}
}
/+ ↓ the same sequence the text writer uses, and for the same reason: it is
the document's own order, and a trailing section holding only its own
heading is not a section +/
string[] htmlSections(ref OcdaDocument doc) {
string[] seq = ["head", "toc", "body"];
foreach (name; ["endnotes", "glossary", "bibliography", "bookindex", "blurb"]) {
auto sec = doc.sectionByName(name);
if (sec !is null && (*sec).length > 1) { seq ~= name; }
}
return seq;
}
/+ ↓ the dpub-aria role of a section, where it has one. A section with no
role of its own gets none rather than an invented one. +/
string sectionRole(string name) {
switch (name) {
case "toc": return "doc-toc";
case "endnotes": return "doc-endnotes";
case "bibliography": return "doc-bibliography";
case "glossary": return "doc-glossary";
case "bookindex": return "doc-index";
default: return "";
}
}
/+ ↓ a section, with no id of its own.
.
It needs none: the document already carries the targets its table of
contents links to, as anchor tags on the section headings - "toc",
"endnotes", "section_endnotes" - and this writer emits those. Inventing
a second id for the same place would put the same name in the document
twice.
.
Spine's scroll output *does* have a link with nowhere to go, its toc
entry for the endnotes pointing at "#section_endnotes", and the reason
is not a missing anchor tag: the abstraction has it. Spine makes an id
out of an anchor tag only when the object is an endnote, so every other
tag the markup placed is written as a target that is not one.
+/
private string sectionOpen(string name) {
string role = sectionRole(name);
return "<section class=\"" ~ name ~ "\""
~ ((role.length > 0) ? " role=\"" ~ role ~ "\"" : "") ~ ">\n";
}
/+ ↓ one object +/
string writeObj(ref const OcdaObject obj, ref OcdaDocument doc,
ref AnchorIds ids) {
switch (obj.is_a) {
case "heading": return heading(obj, doc, ids);
case "toc": return para(obj, doc, ids, "toc");
case "para": return para(obj, doc, ids, "para");
case "endnote": return para(obj, doc, ids, "endnote");
case "glossary": return para(obj, doc, ids, "glossary");
case "bibliography": return para(obj, doc, ids, "bibliography");
case "bookindex": return para(obj, doc, ids, "bookindex");
case "blurb": return para(obj, doc, ids, "blurb");
case "quote": return blockish(obj, doc, ids, "blockquote", "quote");
case "group": return blockish(obj, doc, ids, "div", "group");
case "block": return blockish(obj, doc, ids, "div", "block");
case "verse": return blockish(obj, doc, ids, "div", "verse");
case "code": return code(obj);
case "table": return table(obj, doc, ids);
default: return "";
}
}
/+ ↓ eight markup levels into six html ones.
.
A B C D are the title levels and 1 2 3 4 the body levels beneath them,
so the outline is eight deep and html offers six. The first six take an
element each; deeper headings take h6 and say their real depth in
aria-level, which is what a reading system uses and what h7 would have
meant if html had one.
.
Spine passes the markup level straight through, so a level A heading
becomes <h0>. There is no h0.
+/
int headingDepth(string level) {
switch (level) {
case "A": return 1;
case "B": return 2;
case "C": return 3;
case "D": return 4;
case "1": return 5;
case "2": return 6;
case "3": return 7;
case "4": return 8;
default: return 1;
}
}
private string heading(ref const OcdaObject obj, ref OcdaDocument doc,
ref AnchorIds ids) {
/+ ↓ a dummy heading is furniture the abstraction generated so that a
section has a heading, and its text repeats the real heading above
it. Its text is not rendered - spine does the same - but its anchors
*are*, because they are what the table of contents links to. Spine
drops both, which is why its own toc entry for the endnotes points at
a target that is not in the file.
+/
if (obj.dummy_heading) {
/+ ↓ its identifier is still a link target, and the table of contents
uses it, so it is kept even though the text is not +/
return ((obj.identifier.length > 0)
? "<a id=\"" ~ attr(obj.identifier) ~ "\"></a>" : "")
~ anchorTags(obj, ids);
}
int depth = headingDepth(obj.heading_level);
int element = (depth > 6) ? 6 : depth;
string txt = inlineMarkup(obj, doc, ids);
if (txt.strip.length == 0) { return ""; }
auto out_ = appender!string;
out_ ~= "<div class=\"substance\">";
out_ ~= ocnMark(obj);
out_ ~= "<h" ~ element.to!string;
if (obj.identifier.length > 0) {
out_ ~= " id=\"" ~ attr(obj.identifier) ~ "\"";
}
if (depth > 6) { out_ ~= " aria-level=\"" ~ depth.to!string ~ "\""; }
out_ ~= ">" ~ levelAnchor(obj, ids) ~ txt ~ "</h" ~ element.to!string ~ ">";
out_ ~= "</div>\n";
return out_.data;
}
/+ ↓ the name the markup gave this heading, which is what a link written as
"#that-name" points at: 1,854 of them in the reference collection, and a
link to one is the ordinary way a document refers to its own sections.
It is a second name for the same place, so it is an empty anchor rather
than the heading's own id.
+/
string levelAnchor(ref const OcdaObject obj, ref AnchorIds ids) {
if (obj.heading_lev_anchor.length == 0) { return ""; }
return ids.claim(obj.heading_lev_anchor)
? "<a id=\"" ~ attr(obj.heading_lev_anchor) ~ "\"></a>"
: "";
}
private string para(ref const OcdaObject obj, ref OcdaDocument doc,
ref AnchorIds ids, string cls) {
/+ ↓ a dummy table of contents entry names a heading the abstraction
generated, "[Intro]" or "[Part III]", and it is furniture in the same
way a dummy heading is. Its anchors are kept and its text is not.
.
Spine drops it here and *keeps* it in the text output, which is an
inconsistency between its own two writers rather than a rule.
+/
if (obj.dummy_heading) { return anchorTags(obj, ids); }
string txt = inlineMarkup(obj, doc, ids);
if (txt.strip.length == 0) { return ""; }
auto out_ = appender!string;
out_ ~= "<div class=\"substance\">";
out_ ~= ocnMark(obj);
out_ ~= "<p class=\"" ~ cls ~ "\"";
if (obj.identifier.length > 0) {
out_ ~= " id=\"" ~ attr(obj.identifier) ~ "\"";
}
/+ ↓ the indent is the document's, so it is data rather than presentation;
the stylesheet reads it +/
if (obj.indent_base > 0 || obj.indent_hang > 0) {
out_ ~= " data-indent=\"" ~ obj.indent_base.to!string
~ "\" data-hang=\"" ~ obj.indent_hang.to!string ~ "\"";
}
/+ ↓ a gathered endnote is a note body whatever section it sits in, and
the reference to it carries doc-noteref at the other end +/
if (obj.is_a == "endnote") { out_ ~= " role=\"doc-footnote\""; }
out_ ~= ">";
if (obj.bullet) { out_ ~= "<span class=\"bullet\">●</span> "; }
out_ ~= txt;
out_ ~= "</p></div>\n";
return out_.data;
}
/+ ↓ quote, group, block and verse: one object whose text holds its own line
structure, so the lines are kept and not re-flowed +/
private string blockish(ref const OcdaObject obj, ref OcdaDocument doc,
ref AnchorIds ids, string element, string cls) {
string txt = inlineMarkup(obj, doc, ids);
if (txt.strip.length == 0) { return ""; }
auto out_ = appender!string;
out_ ~= "<div class=\"substance\">";
out_ ~= ocnMark(obj);
out_ ~= "<" ~ element ~ " class=\"" ~ cls ~ "\"";
if (obj.identifier.length > 0) {
out_ ~= " id=\"" ~ attr(obj.identifier) ~ "\"";
}
out_ ~= ">" ~ txt ~ "</" ~ element ~ ">";
out_ ~= "</div>\n";
return out_.data;
}
/+ ↓ a code block: pre and code, and nothing inside it interpreted.
The text is escaped and otherwise untouched, which is the whole point
of a code block.
+/
private string code(ref const OcdaObject obj) {
auto out_ = appender!string;
out_ ~= "<div class=\"substance\">";
out_ ~= ocnMark(obj);
out_ ~= "<pre class=\"code\"";
if (obj.identifier.length > 0) {
out_ ~= " id=\"" ~ attr(obj.identifier) ~ "\"";
}
out_ ~= "><code";
if (obj.code_syntax.length > 0) {
out_ ~= " class=\"language-" ~ attr(obj.code_syntax) ~ "\"";
}
out_ ~= ">" ~ escape(obj.text) ~ "</code></pre>";
out_ ~= "</div>\n";
return out_.data;
}
/+ ↓ a table. The object's text is the whole table: rows separated by
newlines, columns by ┊. A header row becomes th, and the column widths
the markup declared become a colgroup, which is where a width belongs.
+/
private string table(ref const OcdaObject obj, ref OcdaDocument doc,
ref AnchorIds ids) {
auto rows = obj.text.split(rgx_h.table_delimiter_row);
auto widths = obj.table_widths.asplit(" ");
auto aligns = obj.table_aligns.asplit(" ");
auto out_ = appender!string;
out_ ~= "<div class=\"substance\">";
out_ ~= ocnMark(obj);
out_ ~= "<table class=\"table\"";
if (obj.identifier.length > 0) {
out_ ~= " id=\"" ~ attr(obj.identifier) ~ "\"";
}
out_ ~= ">";
if (widths.length > 0 && widths[0].length > 0) {
out_ ~= "<colgroup>";
foreach (w; widths) {
out_ ~= "<col style=\"width: " ~ attr(w) ~ "%\" />";
}
out_ ~= "</colgroup>";
}
bool first = true;
foreach (row; rows) {
if (row.strip.length == 0) { continue; }
bool header = first && obj.table_header;
string cell_element = header ? "th" : "td";
out_ ~= header ? "<thead><tr>" : "<tr>";
auto cols = row.split(rgx_h.table_delimiter_col);
foreach (i, cell; cols) {
string a = (i < aligns.length) ? aligns[i] : "l";
/+ ↓ a cell carries the same inline markup a paragraph does, line
breaks among it: a table of features has cells of several lines +/
out_ ~= "<" ~ cell_element ~ " class=\"align-" ~ alignName(a) ~ "\">"
~ lineBreaks(fontFace(escape(cell)), obj) ~ "</" ~ cell_element ~ ">";
}
out_ ~= header ? "</tr></thead>" : "</tr>";
first = false;
}
out_ ~= "</table></div>\n";
return out_.data;
}
private string alignName(string a) {
switch (a) {
case "c": return "centre";
case "r": return "right";
default: return "left";
}
}
/+ ↓ the object number, which is also the anchor every citation of this
object points at.
.
A span rather than a label: <label> is for form controls, and an object
number labels nothing that can be focused.
+/
/+ ↓ ... and the object's own .ocn_off property is deliberately *not*
consulted.
.
It looks as though it should be, and it is not what suppresses a number:
what suppresses one in spine is the *command line* option --ocn-off,
which is a request from the caller. The object property is set on
objects that do carry an ocn - every bibliography entry in one document
has both .ocn_off and an ocn of its own - and neither of spine's writers
reads it. Consulting it dropped the number from 298 bibliography entries
that spine numbers.
+/
string ocnMark(ref const OcdaObject obj) {
if (obj.ocn == 0) { return ""; }
string n = obj.ocn.to!string;
return "<span class=\"ocn\"><a href=\"#" ~ n ~ "\" class=\"lnkocn\">"
~ n ~ "</a></span>";
}
/+ ↓ everything inside an object's text, in the order the markers require:
images and links first, because their markers hold text that must not
be escaped as content, then notes, then font faces, then the line and
indent markers.
+/
string inlineMarkup(ref const OcdaObject obj, ref OcdaDocument doc,
ref AnchorIds ids) {
string txt = escape(obj.text);
txt = images(txt);
txt = links(txt, obj);
txt = notes(txt);
txt = fontFace(txt);
txt = anchors(txt);
txt = groupIndents(txt, obj);
txt = lineBreaks(txt, obj);
return anchorTags(obj, ids) ~ txt;
}
/+ ↓ the object's own named targets, ahead of its text.
A tag unique in the document becomes an id and so a link target; one
that repeats is recorded in a data attribute, which a script or a
stylesheet can find and a link cannot mistake for a destination.
+/
string anchorTags(ref const OcdaObject obj, ref AnchorIds ids) {
string out_;
foreach (a; obj.anchor_tags) {
out_ ~= ids.claim(a)
? "<a id=\"" ~ attr(a) ~ "\"></a>"
: "<span class=\"marker\" data-anchor=\"" ~ attr(a) ~ "\"></span>";
}
return out_;
}
/+ ↓ the four characters html reserves, and nothing else.
Done first, over the whole text, so that a "<" the author wrote is a
"<" the reader sees and not the start of an element. The markup's own
markers are not html and survive it.
+/
string escape(string txt) {
return txt
.replaceAll(rgx_h.ampersand, "&")
.replaceAll(rgx_h.less_than, "<")
.replaceAll(rgx_h.greater_than, ">")
.replaceAll(rgx_h.nbsp_char, " ");
}
/+ ↓ the same, for a value that goes in an attribute +/
string attr(string txt) {
return txt
.replaceAll(rgx_h.ampersand, "&")
.replaceAll(rgx_h.quotation, """)
.replaceAll(rgx_h.less_than, "<")
.replaceAll(rgx_h.greater_than, ">");
}
private string fontFace(string txt) {
return txt
.replaceAll(rgx_h.inline_emphasis, "<em>$1</em>")
.replaceAll(rgx_h.inline_bold, "<strong>$1</strong>")
.replaceAll(rgx_h.inline_italics, "<i>$1</i>")
.replaceAll(rgx_h.inline_underscore, "<u>$1</u>")
.replaceAll(rgx_h.inline_superscript, "<sup>$1</sup>")
.replaceAll(rgx_h.inline_subscript, "<sub>$1</sub>")
.replaceAll(rgx_h.inline_mono, "<code>$1</code>")
.replaceAll(rgx_h.inline_strike, "<del>$1</del>")
.replaceAll(rgx_h.inline_insert, "<ins>$1</ins>")
.replaceAll(rgx_h.inline_cite, "<cite>$1</cite>");
}
/+ ↓ a note reference, 【<n> the note text】.
.
The reference becomes a superscript link to the gathered note, and the
note's own text is dropped here because the endnotes section carries
it: an object and the endnotes section both holding it would put it in
the document twice.
+/
private string notes(string txt) {
/+ ↓ a note's mark may be a number or a run of stars or pluses, and both
are gathered into the endnotes section, so both need a reference.
.
The target goes through the resolver: in one file the note is in this
file, and in a segmented output it is in the endnotes file, which is
the same question a cross reference asks.
+/
return replaceAll!((m) {
string mark = m["num"].to!string;
return "<sup class=\"noteref\"><a id=\"noteref_" ~ mark
~ "\" href=\"" ~ resolveTarget("#note_" ~ mark)
~ "\" role=\"doc-noteref\">" ~ mark ~ "</a></sup>";
})(txt, rgx_h.inline_notes_al_all_note);
}
/+ ↓ how a link to a target inside the document is written.
.
In one file a "#x" is "#x". In a set of files it is
"<the file holding x>#x", and which file that is is a fact about the
document that only the whole document knows. So the object rendering
takes the answer rather than working it out, and the same code serves
the scroll html and the epub.
+/
alias LinkResolver = string delegate(string target);
/+ ↓ and the answer for a link the markup marked as internal to the
collection: "¤<segment>.fnSuffix#<fragment>", where ".fnSuffix" stands
where an output's file suffix goes.
.
The abstraction *names the segment* in such a link, which is exactly what
a segmented output needs and what a single file has no use for. So the
two writers answer differently, and both answers are one line.
+/
alias SegLinkResolver = string delegate(string segment, string fragment);
private LinkResolver current_resolver;
private SegLinkResolver current_seg_resolver;
/+ ↓ set for the duration of one document.
Module variables rather than parameters on nine functions: the rendering
is single threaded and one document at a time, and threading two
delegates through every shape to be read by one of them would be noise.
+/
void useLinkResolver(LinkResolver r, SegLinkResolver s = null) {
current_resolver = r;
/+ ↓ the one-file answer: the segment is this file, so only the fragment is
left, which is what spine's scroll output does +/
current_seg_resolver = (s is null)
? (string seg, string frag) => frag
: s;
}
private string resolveTarget(string target) {
/+ ↓ an internal link carries its segment; the resolver decides what to do
with it, and only it knows whether there is more than one file +/
if (target.length > 0 && target.startsWith("¤")) {
auto m = target.matchFirst(rgx_h.internal_target);
if (!m.empty) {
return current_seg_resolver(m["seg"].to!string, m["frag"].to!string);
}
}
return (current_resolver is null) ? target : current_resolver(target);
}
/+ ↓ a link, ┥text┝┤target├.
.
Before the writers see it the target may still be a number, which is an
index into the object's own .stow_link list rather than a url. That
indirection is the abstraction's, so undoing it is the writer's job.
+/
private string links(string txt, ref const OcdaObject obj) {
auto stow = obj.stow_link;
txt = replaceAll!((m) {
size_t num = m["num"].to!size_t;
string url = (num < stow.length) ? stow[num] : "";
/+ ↓ escaped here, because a url from the abstraction has not been
through the text escaping: the text was escaped before the writers
saw it and this comes from .stow_link, beside it.
.
An unescaped "&" in an href is a fatal xml parse error in an epub,
and one url in the collection has one:
"...boylesite/law&info.htm". epubcheck stopped reading the file at
it and then reported every id after that point as undefined - 378
errors from one ampersand, none of them where the fault was.
+/
return m["linked_text"] ~ "┤" ~ attr(url) ~ "├";
})(txt, rgx_h.inline_link_number_only);
/+ ↓ a link the markup left with no target at all keeps its text +/
txt = txt.replaceAll(rgx_h.inline_link_empty, "$1");
txt = replaceAll!((m) {
return "<a href=\"" ~ resolveTarget(m.captures[2].to!string) ~ "\">"
~ m.captures[1].to!string ~ "</a>";
})(txt, rgx_h.inline_link);
txt = txt.replaceAll(rgx_h.mark_internal_site_lnk, "");
return txt;
}
/+ ↓ an image, ┥☼<file>,w<W>h<H> "alt"┝┤target├.
.
alt is required. An image with nothing to say for itself takes alt="",
which is how to say "decorative, skip me"; no alt at all is an
omission, and a reading system then announces the filename.
.
The dimensions are dropped when they are zero, because width="0" is a
valid html integer meaning zero wide.
+/
private string images(string txt) {
return replaceAll!((m) {
string alt;
string rest = m["post"].to!string;
if (auto a = rest.matchFirst(rgx_h.inline_image_alt)) {
alt = a["alt"].to!string;
rest = a.post.to!string;
}
string dim = (m["width"] == "0" && m["height"] == "0")
? ""
: " width=\"" ~ m["width"].to!string
~ "\" height=\"" ~ m["height"].to!string ~ "\"";
/+ ↓ the opening link marker is kept, because an image in the markup sits
inside a link whose target may be empty or may be a url. Keeping it
lets the link handling that follows either drop the wrapper or make
the image a link, and dropping it here would leave the closing
marker behind as text.
+/
return m["pre"].to!string
~ "<img src=\"image/" ~ attr(m["img"].to!string) ~ "\" alt=\""
~ attr(alt) ~ "\"" ~ dim ~ " /> " ~ rest;
})(txt, rgx_h.inline_image);
}
/+ ↓ an anchor the markup placed in the text, ┃name┃ +/
private string anchors(string txt) {
return txt.replaceAll(rgx_h.inline_link_anchor, "<a id=\"$1\"></a>");
}
/+ ↓ the line-leading markers of a grouped object.
.
The indent is a number, so it is written as a number, in a data
attribute the stylesheet reads. Spine writes runs of instead, up
to eighteen of them for one line, which puts presentation in the
content and cannot be undone by a reader that wants its own.
+/
private string groupIndents(string txt, ref const OcdaObject obj) {
if (obj.is_a != "group") { return txt; }
return txt
.replaceAll(rgx_h.grouped_para_indent_hang,
"<span class=\"indent\" data-hang=\"$1\" data-indent=\"$2\"></span>")
.replaceAll(rgx_h.grouped_para_bullet_at,
"<span class=\"indent\" data-indent=\"$1\"></span>"
~ "<span class=\"bullet\">●</span> ")
.replaceAll(rgx_h.grouped_para_bullet,
"<span class=\"bullet\">●</span> ")
.replaceAll(rgx_h.grouped_para_indent,
"<span class=\"indent\" data-indent=\"$1\"></span>");
}
/+ ↓ the line breaks the markup asked for, and the ones a block preserves.
Not inside code, whose text is preserved as it stands.
+/
private string lineBreaks(string txt, ref const OcdaObject obj) {
if (obj.is_a == "code") { return txt; }
return txt
.replaceAll(rgx_h.br_line, "<br />\n")
.replaceAll(rgx_h.br_line_inline, "<br />\n")
.replaceAll(rgx_h.br_line_spaced, "<br />\n<br />\n")
.replaceAll(rgx_h.line_break, "<br />\n");
}
/+ ↓ the head.
.
One charset declaration, and a Dublin Core meta only where there is a
value for it. Spine writes two conflicting Content-Type metas, the
first of them text/plain, and writes dc.publisher="Publisher" and
dc.date.issued="year" as literal strings when it has nothing.
+/
private string head(ref OcdaDocument doc) {
string m(string key) { return doc.meta(key); }
string lang = ("language" in doc.source) ? doc.source["language"] : "en";
auto out_ = appender!string;
out_ ~= "<!DOCTYPE html>\n<html lang=\"" ~ attr(lang) ~ "\">\n<head>\n";
out_ ~= "<meta charset=\"utf-8\" />\n";
out_ ~= "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n";
string title = m("title.full");
if (title.length == 0) { title = m("title.main"); }
string author = m("creator.author");
out_ ~= "<title>" ~ escape(title)
~ (author.length > 0 ? ", " ~ escape(author) : "") ~ "</title>\n";
void dc(string name, string value) {
if (value.length == 0) { return; }
out_ ~= "<meta name=\"" ~ name ~ "\" content=\"" ~ attr(value) ~ "\" />\n";
}
dc("dc.title", title);
dc("dc.creator", author);
dc("dc.date", m("date.published"));
dc("dc.date.created", m("date.created"));
dc("dc.date.issued", m("date.issued"));
dc("dc.date.available", m("date.available"));
dc("dc.date.valid", m("date.valid"));
dc("dc.date.modified", m("date.modified"));
dc("dc.language", lang);
dc("dc.rights", m("rights.copyright"));
dc("dc.publisher", m("publisher"));
dc("dc.identifier", m("identifier.oclc"));
out_ ~= "<meta name=\"generator\" content=\"" ~ attr(generator_name) ~ "\" />\n";
out_ ~= "<style>\n" ~ stylesheet ~ "</style>\n";
out_ ~= "</head>\n<body>\n";
return out_.data;
}
private string tail(ref OcdaDocument doc) {
string m(string key) { return doc.meta(key); }
auto out_ = appender!string;
out_ ~= "<footer>\n";
void line(string label, string value) {
if (value.length == 0) { return; }
out_ ~= "<p class=\"meta\"><span class=\"label\">" ~ label
~ "</span> " ~ escape(value) ~ "</p>\n";
}
line("Title:", m("title.full"));
line("Author:", m("creator.author"));
line("Published:", m("date.published"));
line("Copyright:", m("rights.copyright"));
line("License:", m("rights.license"));
out_ ~= "<p class=\"generator\">" ~ escape(generator_name) ~ " <a href=\""
~ attr(generator_url) ~ "\">" ~ escape(generator_url) ~ "</a></p>\n";
out_ ~= "</footer>\n</body>\n</html>\n";
return out_.data;
}
/+ ↓ set to spine's own values so that the comparison is of the writer and
not of the signature +/
enum generator_name = "spine";
enum generator_url = "https://sisudoc.org";
/+ ↓ a stylesheet small enough to embed, so that the file renders on its
own. It styles what this writer emits and nothing else: the object
number in the margin, the indents the document declared, and the
blocks. Not spine's 4,747 line sheet, which is a site's and not a
document's.
+/
enum stylesheet = q"┃html { font-size: 100%; }
body {
margin: 0 auto; padding: 1em 1em 1em 4em; max-width: 46em;
font-family: Georgia, "Times New Roman", serif; line-height: 1.5;
color: #111; background: #fff;
}
@media (prefers-color-scheme: dark) {
body { color: #ddd; background: #121212; }
a { color: #8ab4f8; }
}
.substance { position: relative; }
.ocn {
position: absolute; left: -3.6em; width: 3em; text-align: right;
font-size: 0.7em; line-height: 2.2; color: #888;
}
.ocn a { color: inherit; text-decoration: none; }
h1, h2, h3, h4, h5, h6 { line-height: 1.25; font-weight: 600; }
h1 { font-size: 1.7em; } h2 { font-size: 1.4em; } h3 { font-size: 1.2em; }
h4, h5, h6 { font-size: 1.05em; }
p { margin: 0.6em 0; }
p[data-indent="1"] { margin-left: 1em; } p[data-indent="2"] { margin-left: 2em; }
p[data-indent="3"] { margin-left: 3em; } p[data-indent="4"] { margin-left: 4em; }
p[data-indent="5"] { margin-left: 5em; } p[data-indent="6"] { margin-left: 6em; }
p[data-indent="7"] { margin-left: 7em; } p[data-indent="8"] { margin-left: 8em; }
p[data-indent="9"] { margin-left: 9em; }
.indent { display: inline-block; }
.indent[data-indent="1"] { width: 1em; } .indent[data-indent="2"] { width: 2em; }
.indent[data-indent="3"] { width: 3em; } .indent[data-indent="4"] { width: 4em; }
.indent[data-indent="5"] { width: 5em; } .indent[data-indent="6"] { width: 6em; }
.indent[data-indent="7"] { width: 7em; } .indent[data-indent="8"] { width: 8em; }
.indent[data-indent="9"] { width: 9em; }
blockquote.quote { margin: 0.8em 2em; font-style: normal; }
div.group, div.block, div.verse { margin: 0.8em 0; }
div.verse { white-space: pre-wrap; }
pre.code {
overflow-x: auto; padding: 0.6em; background: #f4f4f4;
font-family: ui-monospace, "DejaVu Sans Mono", monospace; font-size: 0.9em;
}
@media (prefers-color-scheme: dark) { pre.code { background: #1e1e1e; } }
table.table { border-collapse: collapse; margin: 0.8em 0; width: 100%; }
table.table th, table.table td {
border: 1px solid #ccc; padding: 0.2em 0.4em; vertical-align: top;
}
.align-right { text-align: right; } .align-centre { text-align: center; }
sup.noteref a { text-decoration: none; }
section[role="doc-endnotes"] p { font-size: 0.9em; }
footer { margin-top: 3em; border-top: 1px solid #ccc; font-size: 0.85em; }
footer .label { color: #666; }
img { max-width: 100%; height: auto; }
┃";
|