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.
- ymprint/__init__.py +2 -0
- ymprint/blocks/__init__.py +107 -0
- ymprint/blocks/admonition_block.py +42 -0
- ymprint/blocks/blockstyles.py +340 -0
- ymprint/blocks/code_block.py +25 -0
- ymprint/blocks/code_block_styles.py +558 -0
- ymprint/blocks/hrule_block.py +26 -0
- ymprint/blocks/image_block.py +59 -0
- ymprint/blocks/json_block.py +31 -0
- ymprint/blocks/matplotfig_block.py +68 -0
- ymprint/blocks/page_break_block.py +10 -0
- ymprint/blocks/python_block.py +31 -0
- ymprint/blocks/quote_block.py +25 -0
- ymprint/blocks/slide_block.py +25 -0
- ymprint/blocks/spacer_block.py +10 -0
- ymprint/cli/config.py +15 -0
- ymprint/cli/main.py +144 -0
- ymprint/cli/throbber.py +176 -0
- ymprint/config/__init__.py +3 -0
- ymprint/config/check.ipynb +213 -0
- ymprint/config/config_loaders.py +126 -0
- ymprint/config/defaults/defaults.ymprint.yml +68 -0
- ymprint/config/docstyles.py +115 -0
- ymprint/config/doctablestyles.py +115 -0
- ymprint/config/doctemplate.py +135 -0
- ymprint/config/font_registry.py +84 -0
- ymprint/config/fonts/AppleGaramond/AppleGaramond-Bold.ttf +0 -0
- ymprint/config/fonts/AppleGaramond/AppleGaramond-BoldItalic.ttf +0 -0
- ymprint/config/fonts/AppleGaramond/AppleGaramond-Italic.ttf +0 -0
- ymprint/config/fonts/AppleGaramond/AppleGaramond.ttf +0 -0
- ymprint/config/fonts/DejaVuSans/DejaVuSans-Bold.ttf +0 -0
- ymprint/config/fonts/DejaVuSans/DejaVuSans-BoldOblique.ttf +0 -0
- ymprint/config/fonts/DejaVuSans/DejaVuSans-Oblique.ttf +0 -0
- ymprint/config/fonts/DejaVuSans/DejaVuSans.ttf +0 -0
- ymprint/config/fonts/DejaVuSansCondensed/DejaVuSansCondensed-Bold.ttf +0 -0
- ymprint/config/fonts/DejaVuSansCondensed/DejaVuSansCondensed-BoldOblique.ttf +0 -0
- ymprint/config/fonts/DejaVuSansCondensed/DejaVuSansCondensed-Oblique.ttf +0 -0
- ymprint/config/fonts/DejaVuSansCondensed/DejaVuSansCondensed.ttf +0 -0
- ymprint/config/fonts/DejaVuSansMono/DejaVuSansMono-Bold.ttf +0 -0
- ymprint/config/fonts/DejaVuSansMono/DejaVuSansMono-Oblique.ttf +0 -0
- ymprint/config/fonts/DejaVuSansMono/DejaVuSansMono.ttf +0 -0
- ymprint/config/fonts/DejaVuSerif/DejaVuSerif-Bold.ttf +0 -0
- ymprint/config/fonts/DejaVuSerif/DejaVuSerif-BoldItalic.ttf +0 -0
- ymprint/config/fonts/DejaVuSerif/DejaVuSerif-Italic.ttf +0 -0
- ymprint/config/fonts/DejaVuSerif/DejaVuSerif.ttf +0 -0
- ymprint/config/fonts/DejaVuSerifCondensed/DejaVuSerifCondensed-Bold.ttf +0 -0
- ymprint/config/fonts/DejaVuSerifCondensed/DejaVuSerifCondensed-BoldItalic.ttf +0 -0
- ymprint/config/fonts/DejaVuSerifCondensed/DejaVuSerifCondensed-Italic.ttf +0 -0
- ymprint/config/fonts/DejaVuSerifCondensed/DejaVuSerifCondensed.ttf +0 -0
- ymprint/config/fonts/Inter/Inter-Bold.ttf +0 -0
- ymprint/config/fonts/Inter/Inter-BoldItalic.ttf +0 -0
- ymprint/config/fonts/Inter/Inter-Italic.ttf +0 -0
- ymprint/config/fonts/Inter/Inter.ttf +0 -0
- ymprint/config/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
- ymprint/config/fonts/Montserrat/Montserrat-BoldItalic.ttf +0 -0
- ymprint/config/fonts/Montserrat/Montserrat-Italic.ttf +0 -0
- ymprint/config/fonts/Montserrat/Montserrat.ttf +0 -0
- ymprint/config/fonts/NotoSans/NotoSans-Bold.ttf +0 -0
- ymprint/config/fonts/NotoSans/NotoSans-BoldItalic.ttf +0 -0
- ymprint/config/fonts/NotoSans/NotoSans-Italic.ttf +0 -0
- ymprint/config/fonts/NotoSans/NotoSans.ttf +0 -0
- ymprint/config/fonts/NotoSerif/NotoSerif-Bold.ttf +0 -0
- ymprint/config/fonts/NotoSerif/NotoSerif-BoldItalic.ttf +0 -0
- ymprint/config/fonts/NotoSerif/NotoSerif-Italic.ttf +0 -0
- ymprint/config/fonts/NotoSerif/NotoSerif.ttf +0 -0
- ymprint/config/fonts/Playfair/Playfair-Bold.ttf +0 -0
- ymprint/config/fonts/Playfair/Playfair-BoldItalic.ttf +0 -0
- ymprint/config/fonts/Playfair/Playfair-Italic.ttf +0 -0
- ymprint/config/fonts/Playfair/Playfair.ttf +0 -0
- ymprint/config/fonts/Poppins/Poppins-Bold.ttf +0 -0
- ymprint/config/fonts/Poppins/Poppins-BoldItalic.ttf +0 -0
- ymprint/config/fonts/Poppins/Poppins-Italic.ttf +0 -0
- ymprint/config/fonts/Poppins/Poppins.ttf +0 -0
- ymprint/config/fonts/Roboto/Roboto-Bold.ttf +0 -0
- ymprint/config/fonts/Roboto/Roboto-BoldItalic.ttf +0 -0
- ymprint/config/fonts/Roboto/Roboto-Italic.ttf +0 -0
- ymprint/config/fonts/Roboto/Roboto.ttf +0 -0
- ymprint/config/helpers.py +36 -0
- ymprint/config/pdf_fill_forms.py +0 -0
- ymprint/config/pdf_postprocessing.py +139 -0
- ymprint/content_checks.py +141 -0
- ymprint/content_converters.py +107 -0
- ymprint/context_builder.py +70 -0
- ymprint/exceptions.py +2 -0
- ymprint/markdown/inline.py +33 -0
- ymprint/notes.yml +20 -0
- ymprint/report_reader.py +84 -0
- ymprint/story_builder.py +72 -0
- ymprint/yaml_loader.py +13 -0
- ymprint-0.1.0.dist-info/METADATA +150 -0
- ymprint-0.1.0.dist-info/RECORD +93 -0
- ymprint-0.1.0.dist-info/WHEEL +4 -0
- ymprint-0.1.0.dist-info/entry_points.txt +3 -0
ymprint/__init__.py
ADDED
|
@@ -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)
|