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

1"""Styles that set how a `coursebook` project is formatted. 

2 

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`. 

7 

8Every subclass of `Style` is registered automatically. So, to add a style, 

9subclass `Style` (or one of the styles here) and change its defaults: 

10 

11```python 

12import dataclasses 

13 

14import coursebook 

15 

16 

17@dataclasses.dataclass 

18class Seminar(coursebook.Coursebook): 

19 number_headings: bool = True 

20``` 

21 

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. 

29 

30""" 

31 

32from __future__ import annotations 

33 

34import dataclasses 

35from typing import TYPE_CHECKING, Any, ClassVar 

36 

37import wonka 

38 

39from . import markdown 

40 

41if TYPE_CHECKING: 

42 from . import outline 

43 

44 

45@dataclasses.dataclass 

46class Formatting: 

47 """Formatting of the text and notes. 

48 

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. 

54 

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`. 

77 

78 """ 

79 

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 

91 

92 

93@dataclasses.dataclass 

94class Style(wonka.AutoRegistrar): 

95 """Base class for styles. 

96 

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. 

116 

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. 

122 

123 """ 

124 

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, ...]] = () 

135 

136 """ Public Methods """ 

137 

138 def format_heading(self, entry: outline.OutlineEntry) -> str: 

139 """Returns the heading text for an outline entry. 

140 

141 Args: 

142 entry: the outline entry. 

143 

144 Returns: 

145 The title of `entry`, after its label if `number_headings` is 

146 `True` and `entry` is numbered. 

147 

148 """ 

149 if self.number_headings and entry.numbered: 

150 return f'{entry.label}{self.label_separator}{entry.title}' 

151 return entry.title 

152 

153 

154@dataclasses.dataclass 

155class Book(Style): 

156 """Style for a book with accompanying slides. 

157 

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. 

168 

169 """ 

170 

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)) 

179 

180 

181@dataclasses.dataclass 

182class Coursebook(Book): 

183 """Style for a coursebook with slides and teaching notes. 

184 

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. 

195 

196 """ 

197 

198 sections: tuple[str, ...] = ('text', 'slides', 'notes') 

199 aliases: ClassVar[tuple[str, ...]] = ('casebook', 'textbook') 

200 

201 

202@dataclasses.dataclass 

203class LawReview(Style): 

204 """Style for a law review article with accompanying slides. 

205 

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. 

218 

219 """ 

220 

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',) 

236 

237 

238""" Public Functions """ 

239 

240 

241def get_style(name: str, **overrides: Any) -> Style: 

242 """Returns the style with `name`. 

243 

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. 

249 

250 Returns: 

251 An instance of the style. 

252 

253 Raises: 

254 ValueError: if no style has `name`. 

255 

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)