typsphinx 0.3.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.
- typsphinx/__init__.py +58 -0
- typsphinx/builder.py +322 -0
- typsphinx/pdf.py +192 -0
- typsphinx/template_engine.py +396 -0
- typsphinx/templates/base.typ +81 -0
- typsphinx/translator.py +1583 -0
- typsphinx/writer.py +144 -0
- typsphinx-0.3.0.dist-info/METADATA +355 -0
- typsphinx-0.3.0.dist-info/RECORD +13 -0
- typsphinx-0.3.0.dist-info/WHEEL +5 -0
- typsphinx-0.3.0.dist-info/entry_points.txt +3 -0
- typsphinx-0.3.0.dist-info/licenses/LICENSE +21 -0
- typsphinx-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Template engine for Typst document generation.
|
|
3
|
+
|
|
4
|
+
This module implements template loading, parameter mapping, and rendering
|
|
5
|
+
for Typst documents (Requirement 8).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TemplateEngine:
|
|
16
|
+
"""
|
|
17
|
+
Manages Typst templates for document generation.
|
|
18
|
+
|
|
19
|
+
Responsibilities:
|
|
20
|
+
- Load default or custom Typst templates
|
|
21
|
+
- Search templates in multiple directories with priority
|
|
22
|
+
- Provide fallback to default template when custom template not found
|
|
23
|
+
- Map Sphinx metadata to template parameters
|
|
24
|
+
- Render final Typst document with template and content
|
|
25
|
+
|
|
26
|
+
Requirement 8.1: Default Typst template included in package
|
|
27
|
+
Requirement 8.2: Support custom template specification
|
|
28
|
+
Requirement 8.7: Priority search in user project directory
|
|
29
|
+
Requirement 8.9: Fallback to default template with warning
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
# Standard mapping from Sphinx metadata to template parameters
|
|
33
|
+
# Requirement 8.3: Sphinx metadata passed to template
|
|
34
|
+
# Requirement 8.5: Standard metadata name transformation
|
|
35
|
+
DEFAULT_PARAMETER_MAPPING = {
|
|
36
|
+
"project": "title",
|
|
37
|
+
"author": "authors",
|
|
38
|
+
"release": "date",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
template_path: Optional[str] = None,
|
|
44
|
+
template_name: Optional[str] = None,
|
|
45
|
+
search_paths: Optional[List[str]] = None,
|
|
46
|
+
parameter_mapping: Optional[Dict[str, str]] = None,
|
|
47
|
+
typst_package: Optional[str] = None,
|
|
48
|
+
typst_template_function: Optional[str] = None,
|
|
49
|
+
typst_package_imports: Optional[List[str]] = None,
|
|
50
|
+
):
|
|
51
|
+
"""
|
|
52
|
+
Initialize TemplateEngine.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
template_path: Absolute path to template file (highest priority)
|
|
56
|
+
template_name: Template filename to search in search_paths
|
|
57
|
+
search_paths: List of directories to search for templates (priority order)
|
|
58
|
+
parameter_mapping: Custom mapping from Sphinx metadata to template parameters
|
|
59
|
+
(Requirement 8.4: different parameter names)
|
|
60
|
+
typst_package: Typst Universe package specification (e.g., "@preview/charged-ieee:0.1.0")
|
|
61
|
+
(Requirement 8.6: external template packages)
|
|
62
|
+
typst_template_function: Template function name from package
|
|
63
|
+
typst_package_imports: Specific items to import from package
|
|
64
|
+
"""
|
|
65
|
+
self.template_path = template_path
|
|
66
|
+
self.template_name = template_name or "base.typ"
|
|
67
|
+
self.search_paths = search_paths or []
|
|
68
|
+
self.parameter_mapping = (
|
|
69
|
+
parameter_mapping or self.DEFAULT_PARAMETER_MAPPING.copy()
|
|
70
|
+
)
|
|
71
|
+
self.typst_package = typst_package
|
|
72
|
+
self.typst_template_function = typst_template_function
|
|
73
|
+
self.typst_package_imports = typst_package_imports or []
|
|
74
|
+
|
|
75
|
+
def get_default_template_path(self) -> str:
|
|
76
|
+
"""
|
|
77
|
+
Get the path to the default template bundled with the package.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Absolute path to default template file
|
|
81
|
+
"""
|
|
82
|
+
# Template is located in sphinxcontrib/typst/templates/base.typ
|
|
83
|
+
package_dir = Path(__file__).parent
|
|
84
|
+
template_dir = package_dir / "templates"
|
|
85
|
+
default_template = template_dir / "base.typ"
|
|
86
|
+
|
|
87
|
+
return str(default_template)
|
|
88
|
+
|
|
89
|
+
def load_template(self) -> str:
|
|
90
|
+
"""
|
|
91
|
+
Load Typst template with priority order:
|
|
92
|
+
1. Explicit template_path if provided
|
|
93
|
+
2. Search for template_name in search_paths (first match wins)
|
|
94
|
+
3. Default template bundled with package
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Template content as string
|
|
98
|
+
|
|
99
|
+
Requirement 8.1: Load default template
|
|
100
|
+
Requirement 8.2: Load custom template
|
|
101
|
+
Requirement 8.7: Search in user project directory
|
|
102
|
+
Requirement 8.9: Fallback to default with warning
|
|
103
|
+
"""
|
|
104
|
+
template_content = None
|
|
105
|
+
|
|
106
|
+
# Priority 1: Explicit template path
|
|
107
|
+
if self.template_path:
|
|
108
|
+
template_content = self._try_load_file(self.template_path)
|
|
109
|
+
if template_content is None:
|
|
110
|
+
logger.warning(
|
|
111
|
+
f"Custom template not found: {self.template_path}. "
|
|
112
|
+
f"Falling back to default template."
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Priority 2: Search in search_paths
|
|
116
|
+
if template_content is None and self.search_paths:
|
|
117
|
+
for search_dir in self.search_paths:
|
|
118
|
+
candidate_path = Path(search_dir) / self.template_name
|
|
119
|
+
template_content = self._try_load_file(str(candidate_path))
|
|
120
|
+
if template_content is not None:
|
|
121
|
+
logger.debug(f"Loaded template from: {candidate_path}")
|
|
122
|
+
break
|
|
123
|
+
|
|
124
|
+
# Priority 3: Default template
|
|
125
|
+
if template_content is None:
|
|
126
|
+
default_path = self.get_default_template_path()
|
|
127
|
+
template_content = self._try_load_file(default_path)
|
|
128
|
+
|
|
129
|
+
if template_content is None:
|
|
130
|
+
# This should never happen if package is properly installed
|
|
131
|
+
raise FileNotFoundError(
|
|
132
|
+
f"Default template not found at: {default_path}. "
|
|
133
|
+
f"Package installation may be corrupted."
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
return template_content
|
|
137
|
+
|
|
138
|
+
def map_parameters(self, sphinx_metadata: Dict[str, Any]) -> Dict[str, Any]:
|
|
139
|
+
"""
|
|
140
|
+
Map Sphinx metadata to template parameters.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
sphinx_metadata: Dictionary of Sphinx configuration metadata
|
|
144
|
+
(project, author, release, etc.)
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Dictionary of template parameters ready to pass to template
|
|
148
|
+
|
|
149
|
+
Requirement 8.3: Pass Sphinx metadata to template
|
|
150
|
+
Requirement 8.4: Support different parameter names
|
|
151
|
+
Requirement 8.5: Standard metadata name transformation
|
|
152
|
+
Requirement 8.8: Convert to arrays and complex structures
|
|
153
|
+
"""
|
|
154
|
+
params = {}
|
|
155
|
+
|
|
156
|
+
# Apply mapping
|
|
157
|
+
for sphinx_key, template_key in self.parameter_mapping.items():
|
|
158
|
+
if sphinx_key in sphinx_metadata:
|
|
159
|
+
value = sphinx_metadata[sphinx_key]
|
|
160
|
+
|
|
161
|
+
# Special handling for authors (both standard "authors" and custom names)
|
|
162
|
+
if sphinx_key == "author" or template_key in ("authors", "doc_authors"):
|
|
163
|
+
value = self._convert_to_authors_tuple(value)
|
|
164
|
+
|
|
165
|
+
params[template_key] = value
|
|
166
|
+
|
|
167
|
+
# Provide default values for missing required parameters
|
|
168
|
+
if "title" not in params:
|
|
169
|
+
params["title"] = ""
|
|
170
|
+
if "authors" not in params:
|
|
171
|
+
params["authors"] = ()
|
|
172
|
+
if "date" not in params:
|
|
173
|
+
params["date"] = None
|
|
174
|
+
|
|
175
|
+
return params
|
|
176
|
+
|
|
177
|
+
def generate_package_import(self) -> str:
|
|
178
|
+
"""
|
|
179
|
+
Generate Typst package import statement.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Import statement string, or empty string if no package specified
|
|
183
|
+
|
|
184
|
+
Requirement 8.6: Typst Universe external template packages
|
|
185
|
+
"""
|
|
186
|
+
if not self.typst_package:
|
|
187
|
+
return ""
|
|
188
|
+
|
|
189
|
+
# Generate import statement
|
|
190
|
+
if self.typst_package_imports:
|
|
191
|
+
# Import specific items: #import "@package:version": item1, item2
|
|
192
|
+
items = ", ".join(self.typst_package_imports)
|
|
193
|
+
return f'#import "{self.typst_package}": {items}'
|
|
194
|
+
elif self.typst_template_function:
|
|
195
|
+
# Import template function: #import "@package:version": template_func
|
|
196
|
+
return f'#import "{self.typst_package}": {self.typst_template_function}'
|
|
197
|
+
else:
|
|
198
|
+
# Import entire module: #import "@package:version"
|
|
199
|
+
return f'#import "{self.typst_package}"'
|
|
200
|
+
|
|
201
|
+
def extract_toctree_options(self, doctree: Any) -> Dict[str, Any]:
|
|
202
|
+
"""
|
|
203
|
+
Extract toctree options from doctree for template parameters.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
doctree: Docutils document tree
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Dictionary of toctree options for template
|
|
210
|
+
|
|
211
|
+
Requirement 8.12: toctree options passed as template parameters
|
|
212
|
+
Requirement 8.13: template reflects toctree options in #outline()
|
|
213
|
+
Requirement 13.8: #outline() managed at template level
|
|
214
|
+
Requirement 13.9: toctree options mapped to template parameters
|
|
215
|
+
"""
|
|
216
|
+
from sphinx import addnodes
|
|
217
|
+
|
|
218
|
+
# Try to find toctree node in doctree
|
|
219
|
+
toctree_nodes = list(doctree.traverse(addnodes.toctree))
|
|
220
|
+
|
|
221
|
+
if not toctree_nodes:
|
|
222
|
+
# No toctree found - return empty dict
|
|
223
|
+
return {}
|
|
224
|
+
|
|
225
|
+
# Use first toctree node found
|
|
226
|
+
toctree = toctree_nodes[0]
|
|
227
|
+
|
|
228
|
+
# Extract options with defaults
|
|
229
|
+
# Note: Sphinx toctree numbered can be False, True, or int
|
|
230
|
+
# Convert to bool for Typst (0 means False, positive means True)
|
|
231
|
+
numbered_value = toctree.get("numbered", False)
|
|
232
|
+
if isinstance(numbered_value, int):
|
|
233
|
+
numbered_value = numbered_value > 0
|
|
234
|
+
|
|
235
|
+
# Note: Sphinx toctree maxdepth can be -1 (unlimited)
|
|
236
|
+
# Typst outline() requires positive depth or none
|
|
237
|
+
# Convert -1 to none for unlimited depth
|
|
238
|
+
maxdepth_value = toctree.get("maxdepth", 2)
|
|
239
|
+
if maxdepth_value == -1:
|
|
240
|
+
maxdepth_value = None
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
"toctree_maxdepth": maxdepth_value,
|
|
244
|
+
"toctree_numbered": numbered_value,
|
|
245
|
+
"toctree_caption": toctree.get("caption", ""),
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
def get_template_content(self) -> str:
|
|
249
|
+
"""
|
|
250
|
+
Get the template content for writing to a separate file.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Template content as string
|
|
254
|
+
|
|
255
|
+
This is used when templates are written as separate files
|
|
256
|
+
instead of being inlined in the main document.
|
|
257
|
+
"""
|
|
258
|
+
template = self.load_template()
|
|
259
|
+
return template
|
|
260
|
+
|
|
261
|
+
def render(
|
|
262
|
+
self, params: Dict[str, Any], body: str, template_file: str = None
|
|
263
|
+
) -> str:
|
|
264
|
+
"""
|
|
265
|
+
Render final Typst document with template and body.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
params: Template parameters (title, authors, etc.)
|
|
269
|
+
body: Document body content (Typst markup)
|
|
270
|
+
template_file: Path to template file for import (relative to output dir).
|
|
271
|
+
If None, template is inlined (old behavior).
|
|
272
|
+
If specified, template is imported from file.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Complete Typst document string
|
|
276
|
+
|
|
277
|
+
Requirement 8.2: Use custom template
|
|
278
|
+
Requirement 8.10: Pass document settings to template
|
|
279
|
+
Requirement 8.14: #outline() in template, not body
|
|
280
|
+
"""
|
|
281
|
+
# Build output parts
|
|
282
|
+
output_parts = []
|
|
283
|
+
|
|
284
|
+
# Add package import if using Typst Universe package
|
|
285
|
+
package_import = self.generate_package_import()
|
|
286
|
+
if package_import:
|
|
287
|
+
output_parts.append(package_import)
|
|
288
|
+
output_parts.append("") # Blank line
|
|
289
|
+
|
|
290
|
+
if template_file:
|
|
291
|
+
# Import essential packages (needed for content, not just template)
|
|
292
|
+
output_parts.append("// Essential package imports")
|
|
293
|
+
output_parts.append('#import "@preview/codly:1.3.0": *')
|
|
294
|
+
output_parts.append('#import "@preview/codly-languages:0.1.1": *')
|
|
295
|
+
output_parts.append('#import "@preview/mitex:0.2.4": mi, mitex')
|
|
296
|
+
output_parts.append('#import "@preview/gentle-clues:1.2.0": *')
|
|
297
|
+
output_parts.append("") # Blank line
|
|
298
|
+
|
|
299
|
+
# Import template from separate file
|
|
300
|
+
template_func = self.typst_template_function or "project"
|
|
301
|
+
output_parts.append(f'#import "{template_file}": {template_func}')
|
|
302
|
+
output_parts.append("") # Blank line
|
|
303
|
+
else:
|
|
304
|
+
# Load template inline (old behavior)
|
|
305
|
+
# For external packages, we skip loading the template
|
|
306
|
+
if not self.typst_package:
|
|
307
|
+
template = self.load_template()
|
|
308
|
+
output_parts.append(template)
|
|
309
|
+
output_parts.append("") # Blank line
|
|
310
|
+
|
|
311
|
+
# Generate #show statement with template function call
|
|
312
|
+
template_func = self.typst_template_function or "project"
|
|
313
|
+
output_parts.append(f"#show: {template_func}.with(")
|
|
314
|
+
|
|
315
|
+
# Format parameters
|
|
316
|
+
for key, value in params.items():
|
|
317
|
+
formatted_value = self._format_typst_value(value)
|
|
318
|
+
output_parts.append(f" {key}: {formatted_value},")
|
|
319
|
+
|
|
320
|
+
output_parts.append(")")
|
|
321
|
+
output_parts.append("") # Blank line
|
|
322
|
+
|
|
323
|
+
# Add body content
|
|
324
|
+
output_parts.append(body)
|
|
325
|
+
|
|
326
|
+
return "\n".join(output_parts)
|
|
327
|
+
|
|
328
|
+
def _format_typst_value(self, value: Any) -> str:
|
|
329
|
+
"""
|
|
330
|
+
Format Python value as Typst value.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
value: Python value (str, int, bool, tuple, etc.)
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
Typst-formatted value string
|
|
337
|
+
"""
|
|
338
|
+
if value is None:
|
|
339
|
+
return "none"
|
|
340
|
+
elif isinstance(value, bool):
|
|
341
|
+
# Typst uses lowercase true/false
|
|
342
|
+
return "true" if value else "false"
|
|
343
|
+
elif isinstance(value, str):
|
|
344
|
+
# Escape quotes and backslashes
|
|
345
|
+
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
|
|
346
|
+
return f'"{escaped}"'
|
|
347
|
+
elif isinstance(value, (int, float)):
|
|
348
|
+
return str(value)
|
|
349
|
+
elif isinstance(value, (list, tuple)):
|
|
350
|
+
# Format as Typst array/tuple
|
|
351
|
+
formatted_items = [self._format_typst_value(item) for item in value]
|
|
352
|
+
return f'({", ".join(formatted_items)},)' if formatted_items else "()"
|
|
353
|
+
elif isinstance(value, dict):
|
|
354
|
+
# Format as Typst dictionary (not commonly used in templates)
|
|
355
|
+
items = [f"{k}: {self._format_typst_value(v)}" for k, v in value.items()]
|
|
356
|
+
return f'({", ".join(items)})'
|
|
357
|
+
else:
|
|
358
|
+
# Default: convert to string and quote
|
|
359
|
+
return f'"{str(value)}"'
|
|
360
|
+
|
|
361
|
+
def _convert_to_authors_tuple(self, author_value: Any) -> tuple:
|
|
362
|
+
"""
|
|
363
|
+
Convert author metadata to tuple of authors.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
author_value: Author metadata (string or list)
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
Tuple of author names
|
|
370
|
+
|
|
371
|
+
Requirement 8.8: Convert to arrays and complex structures
|
|
372
|
+
"""
|
|
373
|
+
if isinstance(author_value, (list, tuple)):
|
|
374
|
+
return tuple(author_value)
|
|
375
|
+
elif isinstance(author_value, str):
|
|
376
|
+
# Split comma-separated authors
|
|
377
|
+
authors = [a.strip() for a in author_value.split(",")]
|
|
378
|
+
return tuple(authors)
|
|
379
|
+
else:
|
|
380
|
+
return (str(author_value),)
|
|
381
|
+
|
|
382
|
+
def _try_load_file(self, file_path: str) -> Optional[str]:
|
|
383
|
+
"""
|
|
384
|
+
Try to load content from file.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
file_path: Path to file
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
File content as string, or None if file doesn't exist or can't be read
|
|
391
|
+
"""
|
|
392
|
+
try:
|
|
393
|
+
with open(file_path, encoding="utf-8") as f:
|
|
394
|
+
return f.read()
|
|
395
|
+
except (FileNotFoundError, OSError):
|
|
396
|
+
return None
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Default Typst template for sphinx-typst
|
|
2
|
+
// Requirement 8.1: Default template bundled with package
|
|
3
|
+
// Requirement 8.11: Include #outline() in template (not in body)
|
|
4
|
+
// Requirement 7.4: codly package integration for code highlighting
|
|
5
|
+
|
|
6
|
+
// Import codly for code highlighting (Task 4.2.1)
|
|
7
|
+
// Design 3.5: codly is mandatory for all code blocks
|
|
8
|
+
#import "@preview/codly:1.3.0": *
|
|
9
|
+
#import "@preview/codly-languages:0.1.1": *
|
|
10
|
+
|
|
11
|
+
// Import mitex for LaTeX math support (Task 6.1)
|
|
12
|
+
// Design 3.3: mitex for LaTeX math compatibility
|
|
13
|
+
// Requirement 4.1: mitex package integration
|
|
14
|
+
#import "@preview/mitex:0.2.4": *
|
|
15
|
+
|
|
16
|
+
// Import gentle-clues for admonitions (Task 3.4)
|
|
17
|
+
// Design 3.6: gentle-clues for admonition display
|
|
18
|
+
// Requirement 2.8-2.10: Admonition conversion to gentle-clues
|
|
19
|
+
#import "@preview/gentle-clues:1.2.0": *
|
|
20
|
+
|
|
21
|
+
// Initialize codly
|
|
22
|
+
#show: codly-init.with()
|
|
23
|
+
|
|
24
|
+
// Configure codly with codly-languages for comprehensive language support
|
|
25
|
+
#codly(languages: codly-languages)
|
|
26
|
+
|
|
27
|
+
#let project(
|
|
28
|
+
title: "",
|
|
29
|
+
authors: (),
|
|
30
|
+
date: none,
|
|
31
|
+
toctree_maxdepth: 2,
|
|
32
|
+
toctree_numbered: false,
|
|
33
|
+
toctree_caption: "Contents",
|
|
34
|
+
papersize: "a4",
|
|
35
|
+
fontsize: 11pt,
|
|
36
|
+
body
|
|
37
|
+
) = {
|
|
38
|
+
// Document metadata
|
|
39
|
+
set document(title: title, author: authors)
|
|
40
|
+
|
|
41
|
+
// Page setup
|
|
42
|
+
set page(
|
|
43
|
+
paper: papersize,
|
|
44
|
+
numbering: "1",
|
|
45
|
+
number-align: center
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
// Text setup
|
|
49
|
+
set text(size: fontsize, lang: "en")
|
|
50
|
+
|
|
51
|
+
// Heading setup
|
|
52
|
+
set heading(numbering: "1.1")
|
|
53
|
+
|
|
54
|
+
// Title page
|
|
55
|
+
align(center)[
|
|
56
|
+
#text(2em, weight: "bold")[#title]
|
|
57
|
+
#v(1em)
|
|
58
|
+
#text(1.2em)[#authors.join(", ")]
|
|
59
|
+
#v(0.5em)
|
|
60
|
+
#date
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
pagebreak()
|
|
64
|
+
|
|
65
|
+
// Table of Contents
|
|
66
|
+
// Requirement 13.8: #outline() managed at template level, not in body
|
|
67
|
+
// Requirement 8.12, 8.13: toctree options mapped to #outline() parameters
|
|
68
|
+
if toctree_caption != "" [
|
|
69
|
+
#heading(outlined: false)[#toctree_caption]
|
|
70
|
+
]
|
|
71
|
+
outline(
|
|
72
|
+
depth: toctree_maxdepth,
|
|
73
|
+
indent: auto
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
pagebreak()
|
|
77
|
+
|
|
78
|
+
// Document body
|
|
79
|
+
// Requirement 13: body contains #include() directives from toctree
|
|
80
|
+
body
|
|
81
|
+
}
|