postext-epub 0.49.6 → 0.49.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/package/pack.d.ts.map +1 -1
  2. package/dist/package/pack.js +2 -1
  3. package/dist/package/pack.js.map +1 -1
  4. package/dist/reflowable/css/apparatus.d.ts +17 -0
  5. package/dist/reflowable/css/apparatus.d.ts.map +1 -0
  6. package/dist/reflowable/css/apparatus.js +127 -0
  7. package/dist/reflowable/css/apparatus.js.map +1 -0
  8. package/dist/reflowable/css/boxes.d.ts +20 -0
  9. package/dist/reflowable/css/boxes.d.ts.map +1 -0
  10. package/dist/reflowable/css/boxes.js +134 -0
  11. package/dist/reflowable/css/boxes.js.map +1 -0
  12. package/dist/reflowable/css/figures.d.ts +16 -0
  13. package/dist/reflowable/css/figures.d.ts.map +1 -0
  14. package/dist/reflowable/css/figures.js +174 -0
  15. package/dist/reflowable/css/figures.js.map +1 -0
  16. package/dist/reflowable/css/helpers.d.ts +32 -0
  17. package/dist/reflowable/css/helpers.d.ts.map +1 -0
  18. package/dist/reflowable/css/helpers.js +77 -0
  19. package/dist/reflowable/css/helpers.js.map +1 -0
  20. package/dist/reflowable/css/text.d.ts +19 -0
  21. package/dist/reflowable/css/text.d.ts.map +1 -0
  22. package/dist/reflowable/css/text.js +151 -0
  23. package/dist/reflowable/css/text.js.map +1 -0
  24. package/dist/reflowable/css/types.d.ts +46 -0
  25. package/dist/reflowable/css/types.d.ts.map +1 -0
  26. package/dist/reflowable/css/types.js +4 -0
  27. package/dist/reflowable/css/types.js.map +1 -0
  28. package/dist/reflowable/css/verse.d.ts +18 -0
  29. package/dist/reflowable/css/verse.d.ts.map +1 -0
  30. package/dist/reflowable/css/verse.js +126 -0
  31. package/dist/reflowable/css/verse.js.map +1 -0
  32. package/dist/reflowable/css.d.ts +4 -52
  33. package/dist/reflowable/css.d.ts.map +1 -1
  34. package/dist/reflowable/css.js +24 -736
  35. package/dist/reflowable/css.js.map +1 -1
  36. package/package.json +3 -3
@@ -3,80 +3,15 @@
3
3
  // override. Sizes are in ems of the body text (the reader's chosen size),
4
4
  // line heights are ratios, colours come from the resolved palette, and no
5
5
  // rule sets a width the screen would have to honour.
6
- import { dimensionToPx, featureFamilyParts, fontVariantNumeric, isCornerRadii, primaryFontFamily, resolvedCodeStyle, uniformRadius } from 'postext';
7
- import { cssString } from '../shared/assets.js';
8
- import { idOf, round } from './inline.js';
9
- /** CSS `line-break` for a Japanese kinsoku level (`cjk.lineBreak`, #417):
10
- * JLReq's strictest rules are CSS `strict` (no small kana, ー or 々 at a
11
- * line start), the general books' rules CSS `normal` (they may), the
12
- * newspapers' CSS `loose`. Chinese levels write none, as before. */
13
- const LINE_BREAK = { 'ja-very-strict': 'strict', 'ja-strict': 'normal', 'ja-loose': 'loose' };
14
- /** The declarations of the Japanese text of a book (#428): kinsoku and,
15
- * with burasagari (`cjk.hangingPunctuation`, on by default in Japan),
16
- * 、。,. hanging past the line end; and, in any book that asks for it,
17
- * lines broken only between phrases (`cjk.wordBreak: 'keep-all'`, #463).
18
- * None for other books. */
19
- function japaneseDecls(cjk) {
20
- const out = [];
21
- if (cjk.wordBreak === 'keep-all')
22
- out.push('word-break: keep-all');
23
- const lineBreak = LINE_BREAK[cjk.lineBreak];
24
- if (lineBreak)
25
- out.push(`line-break: ${lineBreak}`, `-epub-line-break: ${lineBreak}`, `-webkit-line-break: ${lineBreak}`);
26
- if (cjk.region === 'japan' && cjk.hangingPunctuation !== 'none') {
27
- out.push(`hanging-punctuation: ${cjk.hangingPunctuation === 'force' ? 'force-end' : 'allow-end'}`);
28
- }
29
- return out;
30
- }
31
- /** The emphasis-mark rule of a class `dotsClasses` gives: a shape and fill
32
- * (`pt-dots-filled-sesame`), or a side (`pt-dots-over`: over horizontal
33
- * text and right of vertical text; `pt-dots-under`: under and left). */
34
- function dotsRule(cls) {
35
- const side = /^pt-dots-(over|under)$/.exec(cls);
36
- if (side) {
37
- const pos = side[1] === 'over' ? 'over right' : 'under left';
38
- return rule(`.pt-dots.${cls}`, [`text-emphasis-position: ${pos}`, `-webkit-text-emphasis-position: ${pos}`]);
39
- }
40
- const shape = /^pt-dots-(filled|open)-(dot|circle|sesame)$/.exec(cls);
41
- if (!shape)
42
- return '';
43
- const style = `${shape[1]} ${shape[2]}`;
44
- return rule(`.pt-dots.${cls}`, [`text-emphasis-style: ${style}`, `-epub-text-emphasis-style: ${style}`, `-webkit-text-emphasis-style: ${style}`]);
45
- }
46
- const SANS = /\b(sans|grotesk|grotesque|gothic|helvetica|arial|inter|roboto|lato|montserrat|open sans|source sans|fira sans|heiti|hei\b|黑)/i;
47
- const MONO = /\b(mono|code|courier|consol)/i;
48
- /** A family as a CSS value: the family quoted, then a generic fallback. A
49
- * feature family (#1237, `Lora@onum`) is its base family's. */
50
- export function familyStack(family) {
51
- const name = familyName(family);
52
- const generic = MONO.test(name) ? 'monospace' : SANS.test(name) ? 'sans-serif' : 'serif';
53
- return `${cssString(name)}, ${generic}`;
54
- }
55
- /** The family names the stylesheet asks for (to warn about missing
56
- * faces). */
57
- export function familyName(family) {
58
- return featureFamilyParts(primaryFontFamily(family).trim().replace(/^['"]|['"]$/g, '')).family;
59
- }
60
- /** Whether a configuration sets any style's figures (#1237): a family of
61
- * it is a feature family. */
62
- function setsFigures(config) {
63
- return /"[^"]*@(?:onum|lnum|pnum|tnum)(?:\+(?:onum|lnum|pnum|tnum))*"/.test(JSON.stringify(config));
64
- }
65
- const ALIGN = { left: 'start', right: 'end', center: 'center', justify: 'justify' };
66
- /** Beside every `letter-spacing` (#703): letter-spacing never changes the
67
- * glyphs. A reading system turns the common ligatures off where the
68
- * letter-spacing is not zero; asked for as features they stay, as in the
69
- * print layout, the fixed-layout pages and the PDF. */
70
- const TRACKED_LIGATURES = "font-feature-settings: 'liga' 1, 'clig' 1";
71
- /** A rule from its declarations (the falsy ones left out); none when it
72
- * declares nothing. */
73
- function rule(selector, decls) {
74
- const body = decls.filter((d) => typeof d === 'string' && d.length > 0);
75
- return body.length > 0 ? { selector, decls: body } : '';
76
- }
77
- function ruleText(r) {
78
- return `${r.selector} {\n${r.decls.map((d) => ` ${d};`).join('\n')}\n}\n`;
79
- }
6
+ import { dimensionToPx, fontVariantNumeric, featureFamilyParts, primaryFontFamily } from 'postext';
7
+ import { round } from './inline.js';
8
+ import { setsFigures, familyName, familyStack, ruleText } from './css/helpers.js';
9
+ import { createTextRules } from './css/text.js';
10
+ import { createFigureRules } from './css/figures.js';
11
+ import { createBoxRules } from './css/boxes.js';
12
+ import { createApparatusRules } from './css/apparatus.js';
13
+ import { createVerseRules } from './css/verse.js';
14
+ export { familyStack, familyName } from './css/helpers.js';
80
15
  export function bookStylesheet(config, fontFaces, options) {
81
16
  const dpi = config.page.dpi;
82
17
  const body = config.bodyText;
@@ -118,668 +53,21 @@ export function bookStylesheet(config, fontFaces, options) {
118
53
  out.push('@charset "UTF-8";\n');
119
54
  if (fontFaces)
120
55
  out.push(fontFaces);
121
- // --- body text -----------------------------------------------------------
122
- const hyphens = body.hyphenation.enabled;
123
- if (options.vertical) {
124
- out.push(rule('html', ['writing-mode: vertical-rl', '-epub-writing-mode: vertical-rl', '-webkit-writing-mode: vertical-rl']));
125
- }
126
- out.push(rule('body', [
127
- fam(body.fontFamily),
128
- lh(body.lineHeight, bodyPx),
129
- color(body.color),
130
- `text-align: ${ALIGN[body.textAlign] ?? 'start'}`,
131
- hyphens && 'hyphens: auto',
132
- hyphens && '-epub-hyphens: auto',
133
- hyphens && '-webkit-hyphens: auto',
134
- body.fontWeight !== 400 && `font-weight: ${body.fontWeight}`,
135
- // CSS names the two rules the other way round (#717): `orphans` is
136
- // the lines a paragraph keeps at the foot of a page (the engine's
137
- // `avoidWidows`), `widows` the lines at the head of the next (its
138
- // `avoidOrphans`). A rule that is off writes 1: the initial value of
139
- // both properties is 2, so leaving one out would keep the rule.
140
- `orphans: ${body.avoidWidows ? Math.max(1, body.widowMinLines) : 1}`,
141
- `widows: ${body.avoidOrphans ? Math.max(1, body.orphanMinLines) : 1}`,
142
- 'margin: 0',
143
- 'padding: 0 1em',
144
- ...japaneseDecls(config.cjk),
145
- ]));
146
- const indent = px(body.firstLineIndent);
147
- const ratio = round(dimensionToPx(body.lineHeight, dpi, bodyPx) / bodyPx);
148
- const spacing = spacingEm(body.paragraphSpacing, body.lineHeight);
149
- out.push(rule('p', [
150
- 'margin: 0',
151
- spacing === 0 && indent > 0 && `text-indent: ${round(indent / bodyPx)}em`,
152
- spacing > 0 && `margin-block-end: ${spacing}em`,
153
- ]));
154
- if (spacing === 0 && indent > 0) {
155
- // The first paragraph after a heading, a list, a figure… is not
156
- // indented (unless the book indents after headings).
157
- const after = ['ul', 'ol', 'blockquote', 'figure', 'div', 'aside', 'nav', 'section > p:first-child'];
158
- if (!body.indentAfterHeading)
159
- after.unshift('h1', 'h2', 'h3', 'h4', 'h5', 'h6');
160
- out.push(rule(after.map((s) => (s.includes(' ') ? s : `${s} + p`)).join(', '), ['text-indent: 0']));
161
- }
162
- out.push(rule('strong, b', [`font-weight: ${body.boldFontWeight}`, color(body.boldColor)]));
163
- out.push(rule('em, i', [color(body.italicColor)]));
164
- out.push(rule('a', ['color: inherit', 'text-decoration: underline']));
165
- out.push(rule('a.pt-ref', [
166
- color(body.referenceColor),
167
- 'text-decoration: none',
168
- body.referenceBold && `font-weight: ${body.boldFontWeight}`,
169
- body.referenceItalic && 'font-style: italic',
170
- ]));
171
- out.push(rule('a.pt-pageref', ['text-decoration: none']));
172
- out.push(rule('sup, sub', ['line-height: 0', 'font-size: 0.7em']));
173
- // --- headings ------------------------------------------------------------
174
- const headings = config.headings;
175
- /** The body's line pitch, px: a 行取り heading (`lineSpan`, #424) takes
176
- * whole numbers of it. */
177
- const bodyLinePx = body.lineHeight.unit === 'em' || body.lineHeight.unit === 'rem'
178
- ? body.lineHeight.value * bodyPx
179
- : px(body.lineHeight);
180
- for (const level of headings.levels) {
181
- if (level.level < 1 || level.level > 6)
182
- continue;
183
- const hPx = px(level.fontSize);
184
- // 行取り: the heading's line centred in N body lines, its margins the
185
- // rest of them (a reflowable book has no grid to keep).
186
- const lineSpanMargin = level.lineSpan !== undefined
187
- ? Math.max(0, (level.lineSpan * bodyLinePx - (level.lineHeight.unit === 'em' || level.lineHeight.unit === 'rem' ? level.lineHeight.value * hPx : px(level.lineHeight, hPx))) / 2)
188
- : undefined;
189
- out.push(rule(`h${level.level}`, [
190
- fam(level.fontFamily || headings.fontFamily),
191
- `font-size: ${round(hPx / bodyPx)}em`,
192
- lh(level.lineHeight, hPx),
193
- `font-weight: ${level.fontWeight}`,
194
- `font-style: ${level.italic ? 'italic' : 'normal'}`,
195
- color(level.color),
196
- `text-align: ${ALIGN[headings.textAlign] ?? 'start'}`,
197
- level.letterSpacing.value !== 0 && `letter-spacing: ${em(level.letterSpacing, hPx)}`,
198
- level.letterSpacing.value !== 0 && TRACKED_LIGATURES,
199
- level.textTransform === 'uppercase' && 'text-transform: uppercase',
200
- lineSpanMargin !== undefined
201
- ? `margin: ${round(lineSpanMargin / hPx)}em 0`
202
- : `margin: ${round(px(level.marginTop, hPx) / hPx)}em 0 ${round(px(level.marginBottom, hPx) / hPx)}em`,
203
- // 字下げ: the indent counts body characters.
204
- level.indent && px(level.indent) > 0 && `margin-inline-start: ${round(px(level.indent) / hPx)}em`,
205
- 'hyphens: manual',
206
- '-epub-hyphens: manual',
207
- 'page-break-after: avoid',
208
- 'break-after: avoid',
209
- ]));
210
- }
211
- for (const style of config.headingStyles) {
212
- const o = style.overrides;
213
- const sPx = o.fontSize ? px(o.fontSize) : undefined;
214
- out.push(rule(`.${idOf('hs-', style.id)}`, [
215
- o.fontFamily && fam(o.fontFamily),
216
- sPx !== undefined && `font-size: ${round(sPx / bodyPx)}em`,
217
- o.fontWeight !== undefined && `font-weight: ${o.fontWeight}`,
218
- o.italic !== undefined && `font-style: ${o.italic ? 'italic' : 'normal'}`,
219
- o.color && color(o.color),
220
- o.textTransform && `text-transform: ${o.textTransform === 'uppercase' ? 'uppercase' : 'none'}`,
221
- ]));
222
- }
223
- out.push(rule('.pt-part-title', ['text-align: center', 'margin-block-start: 3em']));
224
- out.push(rule('.pt-part-number', ['display: block', 'font-size: 0.6em', 'margin-block-end: 0.5em']));
225
- out.push(rule('.pt-hidden', [
226
- 'position: absolute',
227
- 'width: 1px',
228
- 'height: 1px',
229
- 'overflow: hidden',
230
- 'clip: rect(0 0 0 0)',
231
- 'white-space: nowrap',
232
- ]));
233
- // --- quotations, lists ---------------------------------------------------
234
- const bq = body.blockquote;
235
- out.push(rule('blockquote', [
236
- `margin: ${ratio / 2}em 0`,
237
- `margin-inline-start: ${em(bq.indent)}`,
238
- color(bq.color),
239
- bq.italic && 'font-style: italic',
240
- ]));
241
- if (bq.italic)
242
- out.push(rule('blockquote em', ['font-style: normal']));
243
- const ul = config.unorderedLists;
244
- const ol = config.orderedLists;
245
- out.push(rule('ul, ol', [
246
- 'list-style: none',
247
- `margin: ${em(ol.marginTop)} 0 ${em(ol.marginBottom)}`,
248
- 'padding-inline-start: 1.6em',
249
- 'text-indent: 0',
250
- ]));
251
- out.push(rule('li', [ol.itemSpacing.value > 0 && `margin-block-start: ${em(ol.itemSpacing)}`]));
252
- out.push(rule('li li', ['margin-block-start: 0']));
253
- out.push(rule('li > .pt-lbl:first-child', [
254
- 'display: inline-block',
255
- 'min-width: 1.6em',
256
- 'margin-inline-start: -1.6em',
257
- 'text-indent: 0',
258
- ]));
259
- out.push(rule('ul > li > .pt-lbl:first-child', [color(ul.color), ul.fontWeight !== body.fontWeight && `font-weight: ${ul.fontWeight}`]));
260
- out.push(rule('ol > li > .pt-lbl:first-child', [color(ol.color), ol.fontWeight !== body.fontWeight && `font-weight: ${ol.fontWeight}`]));
261
- out.push(rule('.pt-done', ['text-decoration: line-through']));
262
- // --- figures, tables, captions ------------------------------------------
263
- const cap = config.captionStyle;
264
- const capPx = px(cap.fontSize);
265
- out.push(rule('figure', ['margin: 1em 0', 'text-align: center', 'page-break-inside: avoid', 'break-inside: avoid']));
266
- out.push(rule('figure img', ['max-width: 100%', 'height: auto']));
267
- // The labels drawn over a picture (#955): a box as wide as the image,
268
- // the SVG laid over all of it.
269
- if (options.labelLayers) {
270
- out.push(rule('.pt-labelled', ['display: inline-block', 'position: relative', 'max-width: 100%', 'line-height: 0']));
271
- out.push(rule('.pt-label-layer', ['position: absolute', 'left: 0', 'top: 0', 'width: 100%', 'height: 100%', 'pointer-events: none']));
272
- }
273
- out.push(rule('figure video', ['width: 100%', 'height: auto', 'background-color: #000']));
274
- // Comic pages (#565): each panel's picture as wide as the text at most,
275
- // its lettering under it as plain lines.
276
- out.push(rule('.pt-comic', ['margin: 1em 0']));
277
- out.push(rule('.pt-comic-panel', ['margin: 1em 0 0.5em']));
278
- out.push(rule('.pt-comic-panel svg', ['display: block', 'max-width: 100%', 'height: auto', 'margin: 0 auto']));
279
- out.push(rule('.pt-comic p', ['text-indent: 0', 'margin: 0.25em 0']));
280
- out.push(rule('a.pt-video-link', ['display: block']));
281
- out.push(rule('figcaption, caption', [
282
- fam(cap.fontFamily),
283
- `font-size: ${round(capPx / bodyPx)}em`,
284
- color(cap.color),
285
- `text-align: ${ALIGN[cap.align] ?? 'start'}`,
286
- cap.descriptionItalic && 'font-style: italic',
287
- cap.backgroundEnabled && color(cap.background, 'background-color'),
288
- cap.backgroundEnabled && `padding: ${round(px(cap.padding, capPx) / capPx)}em`,
289
- `margin: ${round(px(cap.gap, capPx) / capPx)}em 0`,
290
- 'hyphens: manual',
291
- ]));
292
- out.push(rule('caption.pt-caption-below', ['caption-side: bottom']));
293
- out.push(rule('.pt-label', [
294
- color(cap.labelColor),
295
- cap.labelBold ? 'font-weight: bold' : undefined,
296
- `font-style: ${cap.labelItalic ? 'italic' : 'normal'}`,
297
- ]));
298
- const note = cap.note;
299
- out.push(rule('.pt-note', [
300
- `font-size: ${em(note.fontSize)}`,
301
- color(note.color),
302
- `font-style: ${note.italic ? 'italic' : 'normal'}`,
303
- `text-align: ${ALIGN[note.align] ?? 'start'}`,
304
- 'text-indent: 0',
305
- ]));
306
- out.push(rule('figcaption .pt-note', ['display: block', `margin-block-start: ${em(note.gap, px(cap.fontSize))}`]));
307
- out.push(rule('.pt-missing', ['border: 1px dashed currentColor', 'padding: 1em', 'font-style: italic']));
308
- const ts = config.tableStyle;
309
- /** A rule `widthPx` (at the page dpi) wide, in CSS px. */
310
- const line = (widthPx, c) => `${round(Math.max(0.5, widthPx * (96 / dpi)))}px solid ${c.hex}`;
311
- /** What a table style decides, by element: on `table` for the document's
312
- * style; on `table.pt-table-<id>` for a named one (#625), which then
313
- * also undoes what the document's style (`reset`) sets and it does not. */
314
- const tableStyleDecls = (st, reset) => {
315
- const cellPx = px(st.bodyFontSize);
316
- const booktabs = st.borders && st.rules === 'booktabs';
317
- const border = st.borders && st.rules !== 'none' && !booktabs && px(st.borderWidth) > 0
318
- ? line(px(st.borderWidth), st.borderColor)
319
- : undefined;
320
- const tableBorder = border !== undefined && (st.rules === 'outer' || st.rules === 'grid');
321
- // Booktabs: a heavy rule above and under the table, a light one under
322
- // the header and, under a head spanning columns above the last header
323
- // row, a span rule, trimmed at both ends by a background line (a CSS
324
- // border cannot be).
325
- const width = (d) => {
326
- const v = px(d, cellPx);
327
- return booktabs && v > 0 ? Math.max(0.25, v) : 0;
328
- };
329
- const heavy = width(st.heavyRuleWidth);
330
- const light = width(st.lightRuleWidth);
331
- const span = st.spanRules !== 'none' ? width(st.spanRuleWidth) : 0;
332
- const wasBooktabs = reset?.borders === true && reset.rules === 'booktabs';
333
- const extra = [];
334
- if (light > 0)
335
- extra.push(['thead', [`border-bottom: ${line(light, st.borderColor)}`]]);
336
- else if (wasBooktabs)
337
- extra.push(['thead', ['border-bottom: none']]);
338
- const spanSel = 'thead > tr:not(:last-child) > th[colspan]';
339
- if (span > 0 && st.spanRules === 'trimmed') {
340
- extra.push([spanSel, [
341
- `background-image: linear-gradient(${st.borderColor.hex}, ${st.borderColor.hex})`,
342
- 'background-repeat: no-repeat',
343
- 'background-position: center bottom',
344
- `background-size: calc(100% - ${round(2 * (px(st.spanRuleTrim, cellPx) / cellPx))}em) ${round(Math.max(0.5, span * (96 / dpi)))}px`,
345
- ]]);
346
- }
347
- else {
348
- if (span > 0)
349
- extra.push([spanSel, [`border-bottom: ${line(span, st.borderColor)}`]]);
350
- if (wasBooktabs && reset.spanRules === 'trimmed')
351
- extra.push([spanSel, ['background-image: none']]);
352
- }
353
- if (st.groupRules && light > 0) {
354
- extra.push(['tbody > tr.pt-group:not(:first-child) > *', [`border-top: ${line(light, st.borderColor)}`]]);
355
- }
356
- return {
357
- table: [
358
- reset && fam(st.bodyFontFamily),
359
- reset && `font-size: ${round(cellPx / bodyPx)}em`,
360
- reset && color(st.bodyColor),
361
- tableBorder ? `border: ${border}` : reset && 'border: none',
362
- heavy > 0 && `border-top: ${line(heavy, st.borderColor)}`,
363
- heavy > 0 && `border-bottom: ${line(heavy, st.borderColor)}`,
364
- ],
365
- cells: [
366
- reset && `padding: ${round(px(st.cellPadding, cellPx) / cellPx)}em`,
367
- border && st.rules === 'grid' ? `border: ${border}` : undefined,
368
- border && st.rules === 'horizontal' ? `border-block: ${border}` : undefined,
369
- reset && !(border && (st.rules === 'grid' || st.rules === 'horizontal')) && 'border: none',
370
- ],
371
- td: [st.bodyBackgroundEnabled ? color(st.bodyBackground, 'background-color') : reset && 'background-color: transparent'],
372
- alt: st.bodyAlternateBackgroundEnabled ? [color(st.bodyAlternateBackground, 'background-color')] : undefined,
373
- th: [
374
- fam(st.headerFontFamily),
375
- `font-size: ${round(px(st.headerFontSize) / cellPx)}em`,
376
- color(st.headerColor),
377
- `font-weight: ${st.headerBold ? 'bold' : 'normal'}`,
378
- st.headerItalic ? 'font-style: italic' : reset && 'font-style: normal',
379
- st.headerTextTransform === 'uppercase' ? 'text-transform: uppercase' : reset && 'text-transform: none',
380
- st.headerLetterSpacing.value !== 0 ? `letter-spacing: ${round(px(st.headerLetterSpacing, cellPx) / cellPx)}em` : reset && 'letter-spacing: normal',
381
- st.headerLetterSpacing.value !== 0 && TRACKED_LIGATURES,
382
- st.headerBackgroundEnabled ? color(st.headerBackground, 'background-color') : reset && 'background-color: transparent',
383
- ],
384
- extra,
385
- };
386
- };
387
- const bodyCellPx = px(ts.bodyFontSize);
388
- const base = tableStyleDecls(ts, undefined);
389
- out.push(rule('.pt-table', ['margin: 1em 0', 'overflow-x: auto']));
390
- out.push(rule('table', [
391
- 'border-collapse: collapse',
392
- 'margin: 0 auto',
393
- 'max-width: 100%',
394
- fam(ts.bodyFontFamily),
395
- `font-size: ${round(bodyCellPx / bodyPx)}em`,
396
- color(ts.bodyColor),
397
- 'text-indent: 0',
398
- 'hyphens: manual',
399
- ...base.table,
400
- ]));
401
- out.push(rule('th, td', [
402
- `padding: ${round(px(ts.cellPadding, bodyCellPx) / bodyCellPx)}em`,
403
- 'vertical-align: top',
404
- 'text-align: start',
405
- ...base.cells,
406
- ]));
407
- out.push(rule('td', base.td));
408
- if (base.alt)
409
- out.push(rule('td.pt-alt', base.alt));
410
- out.push(rule('th', base.th));
411
- for (const [sel, decls] of base.extra)
412
- out.push(rule(sel, decls));
413
- for (const st of config.tableStyles ?? []) {
414
- const table = `table.${idOf('pt-table-', st.id)}`;
415
- const sub = (sel) => sel.split(', ').map((x) => `${table} ${x}`).join(', ');
416
- const d = tableStyleDecls(st, ts);
417
- out.push(rule(table, d.table));
418
- out.push(rule(sub('th, td'), d.cells));
419
- out.push(rule(sub('td'), d.td));
420
- if (d.alt)
421
- out.push(rule(sub('td.pt-alt'), d.alt));
422
- out.push(rule(sub('th'), d.th));
423
- for (const [sel, decls] of d.extra)
424
- out.push(rule(sub(sel), decls));
425
- }
426
- out.push(rule('td img, th img', ['max-width: 100%', 'height: auto', 'display: block', 'margin: 0 auto']));
427
- // --- callouts ------------------------------------------------------------
428
- out.push(rule('aside.pt-callout', ['margin: 1em 0', 'padding: 0.75em 1em', 'page-break-inside: avoid']));
429
- out.push(rule('.pt-callout-title', ['font-weight: bold', 'text-indent: 0', 'margin-block-end: 0.4em']));
430
- // A composed page's frames (#862): blocks of its section, in Markdown
431
- // order; a frame set in a callout style is drawn as its box.
432
- if (options.composed) {
433
- out.push(rule('section.pt-composed', ['margin-block: 1em']));
434
- out.push(rule('div.pt-frame', ['margin: 0 0 1em']));
435
- // A frame set in a writing mode of its own (#960): its lines run along
436
- // the length written on the box (`height` for a vertical frame).
437
- out.push(rule('div.pt-frame-vertical', ['writing-mode: vertical-rl', '-epub-writing-mode: vertical-rl', '-webkit-writing-mode: vertical-rl']));
438
- out.push(rule('div.pt-frame-horizontal', ['writing-mode: horizontal-tb', '-epub-writing-mode: horizontal-tb', '-webkit-writing-mode: horizontal-tb']));
439
- out.push(rule('div.pt-frame.pt-callout', ['padding: 0.75em 1em', 'page-break-inside: avoid']));
440
- }
441
- const frameStyles = new Set(options.composed?.frameStyles ?? []);
442
- // The boxes an exercise is set in (#1244): its own frameless one, or the
443
- // box style its exercise style names. An exercise is a `div` group.
444
- const exerciseFrames = new Set((config.exerciseSettings?.styles ?? []).flatMap((st) => (st.box ? [st.box] : [])));
445
- for (const s of config.calloutStyles) {
446
- const cls = idOf('pt-callout-', s.id);
447
- // The box's selector, and the frames' set in its style (#862).
448
- const sels = frameStyles.has(s.id) ? [`aside.${cls}`, `div.pt-frame.${cls}`] : [`aside.${cls}`];
449
- if (exerciseFrames.has(s.id) || s.id.startsWith('__postext-ex'))
450
- sels.push(`div.pt-exercise.${cls}`);
451
- const sel = sels.join(', ');
452
- const under = (rest) => sels.map((x) => `${x} ${rest}`).join(', ');
453
- const bPx = px(s.body.fontSize);
454
- const pad = s.padding;
455
- const stripe = s.stripe.enabled && px(s.stripe.width) > 0 ? `${round(px(s.stripe.width) * (96 / dpi))}px solid ${s.stripe.color.hex}` : undefined;
456
- const side = s.stripe.side === 'top' ? 'border-block-start' : s.stripe.side === 'right' ? 'border-inline-end' : 'border-inline-start';
457
- // A box's sides are the body flow's (#371): in a right-to-left book,
458
- // whose flow is mirrored, its left padding is on the page's right.
459
- const [padRight, padLeft] = rtl ? [pad.left, pad.right] : [pad.right, pad.left];
460
- // Corners of their own and a border per side (#1059); a joined tab
461
- // reflows as a plain tab.
462
- const radius = isCornerRadii(s.borderRadius)
463
- ? (() => {
464
- const r = s.borderRadius;
465
- const one = (d) => (d ? em(d) : '0');
466
- // The corners of the body flow's sides, as the padding's.
467
- const [tl, tr, br, bl] = rtl ? [r.topRight, r.topLeft, r.bottomLeft, r.bottomRight] : [r.topLeft, r.topRight, r.bottomRight, r.bottomLeft];
468
- return [tl, tr, br, bl].some((d) => d && px(d) > 0)
469
- ? `border-radius: ${one(tl)} ${one(tr)} ${one(br)} ${one(bl)}`
470
- : undefined;
471
- })()
472
- : px(uniformRadius(s.borderRadius)) > 0 && `border-radius: ${em(uniformRadius(s.borderRadius))}`;
473
- const sideBorders = s.border.sides
474
- ? ['top', 'right', 'bottom', 'left'].map((k) => {
475
- const b = s.border.sides[rtl && k === 'left' ? 'right' : rtl && k === 'right' ? 'left' : k];
476
- return b.enabled && px(b.width) > 0 ? `border-${k}: ${round(px(b.width) * (96 / dpi))}px solid ${b.color.hex}` : `border-${k}: none`;
477
- })
478
- : [];
479
- out.push(rule(sel, [
480
- s.backgroundEnabled && color(s.background, 'background-color'),
481
- !s.border.sides && s.border.enabled && px(s.border.width) > 0 && `border: ${round(px(s.border.width) * (96 / dpi))}px solid ${s.border.color.hex}`,
482
- ...sideBorders,
483
- stripe && `${side}: ${stripe}`,
484
- radius,
485
- `padding: ${em(pad.top)} ${em(padRight)} ${em(pad.bottom)} ${em(padLeft)}`,
486
- `margin: ${em(s.marginTop)} 0 ${em(s.marginBottom)}`,
487
- fam(s.body.fontFamily),
488
- `font-size: ${round(bPx / bodyPx)}em`,
489
- color(s.body.color),
490
- s.body.italic && 'font-style: italic',
491
- s.body.smallCaps && 'font-variant: small-caps',
492
- `text-align: ${ALIGN[s.body.textAlign] ?? 'start'}`,
493
- ]));
494
- out.push(rule(under('p'), [
495
- `text-indent: ${round(px(s.body.firstLineIndent, bPx) / bPx)}em`,
496
- spacingEm(s.body.paragraphSpacing, s.body.lineHeight, bPx) > 0 && `margin-block-end: ${spacingEm(s.body.paragraphSpacing, s.body.lineHeight, bPx)}em`,
497
- lh(s.body.lineHeight, bPx),
498
- ]));
499
- const t = s.titleStyle;
500
- const tPx = px(t.fontSize);
501
- out.push(rule(under('> .pt-callout-title'), [
502
- fam(t.fontFamily),
503
- `font-size: ${round(tPx / bPx)}em`,
504
- `font-weight: ${t.fontWeight}`,
505
- `font-style: ${t.italic ? 'italic' : 'normal'}`,
506
- color(t.color),
507
- t.textTransform === 'uppercase' && 'text-transform: uppercase',
508
- t.letterSpacing.value !== 0 && `letter-spacing: ${round(px(t.letterSpacing, tPx) / tPx)}em`,
509
- t.letterSpacing.value !== 0 && TRACKED_LIGATURES,
510
- `margin-block-end: ${round(px(t.gap, tPx) / tPx)}em`,
511
- 'text-indent: 0',
512
- ]));
513
- out.push(rule(under('li > .pt-lbl:first-child'), [color(s.lists.color)]));
514
- }
515
- // --- paragraph styles ----------------------------------------------------
516
- for (const s of config.paragraphStyles) {
517
- const sPx = px(s.fontSize);
518
- const cls = idOf('ps-', s.id);
519
- out.push(rule(`p.${cls}, div.pt-verse.${cls}, div.pt-stanza.${cls}`, [
520
- fam(s.fontFamily),
521
- `font-size: ${round(sPx / bodyPx)}em`,
522
- lh(s.lineHeight, sPx),
523
- color(s.color),
524
- `text-align: ${ALIGN[s.textAlign] ?? 'start'}`,
525
- s.fontWeight !== body.fontWeight && `font-weight: ${s.fontWeight}`,
526
- s.italic && 'font-style: italic',
527
- s.smallCaps && 'font-variant: small-caps',
528
- !s.hyphenation && 'hyphens: manual',
529
- s.textTransform === 'uppercase' && 'text-transform: uppercase',
530
- // The style's own tracking (#907), in its own ems.
531
- s.letterSpacing && s.letterSpacing.value !== 0 && `letter-spacing: ${round(px(s.letterSpacing, sPx) / sPx)}em`,
532
- s.letterSpacing && s.letterSpacing.value !== 0 && TRACKED_LIGATURES,
533
- // The style's own line breaking between CJK characters (#463).
534
- s.wordBreak && `word-break: ${s.wordBreak}`,
535
- px(s.indent, sPx) > 0 && `margin-inline-start: ${round(px(s.indent, sPx) / sPx)}em`,
536
- // 地からN字上げ (#424).
537
- s.endIndent && px(s.endIndent, sPx) > 0 && `margin-inline-end: ${round(px(s.endIndent, sPx) / sPx)}em`,
538
- `text-indent: ${round(px(s.firstLineIndent, sPx) / sPx)}em`,
539
- px(s.marginTop, sPx) > 0 && `margin-block-start: ${round(px(s.marginTop, sPx) / sPx)}em`,
540
- px(s.marginBottom, sPx) > 0 && `margin-block-end: ${round(px(s.marginBottom, sPx) / sPx)}em`,
541
- ]));
542
- // A hanging indent: the paragraph's turnovers padded in, its first line
543
- // taken back to its own indent (the style's, when it sets one, #620).
544
- // A poem's lines hang on their own (`.pt-verse-line`).
545
- const hang = px(s.hangingIndent, sPx);
546
- if (hang > 0) {
547
- const first = s.ownFirstLineIndent ? px(s.firstLineIndent, sPx) : 0;
548
- out.push(rule(`p.${cls}`, [`padding-inline-start: ${round(hang / sPx)}em`, `text-indent: ${round((first - hang) / sPx)}em`]));
549
- }
550
- }
551
- // --- notes, contents, bibliography, index -------------------------------
552
- const fn = config.footnotes;
553
- const fnPx = px(fn.fontSize);
554
- out.push(rule('.pt-footnotes', ['margin-block-start: 2em', 'border-block-start: 1px solid currentColor', 'padding-block-start: 0.5em']));
555
- out.push(rule('aside.pt-footnote', [
556
- `font-size: ${round(fnPx / bodyPx)}em`,
557
- lh(fn.lineHeight, fnPx),
558
- color(fn.color),
559
- fn.textAlign && `text-align: ${ALIGN[fn.textAlign] ?? 'start'}`,
560
- `margin-block-end: ${round(px(fn.spaceBetween, fnPx) / fnPx)}em`,
561
- ]));
562
- out.push(rule('aside.pt-footnote p', ['text-indent: 0']));
563
- out.push(rule('a.pt-noteref', ['text-decoration: none', color(body.referenceColor)]));
564
- out.push(rule('a[role="doc-backlink"]', ['text-decoration: none']));
565
- out.push(rule('nav.pt-toc ol', ['list-style: none', 'padding-inline-start: 0']));
566
- out.push(rule('nav.pt-toc li', ['margin-block-start: 0.3em']));
567
- out.push(rule('nav.pt-toc a', ['text-decoration: none', 'color: inherit']));
568
- out.push(rule('nav.pt-toc li > .pt-lbl:first-child', ['margin-inline-start: 0', 'min-width: 2em']));
569
- out.push(rule('.pt-toc-part', ['font-weight: bold', 'margin-block-start: 0.8em']));
570
- out.push(rule('.pt-toc-subtitle', ['font-style: italic', 'font-size: 0.9em']));
571
- out.push(rule('p.pt-bib', ['padding-inline-start: 2em', 'text-indent: -2em', 'margin-block-end: 0.3em']));
572
- out.push(rule('p.pt-index-group', ['font-weight: bold', 'text-indent: 0', 'margin-block-start: 1em', 'page-break-after: avoid', 'break-after: avoid']));
573
- // Index entries hang their turnover lines, and a sub-entry sits one
574
- // indent in per level, both in the index's own em.
575
- const ix = config.index;
576
- const ixPx = px(ix.fontSize);
577
- const turnover = round(px(ix.turnoverIndent, ixPx) / ixPx);
578
- const step = round(px(ix.indent, ixPx) / ixPx);
579
- out.push(rule('p.pt-index-entry', [
580
- ixPx !== bodyPx && `font-size: ${round(ixPx / bodyPx)}em`,
581
- `padding-inline-start: ${turnover}em`,
582
- `text-indent: -${turnover}em`,
583
- 'text-align: start',
584
- 'hyphens: manual',
585
- ]));
586
- for (const level of [1, 2, 3]) {
587
- out.push(rule(`p.pt-index-l${level}`, [`margin-inline-start: ${round(step * level)}em`]));
588
- }
589
- // --- maths, chips, swatches, Chinese marks ------------------------------
590
- out.push(rule('.pt-math-display', ['text-align: center', 'margin: 0.5em 0', 'text-indent: 0', 'overflow-x: auto']));
591
- // Music scores (#1172): systems as wide as the column, a little air
592
- // between two.
593
- out.push(rule('.pt-score', ['margin: 0.75em 0', 'text-indent: 0']));
594
- out.push(rule('.pt-score-system', ['display: block', 'width: 100%', 'height: auto', 'margin: 0 0 0.4em']));
595
- out.push(rule('.pt-math svg', ['display: inline']));
596
- out.push(rule('.pt-swatch', ['display: inline-block', 'width: 0.8em', 'height: 0.8em', 'border: 1px solid currentColor', 'vertical-align: baseline']));
597
- out.push(rule('.pt-chip', ['display: inline-block', 'padding: 0 0.3em', 'border-radius: 0.2em', 'text-indent: 0', 'line-height: 1.2']));
598
- for (const c of config.chipStyles) {
599
- const cPx = c.fontSize ? px(c.fontSize) : bodyPx;
600
- out.push(rule(`.${idOf('pt-chip-', c.id)}`, [
601
- c.backgroundEnabled && color(c.background, 'background-color'),
602
- // A blank to write on (#1227) is underlined, not boxed.
603
- px(c.borderWidth) > 0 && `${c.underline ? 'border-bottom' : 'border'}: ${round(px(c.borderWidth) * (96 / dpi))}px ${c.underline ?? 'solid'} ${c.borderColor.hex}`,
604
- px(c.borderRadius) > 0 && `border-radius: ${round(px(c.borderRadius, cPx) / cPx)}em`,
605
- `padding: ${round(px(c.paddingY, cPx) / cPx)}em ${round(px(c.paddingX, cPx) / cPx)}em`,
606
- c.fontFamily && fam(c.fontFamily),
607
- c.fontSize && `font-size: ${round(cPx / bodyPx)}em`,
608
- c.color && color(c.color),
609
- c.bold && 'font-weight: bold',
610
- c.italic && 'font-style: italic',
611
- ]));
612
- }
613
- // Exercises (#1231): a blank holds its input, a drawn box its picture
614
- // (and, the script running, the controls laid over it); the answer key
615
- // for a reader with no script follows the exercise.
616
- out.push(rule('.pt-ex-slot', ['position: relative']));
617
- out.push(rule('.pt-ex-blank', ['height: 1.2em', 'vertical-align: -0.25em']));
618
- out.push(rule('.pt-ex-blank > input', ['box-sizing: border-box', 'width: 100%', 'height: 100%', 'margin: 0', 'padding: 0', 'border: 0', 'background: transparent', 'color: inherit', 'font: inherit', 'text-align: center']));
619
- out.push(rule('.pt-ex-drawn', ['position: relative', 'margin: 0.4em 0', 'text-indent: 0', 'max-width: 100%']));
620
- out.push(rule('.pt-ex-drawn > svg', ['display: block', 'overflow: visible', 'width: 100%', 'height: auto']));
621
- out.push(rule('.pt-ex-static', ['margin: 0.6em 0 0', 'font-size: 0.9em', 'text-indent: 0']));
622
- out.push(rule('.pt-ex-static > summary', ['cursor: pointer']));
623
- out.push(rule('.pt-sc', ['font-variant: small-caps']));
624
- out.push(rule('.pt-dots', ['font-style: normal', 'text-emphasis: filled dot', '-epub-text-emphasis-style: filled dot', '-webkit-text-emphasis-style: filled dot', 'text-emphasis-position: under right', '-webkit-text-emphasis-position: under right']));
625
- // Other shapes and sides (the Japanese sesame over the text, #428),
626
- // sorted so the stylesheet is the same whatever order they were read in.
627
- for (const cls of [...(options.dots ?? [])].sort())
628
- out.push(dotsRule(cls));
629
- out.push(rule('.pt-proper', ['text-decoration: underline']));
630
- out.push(rule('.pt-book', ['text-decoration: underline wavy']));
631
- // Side lines (傍線): under horizontal text and left of vertical text, or
632
- // over and right of it; one unbroken line, descenders and all.
633
- const underSide = ['text-underline-position: under left', '-epub-text-underline-position: under left', '-webkit-text-underline-position: under left'];
634
- out.push(rule('.pt-side', ['text-decoration-line: underline', 'text-decoration-skip-ink: none', ...underSide]));
635
- out.push(rule('.pt-side.pt-side-over', ['text-decoration-line: overline']));
636
- for (const style of ['double', 'wavy', 'dotted'])
637
- out.push(rule(`.pt-side.pt-side-${style}`, [`text-decoration-style: ${style}`]));
638
- out.push(rule('.pt-tcy', ['text-combine-upright: all', '-epub-text-combine: horizontal', '-webkit-text-combine: horizontal']));
639
- out.push(rule('.pt-upright', ['text-orientation: upright', '-epub-text-orientation: upright', '-webkit-text-orientation: upright']));
640
- out.push(rule('.pt-sideways', ['text-orientation: sideways', '-epub-text-orientation: sideways', '-webkit-text-orientation: sideways']));
641
- out.push(rule('.pt-warichu', ['font-size: 0.6em']));
642
- out.push(rule('ruby.pt-ruby-under', ['ruby-position: under', '-epub-ruby-position: under', '-webkit-ruby-position: after']));
643
- out.push(rule('ruby.pt-ruby-right', ['ruby-position: inter-character']));
644
- // Kanbun marks (訓点): half size, the 送り仮名 raised (right of vertical
645
- // text) and the 返り点 lowered (left of it), after their character.
646
- out.push(rule('.pt-okuri, .pt-kaeri', ['font-size: 0.5em', 'line-height: 0']));
647
- out.push(rule('.pt-okuri', ['vertical-align: super']));
648
- out.push(rule('.pt-kaeri', ['vertical-align: sub']));
649
- out.push(rule('.pt-marker', ['height: 0']));
650
- // Japanese note markers (JLReq §4.2.3), only in a book that sets them.
651
- // In the line gap: a box of no advance, its text running back from it
652
- // (right to left, inline-start being the end), its baseline raised past
653
- // the text's em box (0.88 em over its baseline) without growing the
654
- // line. Right of a vertical line: reduced, its box against
655
- // the line's right side.
656
- const marker = config.footnotes.markerPosition;
657
- if (marker === 'side' || marker === 'right') {
658
- const size = config.footnotes.markerSize;
659
- const em = size.unit === 'em' || size.unit === 'rem' ? size.value : px(size) / bodyPx;
660
- if (marker === 'side') {
661
- out.push(rule('.pt-note-side', ['display: inline-block', 'inline-size: 0', 'direction: rtl', 'white-space: nowrap', 'line-height: 0', `font-size: ${round(em)}em`, `vertical-align: ${round(0.88 / em + 0.12)}em`]));
662
- out.push(rule('.pt-note-side > span', ['direction: ltr', 'unicode-bidi: isolate']));
663
- }
664
- else {
665
- out.push(rule('.pt-note-right', [`font-size: ${round(em)}em`, 'vertical-align: text-top', 'line-height: 0']));
666
- }
667
- }
668
- // --- classical verse -----------------------------------------------------
669
- // A bayt is a row of two equal hemistich columns (and the ornament's,
670
- // empty without one): the ṣadr from the start side, the ʿajuz flush
671
- // with the end side, so the rhymes stand in one column as in print. On
672
- // a narrow screen the two halves stagger, the ʿajuz on its own line
673
- // flush with the end, as the print does when they do not fit.
674
- // A stanza set line by line (#620): its lines of verse, a blank line
675
- // between stanzas, never hyphenated; each line's hang is on its own
676
- // element.
677
- if (options.stanzas) {
678
- out.push(rule('.pt-stanza', ['margin: 0 0 1em', 'text-indent: 0', 'text-align: start', 'hyphens: manual']));
679
- out.push(rule('.pt-verse-line', ['display: block']));
680
- }
681
- // Line numbers (#621): a poem's every Nth line carries its number in a
682
- // margin on the start side of the stanza, out of the reading order.
683
- if (options.stanzas && options.lineNumbers) {
684
- out.push(rule('.pt-stanza', ['padding-left: 3em']));
685
- out.push(rule('.pt-verse-line', ['position: relative']));
686
- out.push(rule('.pt-line-number', [
687
- 'position: absolute', 'right: 100%', 'margin-right: 0.8em', 'text-indent: 0', 'font-size: 0.8em',
688
- 'white-space: nowrap', '-webkit-user-select: none', 'user-select: none',
689
- ]));
690
- out.push(rule('[dir="rtl"] .pt-stanza, .pt-stanza[dir="rtl"]', ['padding-left: 0', 'padding-right: 3em']));
691
- out.push(rule('[dir="rtl"] .pt-line-number, .pt-stanza[dir="rtl"] .pt-line-number', ['right: auto', 'left: 100%', 'margin-right: 0', 'margin-left: 0.8em']));
692
- }
693
- // Tab stops (#622): a line holding tabs is a row of its parts; a filler
694
- // (with its leader as a border on the baseline) pushes the part after it
695
- // to the row's end, a start stop's part keeps its printed width.
696
- if (options.tabs) {
697
- out.push(rule('.pt-tab-row', ['display: flex', 'align-items: baseline', 'text-indent: 0']));
698
- out.push(rule('.pt-tab-part', ['flex: 0 1 auto']));
699
- out.push(rule('.pt-tab-rest', ['flex: 1 1 0']));
700
- out.push(rule('.pt-tab-fill', ['flex: 1 1 auto', 'min-width: 1em', 'margin: 0 0.5em']));
701
- out.push(rule('.pt-tab-gap', ['flex: 0 0 0.5em']));
702
- out.push(rule('.pt-leader-dots', ['border-bottom: 0.12em dotted currentColor']));
703
- out.push(rule('.pt-leader-rule', ['border-bottom: 0.06em solid currentColor']));
704
- }
705
- // Code listings (#624): the box of `codeStyle` around `<pre><code>`, the
706
- // lines as written in the code face, turned over by the reading system
707
- // when the screen is narrow (`pre-wrap`); numbers and wrap left out of a
708
- // selection; the highlighted lines tinted.
709
- if (options.code) {
710
- const cs = resolvedCodeStyle(config);
711
- const codePx = dimensionToPx(cs.fontSize, dpi, bodyPx);
712
- const bodyLead = body.lineHeight.unit === 'em' || body.lineHeight.unit === 'rem' ? body.lineHeight.value * bodyPx : px(body.lineHeight);
713
- const codeLead = cs.lineHeight ? lh(cs.lineHeight, codePx) : `line-height: ${round(bodyLead / codePx)}`;
714
- const len = (d) => em(d, codePx);
715
- out.push(rule('.pt-code-box', [
716
- 'display: block',
717
- `margin: ${em(cs.marginTop, codePx)} 0 ${em(cs.marginBottom, codePx)}`,
718
- `font-size: ${round(codePx / bodyPx)}em`,
719
- cs.backgroundEnabled && color(cs.background, 'background-color'),
720
- cs.border.enabled && `border: ${round(px(cs.border.width) / codePx)}em solid ${cs.border.color.hex}`,
721
- px(cs.borderRadius, codePx) > 0 && `border-radius: ${len(cs.borderRadius)}`,
722
- `padding: ${len(cs.padding.top)} ${len(cs.padding.right)} ${len(cs.padding.bottom)} ${len(cs.padding.left)}`,
723
- 'text-indent: 0',
724
- ]));
725
- out.push(rule('.pt-code-title', [fam(cs.titleStyle?.fontFamily ?? cs.fontFamily), 'font-weight: bold', 'margin: 0 0 0.4em', 'text-indent: 0']));
726
- out.push(rule('pre.pt-code', [
727
- fam(cs.fontFamily), 'font-size: 1em', codeLead, 'margin: 0', color(cs.color),
728
- 'white-space: pre-wrap', 'overflow-wrap: anywhere', `tab-size: ${cs.tabSize}`, `-moz-tab-size: ${cs.tabSize}`,
729
- 'text-align: left', 'hyphens: none',
730
- ]));
731
- out.push(rule('pre.pt-code code', ['font: inherit', 'background: none']));
732
- out.push(rule('.pt-code-num', [color(cs.lineNumberColor), 'margin-right: 1em', '-webkit-user-select: none', 'user-select: none']));
733
- out.push(rule('mark.pt-code-hl', [color(cs.highlightBackground, 'background-color'), 'color: inherit']));
734
- }
735
- // Drop caps (#623): CSS initial letters, `lines` tall, sunk `sink`
736
- // lines; a reading system without `initial-letter` floats the letter,
737
- // about as tall, on the start side.
738
- if (options.dropCaps && options.dropCaps.length > 0) {
739
- out.push(rule('.pt-has-dropcap', ['text-indent: 0']));
740
- out.push(rule('.pt-dropcap', ['float: left', 'line-height: 0.85', 'margin: 0.05em 0.08em 0 0', 'text-indent: 0']));
741
- out.push(rule('[dir="rtl"] .pt-dropcap, p[dir="rtl"] .pt-dropcap', ['float: right', 'margin: 0.05em 0 0 0.08em']));
742
- for (const shape of options.dropCaps) {
743
- const [lines, sink] = shape.split('-').map(Number);
744
- out.push(rule(`.pt-dropcap-${shape}`, [`font-size: ${round(lines * 1.25)}em`]));
745
- out.push(`@supports (initial-letter: 1) or (-webkit-initial-letter: 1) {\n .pt-dropcap-${shape} { float: none; margin: 0 0.08em 0 0; font-size: 1em; line-height: inherit; -webkit-initial-letter: ${lines} ${sink}; initial-letter: ${lines} ${sink}; }\n}\n`);
746
- }
747
- }
748
- // Text wrap (#627): a figure or a box floated to its side; what cannot
749
- // run beside it in print (headings, other figures, tables, boxes,
750
- // formulas, poems) clears it.
751
- if (options.wraps) {
752
- out.push(rule('.pt-wrap', ['margin-top: 0', 'margin-bottom: 1em']));
753
- out.push(rule('.pt-wrap-left', ['float: left', 'clear: left']));
754
- out.push(rule('.pt-wrap-right', ['float: right', 'clear: right']));
755
- out.push(rule('.pt-wrap img', ['max-width: 100%', 'height: auto']));
756
- out.push(rule('h1, h2, h3, h4, h5, h6, figure:not(.pt-wrap), table, aside:not(.pt-wrap), .pt-math-display, .pt-stanza, .pt-verse', ['clear: both']));
757
- }
758
- if (options.verse) {
759
- // The poem as wide as its widest bayt, centred: its hemistichs share
760
- // one width, as in print, instead of drifting to the screen's edges.
761
- out.push(rule('.pt-verse', ['width: fit-content', 'max-width: 100%', 'margin: 0.5em auto', 'text-indent: 0']));
762
- out.push(rule('.pt-bayt', [
763
- 'display: grid',
764
- 'grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr)',
765
- 'column-gap: 1em',
766
- 'margin: 0',
767
- 'text-indent: 0',
768
- 'text-align: start',
769
- 'hyphens: manual',
770
- 'page-break-inside: avoid',
771
- 'break-inside: avoid',
772
- ]));
773
- out.push(rule('.pt-sadr', ['grid-column: 1', 'text-align: start']));
774
- out.push(rule('.pt-verse-ornament', ['grid-column: 2', 'text-align: center']));
775
- out.push(rule('.pt-ajuz', ['grid-column: 3', 'text-align: end']));
776
- out.push(rule('.pt-bayt-single .pt-sadr', ['grid-column: 1 / -1', 'text-align: center']));
777
- out.push('@media (max-width: 30em) {\n' +
778
- ' .pt-bayt { display: block; }\n' +
779
- ' .pt-sadr, .pt-ajuz { display: block; }\n' +
780
- ' .pt-verse-ornament { display: none; }\n' +
781
- '}\n');
782
- }
56
+ createTextRules({
57
+ config, options, dpi, body, bodyPx, fam, em, lh, color, px, spacingEm, out
58
+ });
59
+ createFigureRules({
60
+ config, options, dpi, bodyPx, fam, em, color, px, out
61
+ });
62
+ createBoxRules({
63
+ config, options, dpi, body, bodyPx, fam, em, lh, color, px, spacingEm, rtl, out
64
+ });
65
+ createApparatusRules({
66
+ config, options, dpi, body, bodyPx, fam, lh, color, px, out
67
+ });
68
+ createVerseRules({
69
+ config, options, dpi, body, bodyPx, fam, em, lh, color, px, out
70
+ });
783
71
  return {
784
72
  css: out.filter(Boolean).map((r) => (typeof r === 'string' ? r : ruleText(r))).join('\n'),
785
73
  families,