Coverage for src/coursebook/styles.py: 100%
63 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-31 22:06 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-31 22:06 +0000
1"""Styles that set how a `coursebook` project is formatted.
3A style sets the default sections, heading labels, templates, citation style,
4and formatting of a project. The "Style" setting of a project file chooses a
5style by name. Names are matched without regard to case, spaces, or
6underscores, so 'Law Review' chooses `LawReview`.
8Every subclass of `Style` is registered automatically. So, to add a style,
9subclass `Style` (or one of the styles here) and change its defaults:
11```python
12import dataclasses
14import coursebook
17@dataclasses.dataclass
18class Seminar(coursebook.Coursebook):
19 number_headings: bool = True
20```
22Contents:
23 Book: style for a book with accompanying slides.
24 Coursebook: style for a coursebook with slides and teaching notes.
25 Formatting: formatting of the text and notes.
26 LawReview: style for a law review article with accompanying slides.
27 Style: base class for styles.
28 get_style: returns the style with a name.
30"""
32from __future__ import annotations
34import dataclasses
35from typing import TYPE_CHECKING, Any, ClassVar
37import wonka
39from . import markdown
41if TYPE_CHECKING:
42 from . import outline
45@dataclasses.dataclass
46class Formatting:
47 """Formatting of the text and notes.
49 For Word files, the formatting is applied to pandoc's default Word
50 template, so it is only used when a project does not name its own Word
51 template. For PDF and HTML files, the formatting that those formats
52 support is passed to pandoc's templates. A value of `None` keeps the
53 formatting of pandoc's templates.
55 Args:
56 body_font: font for the text. Defaults to `None`.
57 heading_font: font for headings and the title. Defaults to `None`,
58 which uses `body_font`.
59 font_size: size of the text in points. Defaults to `None`.
60 line_spacing: spacing between the lines of the text, as a multiple of
61 single spacing (such as 2.0 for double spacing). Defaults to
62 `None`.
63 first_line_indent: indent of the first line of each paragraph after
64 the first one under a heading, in inches. Defaults to `None`.
65 footnote_size: size of footnotes in points. Defaults to `None`.
66 heading_color: color of headings and the title as a hexadecimal RGB
67 value (such as '000000' for black). Defaults to `None`.
68 center_top_headings: whether to center the top level headings.
69 Defaults to `False`.
70 capitalize_top_headings: whether to show the top level headings in
71 capital letters. The text itself is not changed. Defaults to
72 `False`.
73 margins: the page margins on every side, in inches. Defaults to
74 `None`.
75 page_numbers: whether to put page numbers at the bottom of each page.
76 Defaults to `False`.
78 """
80 body_font: str | None = None
81 heading_font: str | None = None
82 font_size: float | None = None
83 line_spacing: float | None = None
84 first_line_indent: float | None = None
85 footnote_size: float | None = None
86 heading_color: str | None = None
87 center_top_headings: bool = False
88 capitalize_top_headings: bool = False
89 margins: float | None = None
90 page_numbers: bool = False
93@dataclasses.dataclass
94class Style(wonka.AutoRegistrar):
95 """Base class for styles.
97 Args:
98 sections: the sections to create files for when a project does not
99 set "Include". Defaults to ('text', 'slides').
100 number_headings: whether to put the outline labels in headings (such
101 as 'I. Introduction'). Defaults to `False`.
102 table_of_contents: whether to add a table of contents to the text and
103 notes. Defaults to `False`.
104 label_separator: text between an outline label and its title in a
105 heading. Defaults to '. '.
106 text_template: name or path of the Word template used when a project
107 does not set one. Defaults to `None`, which uses pandoc's default
108 template with `formatting` applied.
109 slides_template: name or path of the PowerPoint template used when a
110 project does not set one. Defaults to 'chalkboard'.
111 csl: name or path of the citation style used when a project does not
112 set one. Defaults to `None`, which uses pandoc's default (Chicago
113 author-date).
114 formatting: formatting of the text and notes. Defaults to a
115 `Formatting` that keeps the formatting of pandoc's templates.
117 Attributes:
118 aliases: other names (in lower case with underscores) that choose this
119 style.
120 registry: every `Style` subclass, with keys that are the snake case
121 names of the classes.
123 """
125 sections: tuple[str, ...] = ('text', 'slides')
126 number_headings: bool = False
127 table_of_contents: bool = False
128 label_separator: str = '. '
129 text_template: str | None = None
130 slides_template: str | None = 'chalkboard'
131 csl: str | None = None
132 formatting: Formatting = dataclasses.field(
133 default_factory = Formatting)
134 aliases: ClassVar[tuple[str, ...]] = ()
136 """ Public Methods """
138 def format_heading(self, entry: outline.OutlineEntry) -> str:
139 """Returns the heading text for an outline entry.
141 Args:
142 entry: the outline entry.
144 Returns:
145 The title of `entry`, after its label if `number_headings` is
146 `True` and `entry` is numbered.
148 """
149 if self.number_headings and entry.numbered:
150 return f'{entry.label}{self.label_separator}{entry.title}'
151 return entry.title
154@dataclasses.dataclass
155class Book(Style):
156 """Style for a book with accompanying slides.
158 Args:
159 sections: defaults to ('text', 'slides').
160 number_headings: defaults to `False`.
161 table_of_contents: defaults to `True`.
162 label_separator: defaults to '. '.
163 text_template: defaults to `None`.
164 slides_template: defaults to 'chalkboard'.
165 csl: defaults to `None`.
166 formatting: defaults to Georgia at 11 points with 1.15 line spacing
167 and page numbers.
169 """
171 table_of_contents: bool = True
172 formatting: Formatting = dataclasses.field(
173 default_factory = lambda: Formatting(
174 body_font = 'Georgia',
175 font_size = 11,
176 line_spacing = 1.15,
177 footnote_size = 9,
178 page_numbers = True))
181@dataclasses.dataclass
182class Coursebook(Book):
183 """Style for a coursebook with slides and teaching notes.
185 Args:
186 sections: defaults to ('text', 'slides', 'notes').
187 number_headings: defaults to `False`.
188 table_of_contents: defaults to `True`.
189 label_separator: defaults to '. '.
190 text_template: defaults to `None`.
191 slides_template: defaults to 'chalkboard'.
192 csl: defaults to `None`.
193 formatting: defaults to Georgia at 11 points with 1.15 line spacing
194 and page numbers.
196 """
198 sections: tuple[str, ...] = ('text', 'slides', 'notes')
199 aliases: ClassVar[tuple[str, ...]] = ('casebook', 'textbook')
202@dataclasses.dataclass
203class LawReview(Style):
204 """Style for a law review article with accompanying slides.
206 Args:
207 sections: defaults to ('text', 'slides').
208 number_headings: defaults to `True`.
209 table_of_contents: defaults to `False`.
210 label_separator: defaults to '. '.
211 text_template: defaults to `None`.
212 slides_template: defaults to 'chalkboard'.
213 csl: defaults to 'bluebook-law-review', which formats citations as
214 Bluebook footnotes.
215 formatting: defaults to double spaced Times New Roman at 12 points,
216 with 10 point footnotes, centered, capitalized, black top level
217 headings, one inch margins, and page numbers.
219 """
221 number_headings: bool = True
222 csl: str | None = 'bluebook-law-review'
223 formatting: Formatting = dataclasses.field(
224 default_factory = lambda: Formatting(
225 body_font = 'Times New Roman',
226 font_size = 12,
227 line_spacing = 2.0,
228 first_line_indent = 0.5,
229 footnote_size = 10,
230 heading_color = '000000',
231 center_top_headings = True,
232 capitalize_top_headings = True,
233 margins = 1.0,
234 page_numbers = True))
235 aliases: ClassVar[tuple[str, ...]] = ('law_review_article',)
238""" Public Functions """
241def get_style(name: str, **overrides: Any) -> Style:
242 """Returns the style with `name`.
244 Args:
245 name: the name of a `Style` subclass or one of its aliases (such as
246 'law review' or 'Coursebook').
247 **overrides: values that replace the defaults of the style. Values
248 of `None` are ignored.
250 Returns:
251 An instance of the style.
253 Raises:
254 ValueError: if no style has `name`.
256 """
257 key = markdown.normalize_name(name)
258 choices: dict[str, type[Style]] = {}
259 for registered_name, registered in Style.registry.items():
260 choices[str(registered_name)] = registered
261 for alias in registered.aliases:
262 choices.setdefault(alias, registered)
263 choices.pop('style', None)
264 if key not in choices:
265 known = ', '.join(sorted(choices))
266 message = f'unknown style "{name}" (use one of: {known})'
267 raise ValueError(message)
268 kwargs = {
269 field: value for field, value in overrides.items()
270 if value is not None}
271 return choices[key](**kwargs)