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.
@@ -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"
@@ -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
+ ]