Repository navigation
Expand file tree
/
Copy pathmarginalreader.cls
More file actions
1976 lines (1879 loc) · 87.6 KB
/
Copy pathmarginalreader.cls
File metadata and controls
1976 lines (1879 loc) · 87.6 KB
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
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
%%
%% marginalreader.cls
%% A LuaLaTeX class for graded readers with marginal glosses
%%
%% Copyright (C) 2026 Dino Sepúlveda
%%
%% This work may be distributed and/or modified under the conditions
%% of the LaTeX Project Public License, either version 1.3c of this
%% license or (at your option) any later version. The latest version
%% of this license is in
%% https://www.latex-project.org/lppl.txt
%% and version 1.3c or later is part of all distributions of LaTeX
%% version 2008-05-04 or later.
%%
%% This work has the LPPL maintenance status `maintained'.
%% The Current Maintainer of this work is Dino Sepúlveda.
%%
%% This work consists of the files marginalreader.cls,
%% marginalreader-margin.lua and marginalreader-linenums.lua, the
%% documentation in doc/, and the example and test documents
%% distributed with it.
%%
\NeedsTeXFormat{LaTeX2e}
\ProvidesClass{marginalreader}[2026/08/22 v0.1 Graded reader with marginal glosses]
%% --- Engine check ---
\RequirePackage{iftex}
\ifluatex\else
\ClassError{marginalreader}{LuaLaTeX is required}
{This class must be compiled with lualatex.}
\fi
%% --- Option declarations ---
\RequirePackage{xkeyval}
% Paper and layout
\DeclareOptionX{paper}[b5]{\def\mr@paper{#1}}
\DeclareOptionX{twoside}[true]{\def\mr@twoside{#1}}
\DeclareOptionX{chapteralibi}[any]{\def\mr@chapteralibi{#1}}
% Fonts
\DeclareOptionX{fonts}[ebgaramond]{\def\mr@fonts{#1}}
% Base font size, handed to scrbook (any size KOMA accepts, WITH its
% unit: fontsize=9.5pt, fontsize=11pt, ...). The value is also used
% to derive \headheight, so a unit is required.
\DeclareOptionX{fontsize}[10pt]{\def\mr@fontsize{#1}}
% Interlinear spacing, so students can mark the text between the
% lines: single (default), 1.5, 2, 2.5 or 3 -- word-processor
% semantics, multiplying the normal leading (at 10pt/12pt, 2 means a
% 24pt baseline grid). Any other numeric value is accepted as a raw
% \linespread factor. Applies to the body text; the margin apparatus
% and footnotes deliberately stay single-spaced (see the line-spacing
% section below).
\DeclareOptionX{linespacing}[single]{\def\mr@linespacing{#1}}
% Rules
\DeclareOptionX{toprule}[solid]{\def\mr@toprule{#1}}
\DeclareOptionX{columnrule}[solid]{\def\mr@columnrule{#1}}
% Umbrella: rules=<style> sets toprule and columnrule together --
% rules=none removes both at once. Options are processed in the order
% given, so a later toprule=/columnrule= still overrides one of the
% pair (and vice versa).
\DeclareOptionX{rules}[solid]{\def\mr@toprule{#1}\def\mr@columnrule{#1}}
\DeclareOptionX{rulecolor}[black]{\def\mr@rulecolor{#1}}
\DeclareOptionX{rulewidth}[0.4pt]{\def\mr@rulewidth{#1}}
\DeclareOptionX{ruledash}[2pt/2pt]{\def\mr@ruledash{#1}}
% Line numbering
\DeclareOptionX{linenumbers}[false]{\def\mr@linenumbers{#1}}
\DeclareOptionX{linenumincrement}[5]{\def\mr@linenumincrement{#1}}
\DeclareOptionX{firstlinenum}[1]{\def\mr@firstlinenum{#1}}
% Marginalia
\DeclareOptionX{numberedmargin}[false]{\def\mr@numberedmargin{#1}}
\DeclareOptionX{glosssep}[\space\space]{\def\mr@glosssep{#1}}
% Spacing
\DeclareOptionX{marginpush}[4pt]{\def\mr@marginpush{#1}}
% Table of contents: by default each page number is set just after
% its entry (a fixed 1.5em gap, ragged line end); dottedtoc restores
% dotted leaders with flush-right numbers instead.
\def\mr@dottedtoc{false}
\DeclareOptionX{dottedtoc}[true]{\def\mr@dottedtoc{#1}}
% Drafting: date + time + version string in the centre foot of every
% page, implemented with the class's own scrlayer-scrpage footers
% rather than prelim2e. \myVersion can be defined by the document;
% \finalVersionString is provided for colophons either way.
\def\mr@drafting{false}
\DeclareOptionX{drafting}[true]{\def\mr@drafting{#1}}
\ExecuteOptionsX{paper,twoside,chapteralibi,fonts,fontsize,linespacing,
toprule,columnrule,rulecolor,rulewidth,ruledash,linenumbers,
linenumincrement,firstlinenum,numberedmargin,glosssep,marginpush}
\ProcessOptionsX\relax
%% --- Load base class ---
% NO paper option is passed to scrbook here: the geometry package owns
% the final page size (paperwidth/paperheight from the presets below),
% exactly as it already owns every margin. Two findings forced this
% design, both confirmed with minimal probe classes:
% * a paper=<w>:<h> value routed through \PassOptionsToClass +
% \LoadClass arrives at scrbook with its VALUE STRIPPED on this
% kernel (the log's unused-option list shows a bare "paper"), so
% the dimension pair silently never applied -- KOMA stayed at A4;
% * the named sizes (b5, a5, letter...) only ever worked because
% \documentclass options leak through to \LoadClass-loaded classes
% as GLOBAL options, which KOMA's own parser does understand.
% That leak still happens and is harmless: for a name KOMA knows it
% pre-sets the same size the preset then re-asserts; for one it does
% not (6x9, 5.5x8.5...) it stays at its default until geometry sets
% the real size. A paper value with NO preset below (e.g. an explicit
% paper=17cm:24cm) is honoured the same way: KOMA parses the leaked
% global -- the w:h form does work at \documentclass level -- and the
% preset dispatcher's fallback keeps geometry's paperwidth/paperheight
% at whatever KOMA concluded.
\edef\mr@twoside@opt{%
\ifx\mr@twoside\@empty twoside=true\else twoside=\mr@twoside\fi
}
\PassOptionsToClass{\mr@twoside@opt,headinclude=true,footinclude=false,fontsize=\mr@fontsize}{scrbook}
\LoadClass{scrbook}
%% --- Line spacing ---
% linespacing class option (see its declaration above): named values
% map to \linespread factors; anything else is taken as a raw numeric
% factor. \linespread multiplies \baselineskip at the next size-
% selecting \selectfont, i.e. from \begin{document}'s \normalsize
% onwards. Places that must STAY single-spaced regardless -- the
% margin apparatus (\mr@marginitem's parbox) and the footnote block
% (KOMA's `footnote' font element, below) -- locally reset the factor
% to 1: a doubled body is for students' pens, a doubled 8pt gloss
% column would only thrash the margin overflow machinery, and more
% gloss lines then fit beside each opened-up body line.
\@namedef{mr@lsp@single}{1}
\@namedef{mr@lsp@1}{1}
\@namedef{mr@lsp@1.5}{1.5}
\@namedef{mr@lsp@2}{2}
\@namedef{mr@lsp@2.5}{2.5}
\@namedef{mr@lsp@3}{3}
\@ifundefined{mr@lsp@\mr@linespacing}{%
\edef\mr@lsp@factor{\mr@linespacing}%
}{%
\edef\mr@lsp@factor{\@nameuse{mr@lsp@\mr@linespacing}}%
}
\linespread{\mr@lsp@factor}
% Footnotes single-spaced: KOMA applies the `footnote' font element
% inside the footnote insert's own group, which is exactly the scope
% needed. The element REPLACES the default, so \footnotesize must be
% restated.
\setkomafont{footnote}{\linespread{1}\footnotesize}
%% --- Required packages ---
\RequirePackage{fontspec}
\RequirePackage[tracking=true]{microtype}
\RequirePackage{xcolor}
\RequirePackage{graphicx}
\RequirePackage{ragged2e}
\RequirePackage{textcase}
\RequirePackage{scrlayer-scrpage}
\RequirePackage{luacode}
\RequirePackage{booktabs}
%% --- Geometry ---
% Define class lengths
\newlength{\mr@marginsep}
\setlength{\mr@marginsep}{5mm}
\newlength{\marginwidth}
\newlength{\fullwidth}
\newlength{\mr@outermargin}
\newlength{\mr@innermargin}
% Vertical dimensions, preset-overridable (defaults are the b5
% values): top/bottom page margins and the footskip used both by the
% reader geometry below and by \mr@plainmattergeometry (front/back
% matter). Scaled per paper so a pocket 4.5x6.5 page is not carrying
% letter-sized margins and vice versa.
\newlength{\mr@topmargin}
\setlength{\mr@topmargin}{14mm}
\newlength{\mr@bottommargin}
\setlength{\mr@bottommargin}{14mm}
\newlength{\mr@footskip}
\setlength{\mr@footskip}{8mm}
% Head height: just over one REAL \baselineskip -- i.e. after both
% fontsize= (KOMA's per-size leading table) and linespacing= (the
% \linespread factor) have taken effect; the \normalsize forces
% \baselineskip current before it is read. scrlayer-scrpage checks
% \headheight against exactly this spaced \baselineskip at
% \begin{document} and, when found short, silently enlarges it --
% AFTER geometry laid the page out but BEFORE the Lua margin manager
% reads \headheight for its text-top derivation, skewing every rule
% and carried-item position by the difference (up to ~15pt at
% linespacing=2; observed). Meeting the check up front keeps the
% whole chain consistent.
\normalsize
\newlength{\mr@headheight}
\setlength{\mr@headheight}{\dimexpr1.05\baselineskip\relax}
% Physical page size, set by the presets and handed to the geometry
% package (see the paper-option note above \LoadClass for why KOMA is
% not trusted with it). Initialised from whatever KOMA concluded, so a
% paper value with no preset (a KOMA name like a3, or an explicit
% w:h pair, both parsed from the leaked \documentclass option) keeps
% its size.
\newlength{\mr@paperwidth}
\setlength{\mr@paperwidth}{\paperwidth}
\newlength{\mr@paperheight}
\setlength{\mr@paperheight}{\paperheight}
% Paper-specific geometry presets. Proportions follow the b5 reference
% design (paper 176mm wide: inner ~7%, text ~60%, gloss column ~21%,
% outer edge ~9%; top/bottom ~5.6% of the paper height), rounded to
% whole millimetres; the remaining outer-edge gap is
% paperwidth - inner - textwidth - marginsep - marginwidth, and stays
% between ~10mm (pocket) and ~23mm (letter) across the presets.
% Declared with \@namedef -- see the paper-size mapping note above for
% why \def cannot spell these names.
\@namedef{mr@setup@geometry@b5}{% 176mm x 250mm; edge gap 16mm
\setlength{\mr@paperwidth}{176mm}%
\setlength{\mr@paperheight}{250mm}%
\setlength{\mr@innermargin}{12mm}%
\setlength{\textwidth}{105mm}%
\setlength{\marginwidth}{38mm}%
}
\@namedef{mr@setup@geometry@a5}{% 148mm x 210mm; edge gap 15mm
\setlength{\mr@paperwidth}{148mm}%
\setlength{\mr@paperheight}{210mm}%
\setlength{\mr@innermargin}{10mm}%
\setlength{\textwidth}{88mm}%
\setlength{\marginwidth}{30mm}%
\setlength{\mr@topmargin}{12mm}%
\setlength{\mr@bottommargin}{12mm}%
\setlength{\mr@footskip}{7mm}%
}
\@namedef{mr@setup@geometry@a4}{% 210mm x 297mm; edge gap 20mm
\setlength{\mr@paperwidth}{210mm}%
\setlength{\mr@paperheight}{297mm}%
\setlength{\mr@innermargin}{15mm}%
\setlength{\textwidth}{120mm}%
\setlength{\marginwidth}{50mm}%
\setlength{\mr@topmargin}{16mm}%
\setlength{\mr@bottommargin}{16mm}%
}
\@namedef{mr@setup@geometry@letter}{% 8.5in x 11in; edge gap ~23mm
\setlength{\mr@paperwidth}{8.5in}%
\setlength{\mr@paperheight}{11in}%
\setlength{\mr@innermargin}{15mm}%
\setlength{\textwidth}{125mm}%
\setlength{\marginwidth}{48mm}%
\setlength{\mr@topmargin}{16mm}%
\setlength{\mr@bottommargin}{16mm}%
}
\@namedef{mr@setup@geometry@halfletter}{% 5.5in x 8.5in; edge gap ~13mm
\setlength{\mr@paperwidth}{5.5in}%
\setlength{\mr@paperheight}{8.5in}%
\setlength{\mr@innermargin}{10mm}%
\setlength{\textwidth}{83mm}%
\setlength{\marginwidth}{29mm}%
\setlength{\mr@topmargin}{12mm}%
\setlength{\mr@bottommargin}{12mm}%
\setlength{\mr@footskip}{7mm}%
}
% "5.5x8.5" is the same trim as half letter -- one geometry, two names.
\@namedef{mr@setup@geometry@5.5x8.5}{%
\@nameuse{mr@setup@geometry@halfletter}%
}
\@namedef{mr@setup@geometry@6x9}{% 6in x 9in; edge gap ~14mm
\setlength{\mr@paperwidth}{6in}%
\setlength{\mr@paperheight}{9in}%
\setlength{\mr@innermargin}{11mm}%
\setlength{\textwidth}{90mm}%
\setlength{\marginwidth}{32mm}%
\setlength{\mr@topmargin}{13mm}%
\setlength{\mr@bottommargin}{13mm}%
}
\@namedef{mr@setup@geometry@7x10}{% 7in x 10in; edge gap ~16mm
\setlength{\mr@paperwidth}{7in}%
\setlength{\mr@paperheight}{10in}%
\setlength{\mr@innermargin}{12mm}%
\setlength{\textwidth}{106mm}%
\setlength{\marginwidth}{39mm}%
}
\@namedef{mr@setup@geometry@4.5x6.5}{% 4.5in x 6.5in; edge gap ~10mm
\setlength{\mr@paperwidth}{4.5in}%
\setlength{\mr@paperheight}{6.5in}%
\setlength{\mr@innermargin}{8mm}%
\setlength{\textwidth}{68mm}%
\setlength{\marginwidth}{24mm}%
\setlength{\mr@marginsep}{4mm}%
\setlength{\mr@topmargin}{10mm}%
\setlength{\mr@bottommargin}{12mm}%
\setlength{\mr@footskip}{7mm}%
}
\@ifundefined{mr@setup@geometry@\mr@paper}{%
\@nameuse{mr@setup@geometry@b5}%
}{%
\csname mr@setup@geometry@\mr@paper\endcsname
}
% Compute derived lengths. \mr@outermargin is derived from
% \mr@paperwidth, NOT \paperwidth: until geometry (below) applies the
% preset, \paperwidth still holds whatever KOMA concluded, which for a
% preset KOMA does not know by name is its A4 default.
\setlength{\fullwidth}{\dimexpr\textwidth + \mr@marginsep + \marginwidth\relax}
\setlength{\mr@outermargin}{\dimexpr\mr@paperwidth - \mr@innermargin - \textwidth\relax}
% Apply geometry via geometry package
\RequirePackage{geometry}
\geometry{
paperwidth=\mr@paperwidth,
paperheight=\mr@paperheight,
inner=\mr@innermargin,
outer=\mr@outermargin,
textwidth=\textwidth,
top=\mr@topmargin,
bottom=\mr@bottommargin,
% One \baselineskip of head height: the running head is a full
% \small text line (title small caps + page number),
% and scrlayer-scrpage warns below \baselineskip -- then silently
% enlarges \headheight itself, which would skew the Lua margin
% manager's text-top derivation (it reads \headheight at
% \AtBeginDocument, AFTER that enlargement, while geometry laid the
% page out with the smaller value). Scaled with fontsize= via
% \mr@headheight.
headheight=\mr@headheight,
headsep=3mm,
footskip=\mr@footskip,
marginparsep=\mr@marginsep,
marginparwidth=\marginwidth,
nofoot=false,
}
%% --- Font presets ---
\def\mr@setup@fonts@ebgaramond{%
\setmainfont{EB Garamond}[
Ligatures=TeX,
Numbers=OldStyle,
]%
\setsansfont{TeX Gyre Heros}[
Ligatures=TeX,
Scale=MatchLowercase,
]%
}
\def\mr@setup@fonts@palatino{%
\setmainfont{TeX Gyre Pagella}[
Ligatures=TeX,
Numbers=OldStyle,
]%
\setsansfont{TeX Gyre Heros}[
Ligatures=TeX,
Scale=MatchLowercase,
]%
}
\def\mr@setup@fonts@quadraat{%
\setmainfont{FF Quadraat}[
Ligatures=TeX,
Numbers=OldStyle,
]%
\setsansfont{FF Scala Sans}[
Ligatures=TeX,
Scale=MatchLowercase,
]%
}
\def\mr@setup@fonts@none{}
\@ifundefined{mr@setup@fonts@\mr@fonts}{%
\ClassWarning{marginalreader}{Unknown font preset '\mr@fonts', using ebgaramond}%
\mr@setup@fonts@ebgaramond
}{%
\csname mr@setup@fonts@\mr@fonts\endcsname
}
%% --- Small caps commands ---
\newcommand{\spacedlowsmallcaps}[1]{%
\textls[80]{\textsc{\MakeTextLowercase{#1}}}%
}
\newcommand{\spacedallcaps}[1]{%
\textls[160]{\textsc{\MakeTextUppercase{#1}}}%
}
% Declarative variant of the above: a font SWITCH for places that
% cannot take an argument-form command --
% KOMA font elements, tocbasic entry formats. fontspec's LetterSpace=8
% tracks by 0.08em, the same amount \spacedlowsmallcaps applies via
% \textls[80]. Titles are not lowercased by it (no declarative
% \MakeTextLowercase exists); capital initials render as full caps
% within the small caps.
\DeclareRobustCommand{\spacedlowsmallcapsstyle}{%
\scshape\addfontfeature{LetterSpace=8.0}%
}
%% --- Margin font setup ---
\newcommand{\mr@glossstyle}{\itshape}
\newcommand{\mr@glossdefstyle}{\upshape}
\newcommand{\mr@marginfontsetup}{\footnotesize\RaggedRight}
\newcommand{\glossstyle}[1]{\renewcommand{\mr@glossstyle}{#1}}
\newcommand{\glossdefstyle}[1]{\renewcommand{\mr@glossdefstyle}{#1}}
\newcommand{\marginfontsetup}[1]{\renewcommand{\mr@marginfontsetup}{#1}}
% Public name for this font slot (design spec sec. 8: \rmfamily,
% \sffamily, \glossfont, \linenumfont) -- \mr@marginfontsetup already
% supplies \glossfont's actual role (see \linenumfont's own comment,
% below); this just gives it the user-facing name the spec promises.
\let\glossfont\mr@marginfontsetup
%% --- Chapter formatting ---
% KOMA's default heading font ("disposition", inherited by chapter,
% section and subsection alike) is \sffamily\bfseries (see \sectfont in
% scrbook.cls). That is the wrong base for this class's small-caps
% headings, which should sit in the body serif at
% normal weight -- \spacedlowsmallcaps and \itshape below add the actual
% distinguishing style. Reset it once here for all heading levels.
\addtokomafont{disposition}{\normalfont}
% chapteralibi: control where chapters can start
% (KOMA's real 'open' key toggles \if@openright, consulted by \chapter
% itself as \if@openright\cleardoublepage\else\clearpage\fi -- this is
% the only mechanism needed here.)
\def\mr@chapteralibi@any{%
\KOMAoptions{open=any}%
}
\def\mr@chapteralibi@recto{%
\KOMAoptions{open=right}%
}
\def\mr@chapteralibi@newpage{%
\KOMAoptions{open=any}%
}
\@ifundefined{mr@chapteralibi@\mr@chapteralibi}{%
\mr@chapteralibi@any
}{%
\csname mr@chapteralibi@\mr@chapteralibi\endcsname
}
% Blank pages inserted by \cleardoublepage (before part pages and
% part-opening chapters, and wherever the document uses it) carry no
% header or folio.
\KOMAoptions{cleardoublepage=empty}
% Chapter: spaced low small caps, Large, flush left, no number prefix
\setcounter{secnumdepth}{-1}
\RedeclareSectionCommand[
beforeskip=2\baselineskip,
afterskip=1\baselineskip,
font=\Large,
prefixfont=\Large,
afterindent=false,
]{chapter}
\renewcommand*{\chapterformat}{}
\renewcommand*{\raggedchapter}{\raggedright}
% Small caps are applied to the chapter title itself in
% \chapterlinesformat below (via \spacedlowsmallcaps); the family/weight
% reset happens once, above, via \addtokomafont{disposition}{\normalfont}.
\renewcommand{\chapterlinesformat}[3]{%
\@hangfrom{#2}{\spacedlowsmallcaps{#3}}%
}
% Section: spaced low small caps, large, flush left
\RedeclareSectionCommand[
beforeskip=1\baselineskip,
afterskip=0.5\baselineskip,
font=\large,
afterindent=false,
]{section}
% Blank the number prefix so sections stay unnumbered in print even
% while secnumdepth permits numbering (see below) -- mirrors chapter.
\renewcommand*{\sectionformat}{}
% \sectionmarkformat is a separate hook (used by \automark for the
% running header, see below) that defaults to \thesection\autodot\enskip
% independently of \sectionformat -- blank it too, or section numbers
% leak into the recto running head even though the printed heading and
% \sectionformat are unnumbered.
\renewcommand*{\sectionmarkformat}{}
% \sectionlinesformat is KOMA's shared formatting hook for every
% "section-style" heading level (section, subsection, ...) -- #1 here is
% the level name. Small caps are only wanted for \section itself; without
% discriminating on #1, \subsection below would incorrectly inherit them
% too (it only wants italic, per spec).
\renewcommand{\sectionlinesformat}[4]{%
\Ifstr{#1}{section}{%
\@hangfrom{#3}{\spacedlowsmallcaps{#4}}%
}{%
\@hangfrom{#3}{#4}%
}%
}
% Subsection: italic, normal size
\RedeclareSectionCommand[
beforeskip=0.75\baselineskip,
afterskip=0.25\baselineskip,
font=\normalsize\itshape,
afterindent=false,
]{subsection}
% Same rationale as \sectionformat above.
\renewcommand*{\subsectionformat}{}
% Subsubsection: italic, normal size, styled the same as subsection.
% Unnumbered in print like every other level.
\RedeclareSectionCommand[
beforeskip=0.75\baselineskip,
afterskip=0.25\baselineskip,
font=\normalsize\itshape,
afterindent=false,
]{subsubsection}
\renewcommand*{\subsubsectionformat}{}
% Paragraph: run-in, spaced low small caps, with the body text
% following on the same line. runin=true must be EXPLICIT: KOMA's
% default for the level is runin=bysign (negative afterskip means
% run-in), so a plain positive afterskip here silently turned the
% level into a display heading. With runin=true, afterskip is the
% HORIZONTAL gap between title and text. Run-in levels format their
% title through \sectioncatchphraseformat, not \sectionlinesformat;
% #1 is the level name, #4 the title text.
\RedeclareSectionCommand[
runin=true,
beforeskip=1\baselineskip,
afterskip=1em,
font=\normalsize,
]{paragraph}
\renewcommand*{\paragraphformat}{}
\renewcommand{\sectioncatchphraseformat}[4]{%
\hskip #2#3\spacedlowsmallcaps{#4}%
}
% Description labels: spaced low small caps.
\renewcommand*{\descriptionlabel}[1]{%
\hspace{\labelsep}\spacedlowsmallcaps{#1}%
}
% Part: the division above chapters (hierarchy PART > CHAPTER >
% SECTION > subsection). A part gets its own page:
% centred (KOMA's \raggedpart default), no header or footer
% (\partpagestyle empty), label line "pars i" (\partname under babel's
% main language) in spaced small caps above the title in larger spaced
% small caps. Rules and margin apparatus are switched off around the
% part page by the \part wrapper defined further down, next to
% \mr@flushmargins, which it reuses.
%
% \mr@scspaced: internal alias of the public declarative switch
% \spacedlowsmallcapsstyle (defined in the small-caps section above);
% kept so the KOMA font elements and TOC formats below read the same
% as before the public name existed.
\newcommand*{\mr@scspaced}{\spacedlowsmallcapsstyle}
\renewcommand*{\thepart}{\roman{part}}
\renewcommand*{\partformat}{\MakeTextLowercase{\partname}~\thepart}
\renewcommand*{\partpagestyle}{empty}
\setkomafont{partprefix}{\large\mr@scspaced}
\setkomafont{part}{\LARGE\mr@scspaced}
% \parttext{...} -- a short text (an argumentum, a quotation) set
% under the NEXT part's title on the part page. Implemented on KOMA's
% own \setpartpreamble: position [u] = under the title, in a narrower
% centred measure. Line counting
% is suspended around the text (the part page carries no reader
% apparatus; without the push/pop the preamble's lines would be
% counted and numbered as body text). Like \ctparttext, it applies to
% the next \part only -- KOMA clears the preamble after use.
\newcommand{\parttext}[1]{%
% [uc]: under the title, box centred on the text block. Inside:
% justified at 3/4 measure, one blank line below the title -- a
% quotation-block look, without the quote environment's asymmetric
% indents.
\setpartpreamble[uc][.75\textwidth]{%
\directlua{marginalreader_linenums.display_push()}%
\vspace*{\baselineskip}%
\normalfont\normalsize
#1\par
\directlua{marginalreader_linenums.display_pop()}%
}%
}
% Table of contents:
% * part entries: spaced small caps, no page number (tocbasic's
% pagenumberformat with a one-argument gobble);
% * chapter entries: spaced small caps, entry number in small caps;
% * section/subsection entries: normal font, entry number in
% lowercase small caps;
% * page numbers sit just after their entry, separated by a fixed
% 1.5em gap, unless the
% dottedtoc class option asks for dotted leaders with flush-right
% numbers -- then tocbasic's default fill is left alone.
\newcommand{\mr@tocpartformat}[1]{\large\mr@scspaced #1}
\newcommand{\mr@tocpartnopage}[1]{}
\newcommand{\mr@tocchapformat}[1]{\mr@scspaced #1}
% Small-caps entry numbers. No \MakeTextLowercase here, twice over:
% the numbers are digits and lowercase roman already, and textcase's
% case scan breaks inside a tocbasic entrynumberformat once hyperref
% wraps the entry in its link anchors (probed: 8 errors per TOC with
% it, none without).
\newcommand{\mr@tocscnum}[1]{\textsc{#1}}
\DeclareTOCStyleEntry[
entryformat=\mr@tocpartformat,
pagenumberformat=\mr@tocpartnopage,
]{tocline}{part}
\DeclareTOCStyleEntry[
entryformat=\mr@tocchapformat,
entrynumberformat=\mr@tocscnum,
pagenumberformat=\normalfont,
]{tocline}{chapter}
\DeclareTOCStyleEntry[
entrynumberformat=\mr@tocscnum,
]{tocline}{section}
\DeclareTOCStyleEntry[
entrynumberformat=\mr@tocscnum,
]{tocline}{subsection}
\Ifstr{\mr@dottedtoc}{true}{}{%
\DeclareTOCStyleEntries[
linefill=\hspace*{1.5em},
raggedpagenumber=true,
]{tocline}{chapter,section,subsection}%
}
%% --- Running headers ---
% Headlines: the page number lives IN the header, hanging into the outer
% margin past the text block with a 2em separation; verso carries the
% chapter title, recto the section title, both in spaced low small
% caps; regular pages have no footer at all, and chapter-opening pages
% (KOMA's `plain.scrheadings`) carry only a small page number in the
% outer footer.
\pagestyle{scrheadings}
\clearpairofpagestyles
\automark[section]{chapter}
% The "cap. N" prefix hooks are unused under this layout (marks carry
% the bare title); blanked so nothing re-adds a number.
\renewcommand*{\chaptermarkformat}{}
% \automark (above) redefines \chaptermark/\sectionmark itself -- via
% \def, deep inside scrlayer.sty's \automark@leftlow/
% \automark@righthigh -- to \markboth/\markright the title text (#1)
% UNSTYLED. The stored mark itself must be in spaced low small caps,
% so both commands are overridden again here. This MUST come AFTER
% \automark, not before --
% \automark's own \def would otherwise silently clobber a
% \renewcommand* placed earlier, discarding this fix (confirmed against
% this installation's scrlayer.sty). \chaptermark sets BOTH marks, so
% a recto page in a section-less chapter still shows the chapter title
% instead of an empty header. Sections print unnumbered in this class
% (see \sectionformat above), so the section mark carries the bare
% title, with no number prefix.
\renewcommand*{\chaptermark}[1]{%
\markboth{\spacedlowsmallcaps{#1}}{\spacedlowsmallcaps{#1}}%
}
\renewcommand*{\sectionmark}[1]{%
\markright{\spacedlowsmallcaps{#1}}%
}
% KOMA's default running-head font is slanted (\slshape); the header
% titles here are already spaced small caps and must stay upright,
% one size down from the body.
\setkomafont{pageheadfoot}{\normalfont\small}
% Verso (even): number hangs left of the text block into the outer
% margin; head flush left. Recto (odd): mirrored. \llap/\rlap keep the
% number outside the header's own width, so the title always aligns
% with the text block edge.
\lehead{\mbox{\llap{\small\pagemark\kern2em}\headmark}}
\rohead{\mbox{\headmark\rlap{\kern2em\small\pagemark}}}
% Chapter-opening (plain) pages: small page number in the outer footer,
% nothing else. Regular pages get no footer (the number is in the
% header).
\ofoot[\small\pagemark]{}
%% --- Drafting stamp ---
% drafting=true: every scrheadings/
% plain page carries a small centre-foot stamp with the date, time and
% version -- implemented with the class's own footers rather than
% prelim2e, which would fight scrlayer-scrpage for the foot line.
% (Pages set with \thispagestyle{empty} -- the part pages, title
% pages -- carry no stamp; a draft circulating for comment does not
% need its display pages stamped.) \myVersion may be defined by the
% document before or after loading; \finalVersionString is provided
% either way, for colophons.
\RequirePackage{scrtime}
% The fallback \myVersion is only provided at \begin{document}, so a
% document's own preamble \newcommand{\myVersion}{...} still works
% (providing it at class-load time made that a "Command already
% defined" error; the footer and \finalVersionString expand it at use
% time, well after this hook).
\AddToHook{begindocument}{\providecommand{\myVersion}{}}
\newcommand{\finalVersionString}{%
\emph{Final version} as of \today\ (\myVersion).%
}
\Ifstr{\mr@drafting}{true}{%
\renewcommand{\finalVersionString}{}%
\cfoot[{\footnotesize[\,\today\ at \thistime\ -- \myVersion\,]}]%
{\footnotesize[\,\today\ at \thistime\ -- \myVersion\,]}%
}{}
% Re-enable section numbering in internal counters (but not in print)
\setcounter{secnumdepth}{2}
\RedeclareSectionCommand[counterwithout={}]{chapter}
%% --- Lua margin manager ---
\RequirePackage{luatexbase}
\directlua{marginalreader_margin = require("marginalreader-margin")}
% Pass final geometry to the Lua module and register its page-processing
% callback (collision avoidance / cross-page flow -- see
% marginalreader-margin.lua). Deferred to \AtBeginDocument so every
% length below has its final, geometry-package-adjusted value.
% The rule geometry (text_top / text_left_recto / text_left_verso /
% paper_height, added to the table below) is resolved from this
% document's FINAL, post-geometry-package layout parameters (\topmargin,
% \headheight, \headsep, \oddsidemargin, \evensidemargin, \voffset,
% \hoffset, \paperheight), via the standard LaTeX invariant "distance
% from a paper edge to the text block = 1in + offset + margin" -- NOT
% recomputed from headheight/headsep/margin lengths in Lua the way the
% an early draft did (that code hardcoded "18mm"/"6mm"/
% "14pt" magic numbers, which do not track this class's real,
% overridable geometry). Both this formula and the coordinate convention
% it feeds into (marginalreader-margin.lua's M.draw_rules) were checked
% against this class's own rendered output before being relied on here:
% compiled a probe document with running text spanning several pages,
% and cross-checked the formula's predicted text-block top/left
% positions against `pdftotext -bbox` measurements of the actual first
% line of body text on non-chapter-opening recto and verso pages --
% agreement was within ~0.1mm (a font glyph-ink-vs-typeset-box-edge
% level discrepancy), on both axes, for both page parities. See
% the development notes for the full derivation and measurements.
\AtBeginDocument{%
\directlua{
marginalreader_margin.configure({
margin_width = tex.sp("\the\marginwidth"),
margin_sep = tex.sp("\the\mr@marginsep"),
inner_margin = tex.sp("\the\mr@innermargin"),
text_width = tex.sp("\the\textwidth"),
margin_push = tex.sp("\mr@marginpush"),
page_height = tex.sp("\the\textheight"),
is_twoside = \if@twoside true\else false\fi,
text_top = tex.sp("\the\dimexpr 1in+\voffset+\topmargin+\headheight+\headsep\relax"),
text_left_recto = tex.sp("\the\dimexpr 1in+\hoffset+\oddsidemargin\relax"),
text_left_verso = tex.sp("\the\dimexpr 1in+\hoffset+\evensidemargin\relax"),
paper_height = tex.sp("\the\paperheight"),
topskip = tex.sp("\the\topskip"),
overlay_box = \number\mr@overlaybox,
})
marginalreader_margin.register()
}%
% read back the PREVIOUS compile's recorded anchor positions
% (marginalreader-margin.lua's side file, written at that run's Lua
% `stop_run` callback) and run the collision-avoidance/cross-page-flow
% pre-pass over them, BEFORE the document body below is typeset --
% every \mr@marginitem call after this point consults the result via
% M.get_correction. Must run after M.configure, just above (the
% pre-pass needs the geometry fields set there), which is why this is
% a separate \directlua call rather than folded into the one above --
% and is safe to call unconditionally, even on a first compile with no
% side file yet (degrades to the original uncorrected placement, with no
% correction applied -- see marginalreader-margin.lua's "Pre-pass
% status" note, at the top of that file, for the full two-pass
% rationale).
%
% NOTE for future maintainers: keep Lua source passed through
% \directlua{...} free of `--` comments. \directlua's argument is
% scanned under ordinary TeX catcodes, which collapse end-of-line
% characters to spaces -- so a Lua `--` line comment does not stop at
% the next visual line the way it would in a real .lua file, it runs
% to the true end of the WHOLE \directlua argument, silently eating
% any real code after it. Confirmed empirically (a `pre_output_filter`
% callback registration silently failed to compile this way, with a
% misleading-looking "'end' expected near <eof>" Lua error) while
% implementing this task -- see the development notes. `--[[ ... ]]` block
% comments do not have this problem (they are delimited, not
% newline-terminated), but the simplest fix, used throughout this
% class, is what these two paragraphs are doing right now: put the
% explanation in ordinary `%`-comments OUTSIDE the \directlua argument,
% and keep the argument itself a short, comment-free function call.
\directlua{marginalreader_margin.load_positions()}%
% Rule style/colour/dash configuration, resolved into plain Lua-side
% numbers. Colour needs xcolor's own machinery to reduce an arbitrary
% colour NAME (\mr@rulecolor -- "black" by default, but any colour
% xcolor knows about, in any of its native models) to plain r,g,b
% floats: \extractcolorspec gets the colour's native model+spec,
% \convertcolorspec converts that to the "rgb" model specifically
% (decimal 0-1 floats -- exactly what the PDF `RG` operator wants, see
% M.draw_rules). Done here, TeX-side, with xcolor's own tools, rather
% than reimplementing colour-model conversion in Lua.
%
% The resulting "r,g,b" string, and the raw \mr@ruledash spec text,
% are passed to Lua as plain strings and parsed INSIDE
% marginalreader-margin.lua (M.configure_rules), not inline in this
% \directlua block: Lua's pattern-matching syntax uses `%`
% (`%d`/`%a`/`%s`/`%.`/...), and \directlua's argument is macro-
% expanded under ordinary TeX catcodes, where `%` starts a comment --
% confirmed empirically that inline `%d`/`%s`-using Lua written
% directly in a \directlua{...} argument gets silently truncated
% mid-line by TeX's comment scanning before Lua ever sees it. Passing
% the plain, `%`-free spec strings across and parsing them on the Lua
% side (ordinary Lua source, read from a file, unaffected by TeX
% catcodes) avoids that trap entirely.
\extractcolorspec{\mr@rulecolor}\mr@rc@tmp
\expandafter\convertcolorspec\mr@rc@tmp{rgb}\mr@rc@rgb
\directlua{
marginalreader_margin.configure_rules({
top_rule_style = "\mr@toprule",
col_rule_style = "\mr@columnrule",
rule_width = tex.sp("\mr@rulewidth"),
rule_color_spec = "\mr@rc@rgb",
rule_dash_spec = "\mr@ruledash",
})
}%
}
% Rerun warning: tell the user when this compile's margin
% layout is not (yet) fully corrected. The two-pass mechanism set up in
% \AtBeginDocument above only applies corrections computed from the
% PREVIOUS compile's recorded data -- a single compile (or any compile
% after the document itself changed) therefore ships uncorrected margin
% placement with nothing in the log to say so. \ifmr@positionschanged is
% set by marginalreader-margin.lua's M.get_positions_changed (see that
% function, and the "Rerun-needed detection" section above it, for the
% comparison itself). Mirrors the LaTeX kernel's own "Label(s) may have
% changed. Rerun to get cross-references right." idiom: same purpose
% (tell the user a rerun will change the output), same shape (compare
% this run's recorded data against what was read back at the start of
% the run).
%
% Deliberately NOT \AtEndDocument (despite that being the obvious first
% choice, and how this was drafted initially): the kernel's own
% \enddocument runs \@enddocumenthook -- i.e. every \AtEndDocument block
% -- BEFORE its own final \clearpage (confirmed directly in this
% installation's latex.ltx: \@kernel@after@enddocument ... \clearpage
% ... \UseOneTimeHook{enddocument/afterlastpage} ... -- the exact reason
% packages like lastpage.sty force their OWN \clearpage inside
% \AtEndDocument, to learn the true last page number early). A first
% draft of this fix used \AtEndDocument directly and, on testing,
% silently never warned on any single-page document whose margin items
% all sit on that one (and therefore also LAST, not-yet-shipped) page --
% confirmed against tests/test-gloss.tex and tests/test-margin-basic.tex,
% both single-page, both exercising \gloss -- a real gap, not a
% theoretical one, and a common case for a short document or any chapter
% that does not happen to force a page break right at the end.
% `enddocument/afterlastpage` is the kernel's own, purpose-built hook for
% exactly this: it fires once, AFTER the true final \clearpage (see the
% kernel excerpt above), so every item's anchor -- including any on the
% last page -- has already been recorded by the time this code runs.
% Available unconditionally on this kernel (\NewHook{enddocument/
% afterlastpage} in latex.ltx) -- the same hook-management mechanism this
% class already relies on for \AddToHook{shipout/background} below, so
% this introduces no new version assumption.
\newif\ifmr@positionschanged
\AddToHook{enddocument/afterlastpage}{%
\directlua{marginalreader_margin.get_positions_changed()}%
\ifmr@positionschanged
\ClassWarningNoLine{marginalreader}{Margin positions have changed. Rerun lualatex to get correct margin layout.}%
\fi
}%
% Flush pending margin overflow onto real reader pages. The layout
% pre-pass may have decided (from the previous run's data) that some
% margin items carry onto pages at or beyond the current one. That
% carry is delivered at shipout of the target page -- but the target
% page must (a) EXIST and (b) still be a READER page. Two observed
% failure modes motivate this command:
% * the document simply ends before the target page -- the carried
% items silently vanish (a crowded final page whose overflowing
% glosses, margin table and margin figure disappeared);
% * a matter switch reaches the target page first -- the carry then
% paints on top of a front/back-matter page with centred, rule-free
% geometry (observed: chapter 2's margin spill painted across the
% back-matter Notae page whenever page parity did not happen to
% insert a blank verso in between).
% Same job as marginfix's \dumpmargins: emit as many (otherwise empty)
% reader pages as the pending carry needs -- rules, running headers
% and all. Called at \backmatter (before the geometry switches) and on
% the `enddocument` hook, which the kernel fires BEFORE its own final
% \clearpage (see the note above).
% Absolute page counter: incremented once per \shipout, BEFORE the
% shipout hooks run, so during any given page's shipout -- background
% hook, rules, and the page's own \latelua whatsits alike -- it reads
% as that page's unique, document-wide number. The margin system keys
% ALL its per-page data (position records, blocked regions, carry,
% flush) on THIS counter, never on the page counter: \pagenumbering
% resets (front matter i, ii, iii... then main matter 1, 2, 3...)
% make c@page ambiguous, and that ambiguity was a live bug -- carry
% destined for main-matter page 3 was consumed when TITLE PAGE iii
% shipped, silently losing the carried items and leaving their
% ghost-reserved gap at the top of the real page's margin column.
\newcount\mr@abspage
\mr@abspage=0
\AddToHook{shipout/before}{\global\advance\mr@abspage\@ne}
\newcommand{\mr@flushmargins}{%
% (In the Lua below: the page currently under construction is one
% past the last shipped page, hence the +1.)
\directlua{
local M = marginalreader_margin
local cur = tex.count["mr@abspage"] + 1
local highest = -1
for pg, list in pairs(M.carry_in) do
if list and \string##list > 0 and pg >= cur and pg > highest then
highest = pg
end
end
if highest >= cur then
for i = cur, highest do
tex.print("\string\\null\string\\clearpage")
end
end
}%
}
\AddToHook{enddocument}{\mr@flushmargins}%
% Draw the top rule + column rule on every page. Registered on the
% LaTeX3 kernel's shipout/background hook (ltshipout; available since
% the 2021-06-01 LaTeX release, well predating this TeX Live), which
% fires once per \shipout -- i.e. once per physical page, regardless of
% how many \chapter/\clearpage/page breaks produced it -- rather than as
% a page-list Lua callback (see the engine note above
% marginalreader-margin.lua's M.draw_rules for why: the design draft's own
% pre_shipout_filter-based approach does not exist on this engine, and
% \pdfliteral does not exist under this LuaLaTeX format either).
%
% is_recto is evaluated HERE, at actual shipout time (mirroring
% \mr@marginitem's \if@twoside/\ifodd\value{page} convention exactly, so
% the column rule always lines up with the margin content it separates
% from the text) -- which is in fact MORE reliable than \mr@marginitem's
% own anchor-time check (a concern recorded in the development notes):
% by shipout time,
% page-breaking has already finished deciding this page's final content
% and page number, so there is no remaining risk of the check firing
% before an anchor's "actual" page is settled.
% also passes the current page number (\arabic{page}, evaluated
% at this same shipout-time point) so M.draw_rules can look up THIS
% page's blocked regions from M.blocked_by_page -- see
% marginalreader-margin.lua's M.draw_rules and the "Full-width elements"
% section, below, for the full rationale (blocked regions are always the
% PREVIOUS compile's data, never this run's own).
% Rules can be suspended for title/display pages (\rulesoff ...
% \ruleson). The flag is \global and consulted at SHIPOUT time, so the
% intended pattern brackets whole pages: \rulesoff, the page content,
% \clearpage (which ships the page while the flag is still off), then
% \ruleson. Display front matter (half title, title page, imprint) is
% the intended use.
\newif\ifmr@rules
\mr@rulestrue
\newcommand{\rulesoff}{\global\mr@rulesfalse}
\newcommand{\ruleson}{\global\mr@rulestrue}
\newcommand{\mr@draw@rules}{%
% Record every page's kind first, rules on or off: a rules-off page
% (part page, front/back matter) is a DISPLAY page, and the margin
% system must keep carried items off it -- M.build_overlay defers any
% carry that lands on one this run (see below), and the recorded set,
% written to the blocked-region side file as kind="display" markers,
% lets the NEXT run's pre-pass route carry past display pages up
% front (M.run_blocked_prepass / M.run_layout_prepass).
\directlua{
marginalreader_margin.record_page_display(\the\mr@abspage,
\ifmr@rules false\else true\fi)
}%
\ifmr@rules
\directlua{
marginalreader_margin.draw_rules(%
\if@twoside\ifodd\value{page}true\else false\fi\else true\fi,
\the\mr@abspage)
}%
\fi
}
\AddToHook{shipout/background}{\mr@draw@rules}
% Overlay painting at SHIPOUT time (carried-over margin items). This
% used to be spliced into the page list from the pre_output_filter
% callback (M.process_page), but that callback can fire on
% page-builder FALSE ALARMS -- observed concretely: \chapter's
% \clearpage invoking the output routine on a still-empty page, which
% consumed the (destructively read) carry data and discarded the
% painted overlay before any of the page's content existed, silently
% losing the carried margin items. shipout/background fires exactly
% once per page actually shipped, so consumption here can never lose
% content or paint it twice. M.build_overlay builds the positioned
% chain from the Lua-side boxes, deposits it in \mr@overlaybox, and
% sets \mr@overlayx/\mr@overlayy to the text block's top-left corner
% for this page's parity; the \put places the zero-extent box there
% (kernel picture coordinates: origin at the paper's top-left corner,
% y upward, so the y offset is negative). Runs AFTER \mr@draw@rules
% (hook order = addition order).
\newbox\mr@overlaybox
\newdimen\mr@overlayx
\newdimen\mr@overlayy
\AddToHook{shipout/background}{%
\directlua{
marginalreader_margin.build_overlay(
\the\mr@abspage,
\if@twoside\ifodd\value{page}true\else false\fi\else true\fi)
}%
\ifvoid\mr@overlaybox\else
\put(\mr@overlayx,\mr@overlayy){\copy\mr@overlaybox}%
\fi
}
% Internal counter for margin items, plus the scratch boxes used to
% build and place each one.
\newcount\mr@itemid
\mr@itemid=0
\newif\ifmr@recto
\newif\ifmr@hasprepassrecto
\newsavebox\mr@marginbox
\newbox\mr@marginadjbox