amethyst-cli 0.1.0__py3-none-any.whl
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.
- amethyst/__init__.py +5 -0
- amethyst/__main__.py +6 -0
- amethyst/cli.py +644 -0
- amethyst/config.py +387 -0
- amethyst/document.py +228 -0
- amethyst/errors.py +66 -0
- amethyst/ooxml.py +549 -0
- amethyst/parse/__init__.py +20 -0
- amethyst/parse/assets.py +106 -0
- amethyst/parse/frontmatter.py +62 -0
- amethyst/parse/markdown.py +49 -0
- amethyst/remote.py +236 -0
- amethyst/render/__init__.py +42 -0
- amethyst/render/base.py +85 -0
- amethyst/render/docx.py +1060 -0
- amethyst/render/furniture.py +112 -0
- amethyst/render/highlight.py +315 -0
- amethyst/render/html.py +266 -0
- amethyst/render/pdf.py +219 -0
- amethyst/theme/__init__.py +493 -0
- amethyst/theme/builtin/academic.toml +45 -0
- amethyst/theme/builtin/css/base.css +361 -0
- amethyst/theme/builtin/default.toml +42 -0
- amethyst/theme/builtin/github.toml +44 -0
- amethyst/theme/to_css.py +182 -0
- amethyst/theme/to_docx.py +651 -0
- amethyst_cli-0.1.0.dist-info/METADATA +293 -0
- amethyst_cli-0.1.0.dist-info/RECORD +31 -0
- amethyst_cli-0.1.0.dist-info/WHEEL +4 -0
- amethyst_cli-0.1.0.dist-info/entry_points.txt +2 -0
- amethyst_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/* Structural CSS for the PDF pipeline.
|
|
2
|
+
*
|
|
3
|
+
* Everything here is either layout that does not belong to a theme, or a
|
|
4
|
+
* default that a theme overrides. A theme compiles to a `:root` block appended
|
|
5
|
+
* after this file — after, because two `:root` blocks have the same specificity
|
|
6
|
+
* and the later one wins. The custom properties below are the values it
|
|
7
|
+
* replaces, which keeps this stylesheet readable on its own and keeps the PDF
|
|
8
|
+
* path working with no theme loaded at all.
|
|
9
|
+
*
|
|
10
|
+
* One rule for anything added here: it must render without a WeasyPrint
|
|
11
|
+
* warning. The renderer forwards WeasyPrint's log to the user, so a property it
|
|
12
|
+
* does not support is not a silent no-op — it is a line of noise on every
|
|
13
|
+
* single conversion.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
:root {
|
|
17
|
+
--font-body: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
|
|
18
|
+
--font-heading: var(--font-body);
|
|
19
|
+
--font-mono: ui-monospace, "SF Mono", Menlo, "DejaVu Sans Mono", monospace;
|
|
20
|
+
|
|
21
|
+
--size-body: 11pt;
|
|
22
|
+
--size-small: 9.5pt;
|
|
23
|
+
--size-code: 0.85em;
|
|
24
|
+
--size-title: 2.6em;
|
|
25
|
+
--leading: 1.45;
|
|
26
|
+
--weight-heading: 600;
|
|
27
|
+
|
|
28
|
+
/* Heading sizes, as multiples of the body size. */
|
|
29
|
+
--size-h1: 2em;
|
|
30
|
+
--size-h2: 1.5em;
|
|
31
|
+
--size-h3: 1.22em;
|
|
32
|
+
--size-h4: 1.05em;
|
|
33
|
+
--size-h5: 1em;
|
|
34
|
+
--size-h6: 1em;
|
|
35
|
+
|
|
36
|
+
--color-text: #16151a;
|
|
37
|
+
--color-muted: #5d5a66;
|
|
38
|
+
--color-accent: #6a3fa0;
|
|
39
|
+
--color-rule: #d9d6de;
|
|
40
|
+
--color-fill: #f6f4f8;
|
|
41
|
+
|
|
42
|
+
--block-gap: 0.85em;
|
|
43
|
+
--indent: 1.6em;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* --- page furniture ---------------------------------------------------- */
|
|
47
|
+
|
|
48
|
+
/* @page lives in the generated block. Its size and margins come from the theme
|
|
49
|
+
and the flags, and a paged-media descriptor cannot read a custom property
|
|
50
|
+
anyway. */
|
|
51
|
+
|
|
52
|
+
html {
|
|
53
|
+
font-family: var(--font-body);
|
|
54
|
+
font-size: var(--size-body);
|
|
55
|
+
line-height: var(--leading);
|
|
56
|
+
color: var(--color-text);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
body {
|
|
60
|
+
margin: 0;
|
|
61
|
+
/* Ragged right. Justified text needs hyphenation to avoid rivers, and
|
|
62
|
+
hyphenation needs a language declared per document, which nothing sets
|
|
63
|
+
yet. */
|
|
64
|
+
text-align: left;
|
|
65
|
+
orphans: 2;
|
|
66
|
+
widows: 2;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* --- front matter ------------------------------------------------------ */
|
|
70
|
+
|
|
71
|
+
/* The cover and the contents sit on a named page, which is how the generated
|
|
72
|
+
block takes the running head off them without counting pages: they belong to
|
|
73
|
+
no section, so there is no section for a head to name. */
|
|
74
|
+
|
|
75
|
+
.title-page,
|
|
76
|
+
.contents {
|
|
77
|
+
page: front;
|
|
78
|
+
break-after: page;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* Roughly the upper third of the sheet. The Word renderer sets the same gap
|
|
82
|
+
above its title, in the same multiple of the body size, because neither
|
|
83
|
+
format has a way to read it off the other. */
|
|
84
|
+
.title-page {
|
|
85
|
+
padding-top: 8em;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* Not a heading: an h1 here would open a section, put an entry in the PDF
|
|
89
|
+
outline and set the running head, none of which a cover should do. */
|
|
90
|
+
.doc-title {
|
|
91
|
+
font-family: var(--font-heading);
|
|
92
|
+
font-size: var(--size-title);
|
|
93
|
+
font-weight: var(--weight-heading);
|
|
94
|
+
line-height: 1.15;
|
|
95
|
+
letter-spacing: -0.015em;
|
|
96
|
+
margin: 0 0 0.3em;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.doc-subtitle {
|
|
100
|
+
font-size: var(--size-h3);
|
|
101
|
+
font-weight: 400;
|
|
102
|
+
line-height: 1.3;
|
|
103
|
+
color: var(--color-muted);
|
|
104
|
+
margin: 0 0 3em;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.doc-author {
|
|
108
|
+
margin: 0 0 0.15em;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.doc-date {
|
|
112
|
+
font-size: var(--size-small);
|
|
113
|
+
color: var(--color-muted);
|
|
114
|
+
margin: 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* --- contents ---------------------------------------------------------- */
|
|
118
|
+
|
|
119
|
+
/* The contents names no section of its own, and the generated block sets the
|
|
120
|
+
named string from a heading level that this could well be. A class beats a
|
|
121
|
+
bare element selector, so this wins wherever that lands. */
|
|
122
|
+
.toc-heading {
|
|
123
|
+
string-set: section "";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.contents ol {
|
|
127
|
+
list-style: none;
|
|
128
|
+
margin: 0;
|
|
129
|
+
padding: 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.contents li {
|
|
133
|
+
margin: 0 0 0.3em;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.contents a {
|
|
137
|
+
color: inherit;
|
|
138
|
+
text-decoration: none;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* The dots and the page number, which only a paged renderer can fill in:
|
|
142
|
+
target-counter resolves the href against the page the heading landed on. */
|
|
143
|
+
.contents a::after {
|
|
144
|
+
content: ' ' leader('.') ' ' target-counter(attr(href), page);
|
|
145
|
+
color: var(--color-muted);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.contents .toc-1 {
|
|
149
|
+
font-weight: var(--weight-heading);
|
|
150
|
+
margin-top: 0.7em;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.contents .toc-2 { padding-left: 1.2em; }
|
|
154
|
+
.contents .toc-3 { padding-left: 2.4em; }
|
|
155
|
+
.contents .toc-4 { padding-left: 3.6em; }
|
|
156
|
+
.contents .toc-5 { padding-left: 4.8em; }
|
|
157
|
+
.contents .toc-6 { padding-left: 6em; }
|
|
158
|
+
|
|
159
|
+
/* --- headings ---------------------------------------------------------- */
|
|
160
|
+
|
|
161
|
+
h1,
|
|
162
|
+
h2,
|
|
163
|
+
h3,
|
|
164
|
+
h4,
|
|
165
|
+
h5,
|
|
166
|
+
h6 {
|
|
167
|
+
font-family: var(--font-heading);
|
|
168
|
+
font-weight: var(--weight-heading);
|
|
169
|
+
line-height: 1.2;
|
|
170
|
+
margin: 1.4em 0 0.5em;
|
|
171
|
+
/* A heading alone at the foot of a page is the most visible typesetting
|
|
172
|
+
failure there is, and these two rules are the whole fix. */
|
|
173
|
+
break-after: avoid;
|
|
174
|
+
break-inside: avoid;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
h1 { font-size: var(--size-h1); margin-top: 0; letter-spacing: -0.01em; }
|
|
178
|
+
h2 { font-size: var(--size-h2); }
|
|
179
|
+
h3 { font-size: var(--size-h3); }
|
|
180
|
+
h4 { font-size: var(--size-h4); }
|
|
181
|
+
h5 { font-size: var(--size-h5); }
|
|
182
|
+
h6 { font-size: var(--size-h6); color: var(--color-muted); }
|
|
183
|
+
|
|
184
|
+
/* --- flow -------------------------------------------------------------- */
|
|
185
|
+
|
|
186
|
+
p {
|
|
187
|
+
margin: 0 0 var(--block-gap);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
a {
|
|
191
|
+
color: var(--color-accent);
|
|
192
|
+
text-decoration: none;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
hr {
|
|
196
|
+
border: none;
|
|
197
|
+
border-top: 1px solid var(--color-rule);
|
|
198
|
+
margin: 1.6em 0;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* --- lists ------------------------------------------------------------- */
|
|
202
|
+
|
|
203
|
+
ul,
|
|
204
|
+
ol {
|
|
205
|
+
margin: 0 0 var(--block-gap);
|
|
206
|
+
padding-left: var(--indent);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
li {
|
|
210
|
+
margin-bottom: 0.2em;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* Nested lists sit inside their parent item, so their own outer margin would
|
|
214
|
+
double the gap the item already has. */
|
|
215
|
+
li > ul,
|
|
216
|
+
li > ol {
|
|
217
|
+
margin-top: 0.2em;
|
|
218
|
+
margin-bottom: 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/* Task lists carry their own bullet in the checkbox. */
|
|
222
|
+
.contains-task-list {
|
|
223
|
+
list-style: none;
|
|
224
|
+
padding-left: 0.2em;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/* The browser default stylesheet lays a checkbox out as a block, which puts
|
|
228
|
+
every task on two lines with the box alone on the first. */
|
|
229
|
+
.task-list-item-checkbox {
|
|
230
|
+
display: inline-block;
|
|
231
|
+
width: 0.72em;
|
|
232
|
+
height: 0.72em;
|
|
233
|
+
margin-right: 0.45em;
|
|
234
|
+
vertical-align: -0.02em;
|
|
235
|
+
border: 1px solid var(--color-muted);
|
|
236
|
+
border-radius: 2px;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/* --- definition lists -------------------------------------------------- */
|
|
240
|
+
|
|
241
|
+
dl {
|
|
242
|
+
margin: 0 0 var(--block-gap);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
dt {
|
|
246
|
+
font-weight: 600;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
dd {
|
|
250
|
+
margin: 0 0 0.5em var(--indent);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/* --- quotes ------------------------------------------------------------ */
|
|
254
|
+
|
|
255
|
+
blockquote {
|
|
256
|
+
margin: 0 0 var(--block-gap);
|
|
257
|
+
padding-left: 1em;
|
|
258
|
+
border-left: 2px solid var(--color-rule);
|
|
259
|
+
color: var(--color-muted);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
blockquote > :last-child {
|
|
263
|
+
margin-bottom: 0;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/* --- code -------------------------------------------------------------- */
|
|
267
|
+
|
|
268
|
+
code,
|
|
269
|
+
kbd,
|
|
270
|
+
samp {
|
|
271
|
+
font-family: var(--font-mono);
|
|
272
|
+
font-size: var(--size-code);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
:not(pre) > code {
|
|
276
|
+
background: var(--color-fill);
|
|
277
|
+
padding: 0.1em 0.28em;
|
|
278
|
+
border-radius: 3px;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
pre {
|
|
282
|
+
font-family: var(--font-mono);
|
|
283
|
+
font-size: var(--size-code);
|
|
284
|
+
line-height: 1.4;
|
|
285
|
+
background: var(--color-fill);
|
|
286
|
+
border: 1px solid var(--color-rule);
|
|
287
|
+
border-radius: 4px;
|
|
288
|
+
padding: 0.7em 0.9em;
|
|
289
|
+
margin: 0 0 var(--block-gap);
|
|
290
|
+
/* A page has no horizontal scrollbar: an over-long line has to wrap or it
|
|
291
|
+
is simply lost off the edge of the paper. */
|
|
292
|
+
white-space: pre-wrap;
|
|
293
|
+
overflow-wrap: break-word;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
pre > code {
|
|
297
|
+
background: none;
|
|
298
|
+
padding: 0;
|
|
299
|
+
font-size: inherit;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/* --- tables ------------------------------------------------------------ */
|
|
303
|
+
|
|
304
|
+
table {
|
|
305
|
+
border-collapse: collapse;
|
|
306
|
+
width: 100%;
|
|
307
|
+
margin: 0 0 var(--block-gap);
|
|
308
|
+
font-size: var(--size-small);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
th,
|
|
312
|
+
td {
|
|
313
|
+
border: 1px solid var(--color-rule);
|
|
314
|
+
padding: 0.35em 0.6em;
|
|
315
|
+
text-align: left;
|
|
316
|
+
vertical-align: top;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
th {
|
|
320
|
+
background: var(--color-fill);
|
|
321
|
+
font-weight: 600;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/* A row split across a page break is unreadable; the header repeating is the
|
|
325
|
+
reason the split is survivable at all. */
|
|
326
|
+
tr {
|
|
327
|
+
break-inside: avoid;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
thead {
|
|
331
|
+
display: table-header-group;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/* --- figures ----------------------------------------------------------- */
|
|
335
|
+
|
|
336
|
+
img {
|
|
337
|
+
max-width: 100%;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/* --- footnotes --------------------------------------------------------- */
|
|
341
|
+
|
|
342
|
+
.footnotes-sep {
|
|
343
|
+
margin-top: 2.5em;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.footnotes {
|
|
347
|
+
font-size: var(--size-small);
|
|
348
|
+
color: var(--color-muted);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.footnotes p {
|
|
352
|
+
margin-bottom: 0.35em;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/* Dropped rather than styled. The glyph markdown-it uses for it has no
|
|
356
|
+
coverage in most serif text fonts, so it sets as a missing-glyph box; and a
|
|
357
|
+
back-link is a browser affordance that a typeset page does not want, which
|
|
358
|
+
makes restoring it with a symbol font the wrong repair. */
|
|
359
|
+
.footnote-backref {
|
|
360
|
+
display: none;
|
|
361
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Amethyst's default theme.
|
|
2
|
+
#
|
|
3
|
+
# Copy this file, change what you want and pass it with -t path/to/theme.toml.
|
|
4
|
+
# Anything you leave out is taken from here, so a theme can be as short as one
|
|
5
|
+
# section — or one line.
|
|
6
|
+
|
|
7
|
+
description = "A serif text face, roomy leading and a violet accent."
|
|
8
|
+
|
|
9
|
+
[fonts]
|
|
10
|
+
# Most-preferred family first; the last one should be a generic family so
|
|
11
|
+
# there is always something to fall back to.
|
|
12
|
+
body = ["Iowan Old Style", "Palatino Linotype", "Palatino", "Georgia", "serif"]
|
|
13
|
+
heading = ["Iowan Old Style", "Palatino Linotype", "Palatino", "Georgia", "serif"]
|
|
14
|
+
mono = ["ui-monospace", "SF Mono", "Menlo", "DejaVu Sans Mono", "monospace"]
|
|
15
|
+
|
|
16
|
+
[type]
|
|
17
|
+
# The two absolute sizes are points. Everything else on this page is a
|
|
18
|
+
# multiple of `size`, so changing it alone rescales the whole document.
|
|
19
|
+
size = 11
|
|
20
|
+
small = 9.5
|
|
21
|
+
code = 0.85 # a multiple of `size`, for code blocks and inline code
|
|
22
|
+
title = 2.6 # a multiple of `size`, for the title on a title page
|
|
23
|
+
line_height = 1.45
|
|
24
|
+
heading_weight = 600
|
|
25
|
+
# h1 to h6.
|
|
26
|
+
headings = [2, 1.5, 1.22, 1.05, 1, 1]
|
|
27
|
+
|
|
28
|
+
[colors]
|
|
29
|
+
text = "#16151a" # body text
|
|
30
|
+
muted = "#5d5a66" # footnotes, page numbers, quoted text
|
|
31
|
+
accent = "#6a3fa0" # links
|
|
32
|
+
rule = "#d9d6de" # borders and horizontal rules
|
|
33
|
+
fill = "#f6f4f8" # code and table-header backgrounds
|
|
34
|
+
|
|
35
|
+
[spacing]
|
|
36
|
+
# Multiples of the body size.
|
|
37
|
+
block = 0.85 # the gap after a paragraph, list, table or code block
|
|
38
|
+
indent = 1.6 # how far a list or a definition is indented
|
|
39
|
+
|
|
40
|
+
[page]
|
|
41
|
+
size = "A4"
|
|
42
|
+
margin = "2cm"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# GitHub's Markdown look, set for paper.
|
|
2
|
+
#
|
|
3
|
+
# The web page it is modelled on is sans-serif, blue-linked and tightly ruled;
|
|
4
|
+
# what changes here is the measure and the units, because a screen scrolls and
|
|
5
|
+
# a page does not.
|
|
6
|
+
|
|
7
|
+
description = "GitHub's Markdown look: system sans, blue links, quiet rules."
|
|
8
|
+
|
|
9
|
+
[fonts]
|
|
10
|
+
# GitHub asks for the reader's own system font first. That is spelled
|
|
11
|
+
# `-apple-system` on the web, which is a keyword rather than a family and
|
|
12
|
+
# means nothing to a PDF renderer or to Word, so the stack names the fonts
|
|
13
|
+
# those actually resolve.
|
|
14
|
+
body = ["Helvetica Neue", "Segoe UI", "Roboto", "Noto Sans", "Arial", "sans-serif"]
|
|
15
|
+
heading = ["Helvetica Neue", "Segoe UI", "Roboto", "Noto Sans", "Arial", "sans-serif"]
|
|
16
|
+
mono = ["SFMono-Regular", "SF Mono", "Menlo", "Consolas", "DejaVu Sans Mono", "monospace"]
|
|
17
|
+
|
|
18
|
+
[type]
|
|
19
|
+
# A shade smaller than the default theme: a sans face at the same size reads
|
|
20
|
+
# larger than a serif one, and this one is set at GitHub's own 1.5 leading.
|
|
21
|
+
size = 10.5
|
|
22
|
+
small = 9
|
|
23
|
+
code = 0.85
|
|
24
|
+
title = 2.6
|
|
25
|
+
line_height = 1.5
|
|
26
|
+
heading_weight = 600
|
|
27
|
+
# GitHub's own scale, which drops h5 and h6 below the body size rather than
|
|
28
|
+
# leaving them level with it.
|
|
29
|
+
headings = [2, 1.5, 1.25, 1, 0.875, 0.85]
|
|
30
|
+
|
|
31
|
+
[colors]
|
|
32
|
+
text = "#1f2328"
|
|
33
|
+
muted = "#59636e"
|
|
34
|
+
accent = "#0969da"
|
|
35
|
+
rule = "#d1d9e0"
|
|
36
|
+
fill = "#f6f8fa"
|
|
37
|
+
|
|
38
|
+
[spacing]
|
|
39
|
+
block = 1 # GitHub's paragraph gap is a full line
|
|
40
|
+
indent = 1.8
|
|
41
|
+
|
|
42
|
+
[page]
|
|
43
|
+
size = "A4"
|
|
44
|
+
margin = "2cm"
|
amethyst/theme/to_css.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""A theme, compiled to CSS: the custom properties, and the page block.
|
|
2
|
+
|
|
3
|
+
Two blocks come out of here, and they are separate for a reason.
|
|
4
|
+
|
|
5
|
+
The first is nothing but custom properties. It is appended after ``base.css``
|
|
6
|
+
so that it wins the cascade, and it declares no rules of its own — which is
|
|
7
|
+
what keeps the question of *how* a document is laid out in one file, and the
|
|
8
|
+
question of what it is made of in the theme.
|
|
9
|
+
|
|
10
|
+
The second is ``@page``, which cannot be done that way. ``size`` and ``margin``
|
|
11
|
+
are at-rule descriptors, so ``size: var(--page-size)`` does not resolve; and a
|
|
12
|
+
margin box sits outside the document tree, inheriting nothing from ``:root``,
|
|
13
|
+
so the page number's own font and colour have to be written out in full.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
from collections.abc import Sequence
|
|
20
|
+
|
|
21
|
+
from amethyst.theme import Theme
|
|
22
|
+
|
|
23
|
+
#: A family name that needs no quotes: a bare word, which covers the generic
|
|
24
|
+
#: families — quoting ``serif`` would turn it into a search for a font called
|
|
25
|
+
#: "serif" — and the single-word brands. Everything else is quoted.
|
|
26
|
+
BARE_FAMILY = re.compile(r"\A[A-Za-z][A-Za-z0-9-]*\Z")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def root_css(theme: Theme) -> str:
|
|
30
|
+
"""The custom properties ``base.css`` reads, as one ``:root`` block."""
|
|
31
|
+
type_ = theme.type
|
|
32
|
+
declarations: list[tuple[str, str]] = [
|
|
33
|
+
("font-body", font_stack(theme.fonts.body)),
|
|
34
|
+
("font-heading", font_stack(theme.fonts.heading)),
|
|
35
|
+
("font-mono", font_stack(theme.fonts.mono)),
|
|
36
|
+
("", ""),
|
|
37
|
+
("size-body", points(type_.size)),
|
|
38
|
+
("size-small", points(type_.small)),
|
|
39
|
+
("size-code", multiple(type_.code)),
|
|
40
|
+
("size-title", multiple(type_.title)),
|
|
41
|
+
("leading", number(type_.line_height)),
|
|
42
|
+
("weight-heading", str(type_.heading_weight)),
|
|
43
|
+
("", ""),
|
|
44
|
+
*(
|
|
45
|
+
(f"size-h{level}", multiple(size))
|
|
46
|
+
for level, size in enumerate(type_.headings, start=1)
|
|
47
|
+
),
|
|
48
|
+
("", ""),
|
|
49
|
+
("color-text", theme.colors.text),
|
|
50
|
+
("color-muted", theme.colors.muted),
|
|
51
|
+
("color-accent", theme.colors.accent),
|
|
52
|
+
("color-rule", theme.colors.rule),
|
|
53
|
+
("color-fill", theme.colors.fill),
|
|
54
|
+
("", ""),
|
|
55
|
+
("block-gap", multiple(theme.spacing.block)),
|
|
56
|
+
("indent", multiple(theme.spacing.indent)),
|
|
57
|
+
]
|
|
58
|
+
body = [f" --{name}: {value};" if name else "" for name, value in declarations]
|
|
59
|
+
return "\n".join([f"/* theme: {theme.name} */", ":root {", *body, "}", ""])
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def page_css(
|
|
63
|
+
theme: Theme,
|
|
64
|
+
*,
|
|
65
|
+
page_numbers: bool = True,
|
|
66
|
+
running_title: str | None = None,
|
|
67
|
+
running_section: int | None = None,
|
|
68
|
+
front_matter: bool = False,
|
|
69
|
+
title_page: bool = False,
|
|
70
|
+
) -> str:
|
|
71
|
+
"""The paged-media block: sheet, margins, page number and running head.
|
|
72
|
+
|
|
73
|
+
Everything here is generated rather than written in ``base.css`` because
|
|
74
|
+
it depends on the document as well as the theme — the title is literal
|
|
75
|
+
text, and which heading level feeds the running head is decided by
|
|
76
|
+
counting the document's headings.
|
|
77
|
+
|
|
78
|
+
``running_title`` goes in the top-left corner as a literal string; it is
|
|
79
|
+
not a named string set from the ``h1``, because a document whose body
|
|
80
|
+
opens with an ``h1`` of its own would then have the head change halfway
|
|
81
|
+
down. ``running_section`` is the heading level that feeds the top-right
|
|
82
|
+
corner, and it *is* a named string, because that one is meant to change.
|
|
83
|
+
"""
|
|
84
|
+
furniture = [
|
|
85
|
+
f" font-family: {font_stack(theme.fonts.body)};",
|
|
86
|
+
f" font-size: {points(theme.type.small)};",
|
|
87
|
+
f" color: {theme.colors.muted};",
|
|
88
|
+
]
|
|
89
|
+
lines = [
|
|
90
|
+
"@page {",
|
|
91
|
+
f" size: {theme.page.size};",
|
|
92
|
+
f" margin: {theme.page.margin};",
|
|
93
|
+
]
|
|
94
|
+
if running_title:
|
|
95
|
+
lines += [
|
|
96
|
+
" @top-left {",
|
|
97
|
+
f" content: {css_string(running_title)};",
|
|
98
|
+
*furniture,
|
|
99
|
+
" }",
|
|
100
|
+
]
|
|
101
|
+
if running_section is not None:
|
|
102
|
+
lines += [" @top-right {", " content: string(section);", *furniture, " }"]
|
|
103
|
+
if page_numbers:
|
|
104
|
+
lines += [
|
|
105
|
+
" @bottom-center {",
|
|
106
|
+
" content: counter(page);",
|
|
107
|
+
*furniture,
|
|
108
|
+
" }",
|
|
109
|
+
]
|
|
110
|
+
lines += ["}", ""]
|
|
111
|
+
|
|
112
|
+
if running_title or running_section is not None:
|
|
113
|
+
# The opening page of a document needs no running head: whatever it
|
|
114
|
+
# would name is set in full a few centimetres below it.
|
|
115
|
+
lines += [f"@page :first {{{_no_head(running_title, running_section)} }}", ""]
|
|
116
|
+
if running_section is not None:
|
|
117
|
+
lines += [f"h{running_section} {{ string-set: section content(); }}", ""]
|
|
118
|
+
|
|
119
|
+
if front_matter:
|
|
120
|
+
# Front matter belongs to no section and is not the document yet, so
|
|
121
|
+
# it carries no head — and a cover carries no page number either.
|
|
122
|
+
lines += [f"@page front {{{_no_head(running_title, running_section)} }}", ""]
|
|
123
|
+
if title_page:
|
|
124
|
+
lines += ["@page front:first { @bottom-center { content: none } }", ""]
|
|
125
|
+
return "\n".join(lines)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _no_head(running_title: str | None, running_section: int | None) -> str:
|
|
129
|
+
"""Empty out whichever margin boxes the running head was put in."""
|
|
130
|
+
boxes = []
|
|
131
|
+
if running_title:
|
|
132
|
+
boxes.append(" @top-left { content: none }")
|
|
133
|
+
if running_section is not None:
|
|
134
|
+
boxes.append(" @top-right { content: none }")
|
|
135
|
+
return "".join(boxes)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def css_string(value: str) -> str:
|
|
139
|
+
"""Quote arbitrary text as a CSS string, so a quote in a title is safe.
|
|
140
|
+
|
|
141
|
+
The title reaches the stylesheet as a literal because a margin box cannot
|
|
142
|
+
read a custom property. It is the author's text, though, so it has to be
|
|
143
|
+
escaped rather than trusted: an unescaped quote would end the string and
|
|
144
|
+
the rest of the block would be read as something else entirely.
|
|
145
|
+
"""
|
|
146
|
+
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
|
|
147
|
+
# A newline cannot appear inside a CSS string at all, escaped or not.
|
|
148
|
+
escaped = " ".join(escaped.split())
|
|
149
|
+
return f'"{escaped}"'
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def font_stack(families: Sequence[str]) -> str:
|
|
153
|
+
"""Join family names into a CSS font stack, quoting only what needs it."""
|
|
154
|
+
return ", ".join(
|
|
155
|
+
family if BARE_FAMILY.match(family) else f'"{family}"' for family in families
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def points(value: float) -> str:
|
|
160
|
+
"""An absolute size, in the unit print is measured in."""
|
|
161
|
+
return f"{number(value)}pt"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def multiple(value: float) -> str:
|
|
165
|
+
"""A size relative to the text it sits in."""
|
|
166
|
+
return f"{number(value)}em"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def number(value: float) -> str:
|
|
170
|
+
"""Write a number the short way: 11 rather than 11.0, 1.45 as it is."""
|
|
171
|
+
return f"{value:g}"
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
__all__ = [
|
|
175
|
+
"css_string",
|
|
176
|
+
"font_stack",
|
|
177
|
+
"multiple",
|
|
178
|
+
"number",
|
|
179
|
+
"page_css",
|
|
180
|
+
"points",
|
|
181
|
+
"root_css",
|
|
182
|
+
]
|