ymprint 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.
Files changed (93) hide show
  1. ymprint/__init__.py +2 -0
  2. ymprint/blocks/__init__.py +107 -0
  3. ymprint/blocks/admonition_block.py +42 -0
  4. ymprint/blocks/blockstyles.py +340 -0
  5. ymprint/blocks/code_block.py +25 -0
  6. ymprint/blocks/code_block_styles.py +558 -0
  7. ymprint/blocks/hrule_block.py +26 -0
  8. ymprint/blocks/image_block.py +59 -0
  9. ymprint/blocks/json_block.py +31 -0
  10. ymprint/blocks/matplotfig_block.py +68 -0
  11. ymprint/blocks/page_break_block.py +10 -0
  12. ymprint/blocks/python_block.py +31 -0
  13. ymprint/blocks/quote_block.py +25 -0
  14. ymprint/blocks/slide_block.py +25 -0
  15. ymprint/blocks/spacer_block.py +10 -0
  16. ymprint/cli/config.py +15 -0
  17. ymprint/cli/main.py +144 -0
  18. ymprint/cli/throbber.py +176 -0
  19. ymprint/config/__init__.py +3 -0
  20. ymprint/config/check.ipynb +213 -0
  21. ymprint/config/config_loaders.py +126 -0
  22. ymprint/config/defaults/defaults.ymprint.yml +68 -0
  23. ymprint/config/docstyles.py +115 -0
  24. ymprint/config/doctablestyles.py +115 -0
  25. ymprint/config/doctemplate.py +135 -0
  26. ymprint/config/font_registry.py +84 -0
  27. ymprint/config/fonts/AppleGaramond/AppleGaramond-Bold.ttf +0 -0
  28. ymprint/config/fonts/AppleGaramond/AppleGaramond-BoldItalic.ttf +0 -0
  29. ymprint/config/fonts/AppleGaramond/AppleGaramond-Italic.ttf +0 -0
  30. ymprint/config/fonts/AppleGaramond/AppleGaramond.ttf +0 -0
  31. ymprint/config/fonts/DejaVuSans/DejaVuSans-Bold.ttf +0 -0
  32. ymprint/config/fonts/DejaVuSans/DejaVuSans-BoldOblique.ttf +0 -0
  33. ymprint/config/fonts/DejaVuSans/DejaVuSans-Oblique.ttf +0 -0
  34. ymprint/config/fonts/DejaVuSans/DejaVuSans.ttf +0 -0
  35. ymprint/config/fonts/DejaVuSansCondensed/DejaVuSansCondensed-Bold.ttf +0 -0
  36. ymprint/config/fonts/DejaVuSansCondensed/DejaVuSansCondensed-BoldOblique.ttf +0 -0
  37. ymprint/config/fonts/DejaVuSansCondensed/DejaVuSansCondensed-Oblique.ttf +0 -0
  38. ymprint/config/fonts/DejaVuSansCondensed/DejaVuSansCondensed.ttf +0 -0
  39. ymprint/config/fonts/DejaVuSansMono/DejaVuSansMono-Bold.ttf +0 -0
  40. ymprint/config/fonts/DejaVuSansMono/DejaVuSansMono-Oblique.ttf +0 -0
  41. ymprint/config/fonts/DejaVuSansMono/DejaVuSansMono.ttf +0 -0
  42. ymprint/config/fonts/DejaVuSerif/DejaVuSerif-Bold.ttf +0 -0
  43. ymprint/config/fonts/DejaVuSerif/DejaVuSerif-BoldItalic.ttf +0 -0
  44. ymprint/config/fonts/DejaVuSerif/DejaVuSerif-Italic.ttf +0 -0
  45. ymprint/config/fonts/DejaVuSerif/DejaVuSerif.ttf +0 -0
  46. ymprint/config/fonts/DejaVuSerifCondensed/DejaVuSerifCondensed-Bold.ttf +0 -0
  47. ymprint/config/fonts/DejaVuSerifCondensed/DejaVuSerifCondensed-BoldItalic.ttf +0 -0
  48. ymprint/config/fonts/DejaVuSerifCondensed/DejaVuSerifCondensed-Italic.ttf +0 -0
  49. ymprint/config/fonts/DejaVuSerifCondensed/DejaVuSerifCondensed.ttf +0 -0
  50. ymprint/config/fonts/Inter/Inter-Bold.ttf +0 -0
  51. ymprint/config/fonts/Inter/Inter-BoldItalic.ttf +0 -0
  52. ymprint/config/fonts/Inter/Inter-Italic.ttf +0 -0
  53. ymprint/config/fonts/Inter/Inter.ttf +0 -0
  54. ymprint/config/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
  55. ymprint/config/fonts/Montserrat/Montserrat-BoldItalic.ttf +0 -0
  56. ymprint/config/fonts/Montserrat/Montserrat-Italic.ttf +0 -0
  57. ymprint/config/fonts/Montserrat/Montserrat.ttf +0 -0
  58. ymprint/config/fonts/NotoSans/NotoSans-Bold.ttf +0 -0
  59. ymprint/config/fonts/NotoSans/NotoSans-BoldItalic.ttf +0 -0
  60. ymprint/config/fonts/NotoSans/NotoSans-Italic.ttf +0 -0
  61. ymprint/config/fonts/NotoSans/NotoSans.ttf +0 -0
  62. ymprint/config/fonts/NotoSerif/NotoSerif-Bold.ttf +0 -0
  63. ymprint/config/fonts/NotoSerif/NotoSerif-BoldItalic.ttf +0 -0
  64. ymprint/config/fonts/NotoSerif/NotoSerif-Italic.ttf +0 -0
  65. ymprint/config/fonts/NotoSerif/NotoSerif.ttf +0 -0
  66. ymprint/config/fonts/Playfair/Playfair-Bold.ttf +0 -0
  67. ymprint/config/fonts/Playfair/Playfair-BoldItalic.ttf +0 -0
  68. ymprint/config/fonts/Playfair/Playfair-Italic.ttf +0 -0
  69. ymprint/config/fonts/Playfair/Playfair.ttf +0 -0
  70. ymprint/config/fonts/Poppins/Poppins-Bold.ttf +0 -0
  71. ymprint/config/fonts/Poppins/Poppins-BoldItalic.ttf +0 -0
  72. ymprint/config/fonts/Poppins/Poppins-Italic.ttf +0 -0
  73. ymprint/config/fonts/Poppins/Poppins.ttf +0 -0
  74. ymprint/config/fonts/Roboto/Roboto-Bold.ttf +0 -0
  75. ymprint/config/fonts/Roboto/Roboto-BoldItalic.ttf +0 -0
  76. ymprint/config/fonts/Roboto/Roboto-Italic.ttf +0 -0
  77. ymprint/config/fonts/Roboto/Roboto.ttf +0 -0
  78. ymprint/config/helpers.py +36 -0
  79. ymprint/config/pdf_fill_forms.py +0 -0
  80. ymprint/config/pdf_postprocessing.py +139 -0
  81. ymprint/content_checks.py +141 -0
  82. ymprint/content_converters.py +107 -0
  83. ymprint/context_builder.py +70 -0
  84. ymprint/exceptions.py +2 -0
  85. ymprint/markdown/inline.py +33 -0
  86. ymprint/notes.yml +20 -0
  87. ymprint/report_reader.py +84 -0
  88. ymprint/story_builder.py +72 -0
  89. ymprint/yaml_loader.py +13 -0
  90. ymprint-0.1.0.dist-info/METADATA +150 -0
  91. ymprint-0.1.0.dist-info/RECORD +93 -0
  92. ymprint-0.1.0.dist-info/WHEEL +4 -0
  93. ymprint-0.1.0.dist-info/entry_points.txt +3 -0
ymprint/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ from ymprint import config
2
+ from ymprint import blocks
@@ -0,0 +1,107 @@
1
+ import re
2
+
3
+ from typing import Callable, Optional, Union, TypeAlias, Any
4
+
5
+ from reportlab.platypus import (
6
+ Paragraph,
7
+ Spacer,
8
+ Table,
9
+ Image,
10
+ HRFlowable,
11
+ KeepTogether,
12
+ )
13
+ from reportlab.lib.units import mm
14
+ from ymprint.config.docstyles import ReportStyles
15
+ from ..content_checks import check_for_variable
16
+
17
+ RLFlowables: TypeAlias = Union[Paragraph, Spacer, Table, KeepTogether, Image]
18
+
19
+ YAML_Values: TypeAlias =Union[str, list, dict, float, int, None]
20
+
21
+ # TODO: Create a block registration function and a singleton block registry
22
+
23
+ class BlockExistsError(Exception):
24
+ pass
25
+
26
+ def create_block_registry() -> tuple[Callable, Callable, Callable]:
27
+ """
28
+ Creates the block registry
29
+ """
30
+ _BLOCK_REGISTRY = {}
31
+
32
+ def register_block(block_code: str, block_convert: Callable) -> None:
33
+ """
34
+ Returns None. Adds a new block to the block registry.
35
+
36
+ 'block_code': a str of the form '_{code}' where 'code' is an alphanumeric code used
37
+ to identify the block.
38
+ 'block_convert':a function with the following signature:
39
+ my_func(obj: dict, context: dict) -> list[Flowable]
40
+
41
+ Where:
42
+ obj: a dict which has a key that starts with the block code and a value
43
+ which is a user-defined data structure that contains the data needed
44
+ to generate the block as a ReportLab Flowable.
45
+ context: a dict that contains all of the internal state of this program
46
+ at the time the conversion is executed. This will get passed to your
47
+ function automatically and gives your conversion function access to
48
+ anything and everything it needs to render your custom block.
49
+ Feel free to explore the context dict by using a print(context) call
50
+ in your function.
51
+
52
+ Return:
53
+ A list of ReportLab Flowable from the reportlab.platypus module. Your custom
54
+ block may be just one flowable (like a custom-populated table) or it can
55
+ be a list of many flowable.
56
+ """
57
+ _BLOCK_REGISTRY.update({block_code: block_convert})
58
+
59
+ def list_blocks():
60
+ return list(_BLOCK_REGISTRY.keys())
61
+
62
+ def get_block_callable(block_code: str) -> Optional[Callable]:
63
+ return _BLOCK_REGISTRY.get(block_code)
64
+
65
+ return list_blocks, get_block_callable, register_block
66
+
67
+ list_blocks, get_block_callable, register_block = create_block_registry()
68
+
69
+ def convert_blocks(block_key: str, block_value: YAML_Values, context: dict) -> list[RLFlowables]:
70
+ block_code_pattern = re.compile(r"(^_[a-zA-Z0-9]+)")
71
+ matches = block_code_pattern.match(block_key)
72
+ if matches is not None:
73
+ block_code = matches.groups()[0]
74
+ else:
75
+ raise ValueError(f"Block code not found within block key: {block_key=}")
76
+ block_converter = get_block_callable(block_code)
77
+ block_value_w_python_objects = retrieve_block_variables(block_value, context)
78
+ flowables = block_converter(block_key, block_value_w_python_objects, context)
79
+ return flowables
80
+
81
+
82
+ def retrieve_block_variables(block_value: YAML_Values, context: dict) -> YAML_Values:
83
+ """
84
+ Returns 'block_value' but with list values or dictionary values that have
85
+ the "$VAR" syntax substituted with the object values
86
+ """
87
+ if isinstance(block_value, str) and check_for_variable(block_value, context):
88
+ var_name = get_variable_name(block_value)
89
+ return context['vars'].get(var_name, block_value)
90
+ elif isinstance(block_value, list):
91
+ acc = []
92
+ for elem in block_value:
93
+ new_elem = retrieve_block_variables(elem, context)
94
+ acc.append(new_elem)
95
+ return acc
96
+ elif isinstance(block_value, dict):
97
+ acc = {}
98
+ for k, v in block_value.items():
99
+ new_v = retrieve_block_variables(v, context)
100
+ acc.update({k: new_v})
101
+ return acc
102
+ else:
103
+ return block_value
104
+
105
+
106
+ def get_variable_name(var_string: str) -> str:
107
+ return var_string.lstrip('$')
@@ -0,0 +1,42 @@
1
+ from reportlab.platypus import Table, Paragraph, Spacer, KeepTogether
2
+ from . import register_block
3
+ from typing import Callable, Any
4
+ from . import blockstyles
5
+
6
+ def generate_admonition_block(kind: str) -> Callable:
7
+ """
8
+ Returns a callable to render that particular admonition type
9
+ """
10
+
11
+
12
+ def convert_admonition_block(block_key: str, block_value: Any, context: dict) -> list[KeepTogether | Spacer]:
13
+ # Need an admonition block style or style modification
14
+ available_width = context['frames']['all_pages']['width']
15
+ text_spacing = context['styles']['ymprint'].body.spacing
16
+ text_size = context['styles']['ymprint'].body.size
17
+ space_around = text_spacing * text_size
18
+ width_ratio = 0.8
19
+ block_width = width_ratio * available_width
20
+ value = block_value
21
+ tablestyle = blockstyles.get_table_style(kind)
22
+ body_textstyle = blockstyles.get_text_styles().get(f'admonition_{kind}_body')
23
+ title_textstyle = blockstyles.get_text_styles().get(f'admonition_{kind}_title')
24
+ notice = Paragraph(text=blockstyles.admonition_title_text(kind), style=title_textstyle)
25
+ content = Paragraph(text=value, style=body_textstyle)
26
+ table = Table(
27
+ data=[[notice], [content]],
28
+ colWidths=[block_width],
29
+ style=tablestyle,
30
+ spaceBefore=space_around,
31
+ spaceAfter=space_around
32
+ )
33
+ return [KeepTogether(table), Spacer(1, 10)]
34
+
35
+ return convert_admonition_block
36
+
37
+
38
+ register_block("_info", generate_admonition_block("info"))
39
+ register_block("_warning", generate_admonition_block("warning"))
40
+ register_block("_danger", generate_admonition_block("danger"))
41
+ register_block("_tip", generate_admonition_block("tip"))
42
+ register_block("_note", generate_admonition_block("note"))
@@ -0,0 +1,340 @@
1
+ """
2
+ styles.py
3
+ ReportLab styles for markdown-to-PDF report generation.
4
+
5
+ Covers:
6
+ - Admonition blocks: Info, Warning, Danger, Tip, Note
7
+ - Image blocks (image + caption)
8
+
9
+ Usage:
10
+ from styles import get_styles, get_table_style
11
+ styles = get_styles()
12
+ ts = get_table_style("info")
13
+ """
14
+
15
+ from reportlab.lib import colors
16
+ from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
17
+ pt = 1 # ReportLab's internal unit; 1 pt == 1 unit
18
+ from reportlab.platypus import TableStyle
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # Design tokens
22
+ # ---------------------------------------------------------------------------
23
+
24
+ # Base font
25
+ FONT_NORMAL = "Helvetica"
26
+ FONT_BOLD = "Helvetica-Bold"
27
+ FONT_ITALIC = "Helvetica-Oblique"
28
+ FONT_MONO = "Courier"
29
+
30
+ # Base sizes
31
+ BODY_FONT_SIZE = 10
32
+ LABEL_FONT_SIZE = 8
33
+ CAPTION_FONT_SIZE = 9
34
+
35
+ # Spacing
36
+ CELL_PADDING_H = 10 # horizontal padding inside admonition cells (pt)
37
+ CELL_PADDING_V = 8 # vertical padding inside admonition cells (pt)
38
+ BORDER_RADIUS = 4 # not directly used by ReportLab, but documents intent
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # Colour palette
42
+ # ---------------------------------------------------------------------------
43
+
44
+ class Palette:
45
+ # Neutrals
46
+ WHITE = colors.HexColor("#FFFFFF")
47
+ BODY_TEXT = colors.HexColor("#1A1A2E")
48
+ CAPTION_TEXT = colors.HexColor("#555555")
49
+ TABLE_BORDER = colors.HexColor("#CCCCCC")
50
+
51
+ # Info (blue)
52
+ INFO_BG = colors.HexColor("#EFF6FF")
53
+ INFO_BORDER = colors.HexColor("#3B82F6")
54
+ INFO_TITLE = colors.HexColor("#1D4ED8")
55
+ INFO_TEXT = colors.HexColor("#1E3A5F")
56
+ INFO_ICON = "ℹ"
57
+
58
+ # Warning (amber)
59
+ WARNING_BG = colors.HexColor("#FFFBEB")
60
+ WARNING_BORDER = colors.HexColor("#F59E0B")
61
+ WARNING_TITLE = colors.HexColor("#B45309")
62
+ WARNING_TEXT = colors.HexColor("#4D3A00")
63
+ WARNING_ICON = "⚠"
64
+
65
+ # Danger (red)
66
+ DANGER_BG = colors.HexColor("#FEF2F2")
67
+ DANGER_BORDER = colors.HexColor("#EF4444")
68
+ DANGER_TITLE = colors.HexColor("#B91C1C")
69
+ DANGER_TEXT = colors.HexColor("#4D0000")
70
+ DANGER_ICON = "✖"
71
+
72
+ # Tip (green)
73
+ TIP_BG = colors.HexColor("#F0FDF4")
74
+ TIP_BORDER = colors.HexColor("#22C55E")
75
+ TIP_TITLE = colors.HexColor("#15803D")
76
+ TIP_TEXT = colors.HexColor("#14532D")
77
+ TIP_ICON = "✔"
78
+
79
+ # Note (purple)
80
+ NOTE_BG = colors.HexColor("#FAF5FF")
81
+ NOTE_BORDER = colors.HexColor("#A855F7")
82
+ NOTE_TITLE = colors.HexColor("#7E22CE")
83
+ NOTE_TEXT = colors.HexColor("#3B0764")
84
+ NOTE_ICON = "✎"
85
+
86
+ # Blockquote (neutral slate)
87
+ BLOCKQUOTE_BG = colors.HexColor("#F8F9FA")
88
+ BLOCKQUOTE_BORDER = colors.HexColor("#444444") # slate-400
89
+ BLOCKQUOTE_TEXT = colors.HexColor("#374151") # slate-700
90
+ BLOCKQUOTE_ATTRIBUTION = colors.HexColor("#6B7280") # slate-500
91
+
92
+ # ---------------------------------------------------------------------------
93
+ # Paragraph styles
94
+ # ---------------------------------------------------------------------------
95
+
96
+ def get_text_styles():
97
+ """
98
+ Return a dict of ParagraphStyle objects keyed by name.
99
+
100
+ Keys
101
+ ----
102
+ admonition_<kind>_title — bold label line (kind: info/warning/danger/tip/note)
103
+ admonition_<kind>_body — body text inside the block
104
+ image_caption — caption below an image
105
+ """
106
+ base = getSampleStyleSheet()
107
+
108
+ # Shared defaults for admonition body text
109
+ _admonition_body_defaults = dict(
110
+ fontName=FONT_NORMAL,
111
+ fontSize=BODY_FONT_SIZE,
112
+ leading=BODY_FONT_SIZE * 1.4,
113
+ spaceAfter=0,
114
+ spaceBefore=0,
115
+ )
116
+
117
+ # Shared defaults for admonition title / label
118
+ _admonition_title_defaults = dict(
119
+ fontName=FONT_BOLD,
120
+ fontSize=LABEL_FONT_SIZE,
121
+ leading=LABEL_FONT_SIZE * 1.3,
122
+ spaceAfter=3,
123
+ spaceBefore=0,
124
+ textTransform="uppercase",
125
+ )
126
+
127
+ blocks = {
128
+ "info": (Palette.INFO_TITLE, Palette.INFO_TEXT),
129
+ "warning": (Palette.WARNING_TITLE, Palette.WARNING_TEXT),
130
+ "danger": (Palette.DANGER_TITLE, Palette.DANGER_TEXT),
131
+ "tip": (Palette.TIP_TITLE, Palette.TIP_TEXT),
132
+ "note": (Palette.NOTE_TITLE, Palette.NOTE_TEXT),
133
+ }
134
+
135
+ styles = {}
136
+
137
+ for kind, (title_color, body_color) in blocks.items():
138
+ styles[f"admonition_{kind}_title"] = ParagraphStyle(
139
+ name=f"admonition_{kind}_title",
140
+ textColor=title_color,
141
+ **_admonition_title_defaults,
142
+ )
143
+ styles[f"admonition_{kind}_body"] = ParagraphStyle(
144
+ name=f"admonition_{kind}_body",
145
+ textColor=body_color,
146
+ **_admonition_body_defaults,
147
+ )
148
+
149
+ # Blockquote — quote body (italic, indented feel via left padding on the table)
150
+ styles["blockquote_body"] = ParagraphStyle(
151
+ name="blockquote_body",
152
+ fontName=FONT_ITALIC,
153
+ fontSize=BODY_FONT_SIZE + 1, # slightly larger feels more editorial
154
+ leading=(BODY_FONT_SIZE + 1) * 1.5, # generous leading for readability
155
+ textColor=Palette.BLOCKQUOTE_TEXT,
156
+ spaceAfter=0,
157
+ spaceBefore=0,
158
+ )
159
+
160
+ # Blockquote — optional attribution line ("— Author Name")
161
+ styles["blockquote_attribution"] = ParagraphStyle(
162
+ name="blockquote_attribution",
163
+ fontName=FONT_NORMAL,
164
+ fontSize=BODY_FONT_SIZE - 1,
165
+ leading=(BODY_FONT_SIZE - 1) * 1.4,
166
+ textColor=Palette.BLOCKQUOTE_ATTRIBUTION,
167
+ spaceAfter=0,
168
+ spaceBefore=6,
169
+ alignment=2, # right-align attribution
170
+ )
171
+
172
+
173
+ # Image caption
174
+ styles["image_caption"] = ParagraphStyle(
175
+ name="image_caption",
176
+ fontName=FONT_ITALIC,
177
+ fontSize=CAPTION_FONT_SIZE,
178
+ leading=CAPTION_FONT_SIZE * 1.4,
179
+ textColor=Palette.CAPTION_TEXT,
180
+ alignment=1, # centre
181
+ spaceBefore=4,
182
+ spaceAfter=0,
183
+ )
184
+
185
+ return styles
186
+
187
+
188
+ # ---------------------------------------------------------------------------
189
+ # Table styles
190
+ # ---------------------------------------------------------------------------
191
+
192
+ # Left-accent border width (pt)
193
+ ACCENT_WIDTH = 4
194
+
195
+ def _admonition_table_style(bg: colors.Color,
196
+ border: colors.Color) -> TableStyle:
197
+ """
198
+ Build a TableStyle for a single-column admonition table.
199
+
200
+ Layout assumption:
201
+ Table has 1 column and 2 rows:
202
+ row 0 — title (icon + label)
203
+ row 1 — body text
204
+ """
205
+ return TableStyle([
206
+ # Background
207
+ ("BACKGROUND", (0, 0), (-1, -1), bg),
208
+
209
+ # Left accent bar
210
+ ("LINEBEFORE", (0, 0), (0, -1), ACCENT_WIDTH, border),
211
+
212
+ # Outer border (thin, same hue as accent)
213
+ ("BOX", (0, 0), (-1, -1), 0.5, border),
214
+
215
+ # No internal grid lines
216
+ ("INNERGRID", (0, 0), (-1, -1), 0, colors.transparent),
217
+
218
+ # Padding
219
+ ("LEFTPADDING", (0, 0), (-1, -1), CELL_PADDING_H),
220
+ ("RIGHTPADDING", (0, 0), (-1, -1), CELL_PADDING_H),
221
+ ("TOPPADDING", (0, 0), (0, 0), CELL_PADDING_V), # title row top
222
+ ("BOTTOMPADDING", (0, 0), (0, 0), 3), # title row bottom (tight)
223
+ ("TOPPADDING", (0, 1), (0, -1), 4), # body row top
224
+ ("BOTTOMPADDING", (0, 1), (0, -1), CELL_PADDING_V), # body row bottom
225
+
226
+ # Vertical alignment
227
+ ("VALIGN", (0, 0), (-1, -1), "TOP"),
228
+ ("ROUNDEDCORNERS", [4, 4, 4, 4])
229
+ # ROUNDEDCORNERS, [tl, tr, bl, br]
230
+ ])
231
+
232
+
233
+ # Blockquote table style
234
+ # Layout: 1 column, 1–2 rows
235
+ # row 0 — quote body text (required)
236
+ # row 1 — attribution (optional; omit the row if no attribution)
237
+ BLOCKQUOTE_ACCENT_WIDTH = 5 # slightly thicker than admonition bars
238
+
239
+ BLOCKQUOTE_TABLE_STYLE = TableStyle([
240
+ # Subtle background tint
241
+ ("BACKGROUND", (0, 0), (-1, -1), Palette.BLOCKQUOTE_BG),
242
+
243
+ # Thick left accent bar — the defining visual of a blockquote
244
+ ("LINEBEFORE", (0, 0), (0, -1), BLOCKQUOTE_ACCENT_WIDTH, Palette.BLOCKQUOTE_BORDER),
245
+
246
+ # No outer box, no grid — keep it clean and "open"
247
+ ("BOX", (0, 0), (-1, -1), 0, colors.transparent),
248
+ ("INNERGRID", (0, 0), (-1, -1), 0, colors.transparent),
249
+
250
+ # Generous horizontal padding to offset the accent bar visually
251
+ ("LEFTPADDING", (0, 0), (-1, -1), 14),
252
+ ("RIGHTPADDING", (0, 0), (-1, -1), 12),
253
+
254
+ # Vertical padding: roomy on the quote body
255
+ ("TOPPADDING", (0, 0), (0, 0), 10),
256
+ ("BOTTOMPADDING", (0, 0), (0, 0), 6),
257
+
258
+ # Attribution row (row 1) sits tight to the quote
259
+ ("TOPPADDING", (0, 1), (0, -1), 0),
260
+ ("BOTTOMPADDING", (0, 1), (0, -1), 10),
261
+ ("ROUNDEDCORNERS", [4, 4, 4, 4]),
262
+
263
+ ("VALIGN", (0, 0), (-1, -1), "TOP"),
264
+ ])
265
+
266
+
267
+ # Pre-built table styles for each admonition kind
268
+ ADMONITION_TABLE_STYLES: dict[str, TableStyle] = {
269
+ "info": _admonition_table_style(Palette.INFO_BG, Palette.INFO_BORDER),
270
+ "warning": _admonition_table_style(Palette.WARNING_BG, Palette.WARNING_BORDER),
271
+ "danger": _admonition_table_style(Palette.DANGER_BG, Palette.DANGER_BORDER),
272
+ "tip": _admonition_table_style(Palette.TIP_BG, Palette.TIP_BORDER),
273
+ "note": _admonition_table_style(Palette.NOTE_BG, Palette.NOTE_BORDER),
274
+ }
275
+
276
+ # Image/caption table style
277
+ IMAGE_TABLE_STYLE = TableStyle([
278
+ # No background, no border — image block is decoration-free
279
+ ("BACKGROUND", (0, 0), (-1, -1), colors.transparent),
280
+ ("BOX", (0, 0), (-1, -1), 0, colors.transparent),
281
+ ("INNERGRID", (0, 0), (-1, -1), 0, colors.transparent),
282
+
283
+ # Padding
284
+ ("LEFTPADDING", (0, 0), (-1, -1), 0),
285
+ ("RIGHTPADDING", (0, 0), (-1, -1), 0),
286
+ ("TOPPADDING", (0, 0), (-1, -1), 0),
287
+ ("BOTTOMPADDING",(0, 0), (0, 0), 4), # gap between image and caption
288
+ ("BOTTOMPADDING",(0, 1), (0, -1), 0),
289
+
290
+ # Centre both image and caption horizontally
291
+ ("ALIGN", (0, 0), (-1, -1), "CENTER"),
292
+ ("VALIGN", (0, 0), (-1, -1), "TOP"),
293
+ ])
294
+
295
+
296
+ def get_table_style(block_type: str) -> TableStyle:
297
+ """
298
+ Return the TableStyle for a given block type.
299
+
300
+ Parameters
301
+ ----------
302
+ block_type : str
303
+ One of: "info", "warning", "danger", "tip", "note", "image"
304
+
305
+ Returns
306
+ -------
307
+ TableStyle
308
+ """
309
+ if block_type == "image":
310
+ return IMAGE_TABLE_STYLE
311
+ if block_type == "blockquote":
312
+ return BLOCKQUOTE_TABLE_STYLE
313
+ try:
314
+ return ADMONITION_TABLE_STYLES[block_type]
315
+ except KeyError:
316
+ valid = list(ADMONITION_TABLE_STYLES) + ["blockquote", "image"]
317
+ raise ValueError(
318
+ f"Unknown block type '{block_type}'. Choose from: {valid}"
319
+ )
320
+
321
+
322
+ # ---------------------------------------------------------------------------
323
+ # Icon / label helpers
324
+ # ---------------------------------------------------------------------------
325
+
326
+ ADMONITION_META: dict[str, dict] = {
327
+ "info": {"icon": Palette.INFO_ICON, "label": "Info"},
328
+ "warning": {"icon": Palette.WARNING_ICON, "label": "Warning"},
329
+ "danger": {"icon": Palette.DANGER_ICON, "label": "Danger"},
330
+ "tip": {"icon": Palette.TIP_ICON, "label": "Tip"},
331
+ "note": {"icon": Palette.NOTE_ICON, "label": "Note"},
332
+ }
333
+
334
+
335
+ def admonition_title_text(kind: str, label: str | None = None) -> str:
336
+ """Return '⚠ WARNING' style label string for the title row."""
337
+ meta = ADMONITION_META[kind]
338
+ if label is not None:
339
+ return f"{meta['icon']} {label.upper()}"
340
+ return f"{meta['icon']} {meta['label'].upper()}"
@@ -0,0 +1,25 @@
1
+ from reportlab.platypus import Table, KeepTogether
2
+ from . import register_block
3
+ from .code_block_styles import generic_code_block
4
+ from typing import Callable
5
+
6
+
7
+
8
+ def convert_code_block(block_key: str, block_value: dict, context: dict) -> list[KeepTogether]:
9
+ # Need an admonition block style or style modification
10
+ source = block_value['source']
11
+ caption = block_value.get('caption')
12
+ line_numbers = block_value.get('line_numbers')
13
+ width_ratio = block_value.get('width_ratio', 0.75)
14
+ available_width = context['frames']['all_pages']['width']
15
+ text_spacing = context['styles']['ymprint'].body.spacing
16
+ text_size = context['styles']['ymprint'].body.size
17
+ space_around = text_spacing * text_size
18
+ namespace = block_value.get('namespace')
19
+ code_block = generic_code_block(source, available_width * width_ratio, context, caption=caption, show_line_numbers=line_numbers)
20
+ code_block.spaceBefore = space_around
21
+ code_block.spaceAfter = space_around
22
+ return [KeepTogether(code_block)]
23
+
24
+
25
+ register_block("_code", convert_code_block)