Coverage for liitos/figures.py: 97.85%
73 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-14 21:47:10 +00:00
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-14 21:47:10 +00:00
1"""Apply any scale command to subsequent figure environment.
3Implementation Note: The not a number (NAN) marker is used to indicate absence of scale command.
4"""
6from collections.abc import Iterable
7from enum import Enum
8import math
9from typing import Union
11from liitos import log
13Modus = Enum('Modus', 'COPY SCALE')
14NAN = float('nan')
16EQ = '='
17SP = ' '
18SCALE_START_TRIGGER_STARTSWITH = r'\scale'
19BARE_GRAPHICS_START_STARTSWITH = r'\includegraphics{'
20WRAPPED_GRAPHICS_START_IN = r'\pandocbounded{\includegraphics'
21EVEN_MORE_SO = r'\pandocbounded{\includegraphics[keepaspectratio,alt={' # HACK A DID ACK
24def filter_seek_scale(line: str, slot: int, modus: Modus, rescale: float, outgoing: list[str]) -> tuple[Modus, float]:
25 r"""Filter line, seek for a scale command, and return updated mnodus, rescale pair.
27 Examples:
29 >>> o = []
30 >>> line = SCALE_START_TRIGGER_STARTSWITH + EQ # r'\scale='
31 >>> m, r = filter_seek_scale(line, 0, Modus.COPY, NAN, o)
32 >>> assert not o
33 >>> assert m == Modus.SCALE
34 >>> assert math.isnan(r)
36 >>> o = []
37 >>> m, r = filter_seek_scale('foo', 0, Modus.COPY, NAN, o)
38 >>> assert o == ['foo']
39 >>> assert m == Modus.COPY
40 >>> assert math.isnan(r)
42 >>> o = []
43 >>> m, r = filter_seek_scale(r'\scale=80\%', 0, Modus.COPY, NAN, o)
44 >>> assert not o
45 >>> assert m == Modus.SCALE
46 >>> assert r == 0.8
48 >>> o = []
49 >>> m, r = filter_seek_scale(r'\scale 80\%', 0, Modus.COPY, NAN, o)
50 >>> assert not o
51 >>> assert m == Modus.SCALE
52 >>> assert r == 0.8
54 >>> o = []
55 >>> m, r = filter_seek_scale(r'\scale = 0.8', 0, Modus.COPY, NAN, o)
56 >>> assert not o
57 >>> assert m == Modus.SCALE
58 >>> assert r == 0.8
59 """
60 if any(line.startswith(SCALE_START_TRIGGER_STARTSWITH + other) for other in (EQ, SP)):
61 log.info(f'trigger a scale mod for the next figure environment at line #{slot + 1}|{line}')
62 modus = Modus.SCALE
63 scale = line # only for reporting will not pass the filter
64 try:
65 # \scale = 75\% --> 75\%
66 # \scale 75\% --> 75\%
67 sca = scale.split(EQ, 1)[1].strip() if EQ in scale else SP.join(scale.split()).split(SP, 1)[1].strip()
68 rescale = float(sca.replace(r'\%', '')) / 100 if r'\%' in sca else float(sca)
69 except Exception as err:
70 log.error(f'failed to parse scale value from {line.strip()} with err: {err}')
71 else:
72 outgoing.append(line)
74 return modus, rescale
77def filter_seek_figure(line: str, slot: int, modus: Modus, rescale: float, outgoing: list[str]) -> tuple[Modus, float]:
78 r"""Filter line, seek for a figure, rescale if applicable, and return updated mnodus, rescale pair.
80 Examples:
82 >>> o = []
83 >>> m, r = filter_seek_figure(r'\includegraphics{', 0, Modus.COPY, NAN, o)
84 >>> assert o == [r'\includegraphics{']
85 >>> assert m == Modus.COPY
86 >>> assert math.isnan(r)
88 >>> o = []
89 >>> m, r = filter_seek_figure('foo', 0, Modus.COPY, 0.8, o)
90 >>> assert o == ['foo']
91 >>> assert m == Modus.COPY
92 >>> assert r == 0.8
94 >>> o = []
95 >>> rescale = 0.8
96 >>> m, r = filter_seek_figure(r'\pandocbounded{\includegraphics', 0, Modus.COPY, rescale, o)
97 >>> assert o[0].startswith(r'\pandocbounded{\includegraphics')
98 >>> assert f'textwidth,height={rescale}' in o[0]
99 >>> assert m == Modus.COPY
100 >>> assert math.isnan(r)
102 >>> o = []
103 >>> m, r = filter_seek_figure(r'\pandocbounded{\includegraphics', 0, Modus.COPY, NAN, o)
104 >>> assert o[0].startswith(r'\pandocbounded{\includegraphics')
105 >>> assert m == Modus.COPY
106 >>> assert math.isnan(r)
107 """
108 if line.startswith(BARE_GRAPHICS_START_STARTSWITH):
109 if not math.isnan(rescale):
110 log.info(f'- found the scale target start at line #{slot + 1}|{line}')
111 target = line.replace(BARE_GRAPHICS_START_STARTSWITH, '{')
112 option = f'[width={round(rescale, 2)}\\textwidth,height={round(rescale, 2)}' '\\textheight,keepaspectratio]'
113 outgoing.append(f'\\includegraphics{option}{target}')
114 else:
115 outgoing.append(line)
116 modus = Modus.COPY
117 rescale = NAN
118 elif EVEN_MORE_SO in line:
119 if not math.isnan(rescale): 119 ↛ 131line 119 didn't jump to line 131 because the condition on line 119 was always true
120 log.info(f'- found the scale target start at line #{slot + 1}|{line}')
121 target = line.replace(WRAPPED_GRAPHICS_START_IN, '').replace('[keepaspectratio', '')
122 parts = target.split('}}')
123 rest, inside = ('', '') if len(parts) < 2 else (parts[1].lstrip('}'), parts[0] + '}')
124 option = f'[width={round(rescale, 2)}\\textwidth,height={round(rescale, 2)}' '\\textheight,keepaspectratio'
125 patched = f'{WRAPPED_GRAPHICS_START_IN}{option}{inside}}}{rest}'
126 hack = '}}'
127 if not patched.endswith(hack):
128 patched += hack
129 outgoing.append(patched)
130 else:
131 outgoing.append(line)
132 modus = Modus.COPY
133 rescale = NAN
134 elif WRAPPED_GRAPHICS_START_IN in line:
135 if not math.isnan(rescale):
136 log.info(f'- found the scale target start at line #{slot + 1}|{line}')
137 target = line.replace(WRAPPED_GRAPHICS_START_IN, '').replace('[keepaspectratio]', '')
138 parts = target.split('}}')
139 rest, inside = ('', '') if len(parts) < 2 else (parts[1].lstrip('}'), parts[0] + '}')
140 option = f'[width={round(rescale, 2)}\\textwidth,height={round(rescale, 2)}' '\\textheight,keepaspectratio'
141 outgoing.append(f'{WRAPPED_GRAPHICS_START_IN}{option}{inside}}}{rest}')
142 else:
143 outgoing.append(line)
144 modus = Modus.COPY
145 rescale = NAN
146 else:
147 outgoing.append(line)
149 return modus, rescale
152def scale(incoming: Iterable[str], lookup: Union[dict[str, str], None] = None) -> list[str]:
153 r"""Scan for scale command and if, apply it to the includegraphics LaTeX command.
155 Examples:
157 >>> in_lines = [r'\scale=80\%', '', r'\includegraphics{', '', 'quux']
158 >>> scaled = scale(in_lines)
159 >>> scaled
160 ['', '\\includegraphics[width=0.8\\textwidth,height=0.8\\textheight,keepaspectratio]{', '', 'quux']
163 >>> in_lines = ['foo', '', r'\includegraphics{', '', 'quux']
164 >>> scaled = scale(in_lines)
165 >>> scaled
166 ['foo', '', '\\includegraphics{', '', 'quux']
167 """
168 outgoing: list[str] = []
169 modus = Modus.COPY
170 rescale = NAN
171 for slot, line in enumerate(incoming):
172 line = line.rstrip('\n')
173 if modus == Modus.COPY:
174 modus, rescale = filter_seek_scale(line, slot, modus, rescale, outgoing)
175 else: # if modus == Modus.SCALE:
176 modus, rescale = filter_seek_figure(line, slot, modus, rescale, outgoing)
178 return outgoing