Coverage for liitos/figures.py: 97.80%
71 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-28 20:14:46 +00:00
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-28 20:14:46 +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')
16SCALE_START_TRIGGER_STARTSWITH = r'\scale='
17BARE_GRAPHICS_START_STARTSWITH = r'\includegraphics{'
18WRAPPED_GRAPHICS_START_IN = r'\pandocbounded{\includegraphics'
19EVEN_MORE_SO = r'\pandocbounded{\includegraphics[keepaspectratio,alt={' # HACK A DID ACK
22def filter_seek_scale(line: str, slot: int, modus: Modus, rescale: float, outgoing: list[str]) -> tuple[Modus, float]:
23 r"""Filter line, seek for a scale command, and return updated mnodus, rescale pair.
25 Examples:
27 >>> o = []
28 >>> line = SCALE_START_TRIGGER_STARTSWITH # r'\scale='
29 >>> m, r = filter_seek_scale(line, 0, Modus.COPY, NAN, o)
30 >>> assert not o
31 >>> assert m == Modus.SCALE
32 >>> assert math.isnan(r)
34 >>> o = []
35 >>> m, r = filter_seek_scale('foo', 0, Modus.COPY, NAN, o)
36 >>> assert o == ['foo']
37 >>> assert m == Modus.COPY
38 >>> assert math.isnan(r)
40 >>> o = []
41 >>> m, r = filter_seek_scale(r'\scale=80\%', 0, Modus.COPY, NAN, o)
42 >>> assert not o
43 >>> assert m == Modus.SCALE
44 >>> assert r == 0.8
45 """
46 if line.startswith(SCALE_START_TRIGGER_STARTSWITH):
47 log.info(f'trigger a scale mod for the next figure environment at line #{slot + 1}|{line}')
48 modus = Modus.SCALE
49 scale = line # only for reporting will not pass the filter
50 try:
51 sca = scale.split('=', 1)[1].strip() # \scale = 75\% --> 75\%
52 rescale = float(sca.replace(r'\%', '')) / 100 if r'\%' in sca else float(sca)
53 except Exception as err:
54 log.error(f'failed to parse scale value from {line.strip()} with err: {err}')
55 else:
56 outgoing.append(line)
58 return modus, rescale
61def filter_seek_figure(line: str, slot: int, modus: Modus, rescale: float, outgoing: list[str]) -> tuple[Modus, float]:
62 r"""Filter line, seek for a figure, rescale if applicable, and return updated mnodus, rescale pair.
64 Examples:
66 >>> o = []
67 >>> m, r = filter_seek_figure(r'\includegraphics{', 0, Modus.COPY, NAN, o)
68 >>> assert o == [r'\includegraphics{']
69 >>> assert m == Modus.COPY
70 >>> assert math.isnan(r)
72 >>> o = []
73 >>> m, r = filter_seek_figure('foo', 0, Modus.COPY, 0.8, o)
74 >>> assert o == ['foo']
75 >>> assert m == Modus.COPY
76 >>> assert r == 0.8
78 >>> o = []
79 >>> rescale = 0.8
80 >>> m, r = filter_seek_figure(r'\pandocbounded{\includegraphics', 0, Modus.COPY, rescale, o)
81 >>> assert o[0].startswith(r'\pandocbounded{\includegraphics')
82 >>> assert f'textwidth,height={rescale}' in o[0]
83 >>> assert m == Modus.COPY
84 >>> assert math.isnan(r)
86 >>> o = []
87 >>> m, r = filter_seek_figure(r'\pandocbounded{\includegraphics', 0, Modus.COPY, NAN, o)
88 >>> assert o[0].startswith(r'\pandocbounded{\includegraphics')
89 >>> assert m == Modus.COPY
90 >>> assert math.isnan(r)
91 """
92 if line.startswith(BARE_GRAPHICS_START_STARTSWITH):
93 if not math.isnan(rescale):
94 log.info(f'- found the scale target start at line #{slot + 1}|{line}')
95 target = line.replace(BARE_GRAPHICS_START_STARTSWITH, '{')
96 option = f'[width={round(rescale, 2)}\\textwidth,height={round(rescale, 2)}' '\\textheight,keepaspectratio]'
97 outgoing.append(f'\\includegraphics{option}{target}')
98 else:
99 outgoing.append(line)
100 modus = Modus.COPY
101 rescale = NAN
102 elif EVEN_MORE_SO in line:
103 if not math.isnan(rescale): 103 ↛ 115line 103 didn't jump to line 115 because the condition on line 103 was always true
104 log.info(f'- found the scale target start at line #{slot + 1}|{line}')
105 target = line.replace(WRAPPED_GRAPHICS_START_IN, '').replace('[keepaspectratio', '')
106 parts = target.split('}}')
107 rest, inside = ('', '') if len(parts) < 2 else (parts[1].lstrip('}'), parts[0] + '}')
108 option = f'[width={round(rescale, 2)}\\textwidth,height={round(rescale, 2)}' '\\textheight,keepaspectratio'
109 patched = f'{WRAPPED_GRAPHICS_START_IN}{option}{inside}}}{rest}'
110 hack = '}}'
111 if not patched.endswith(hack):
112 patched += hack
113 outgoing.append(patched)
114 else:
115 outgoing.append(line)
116 modus = Modus.COPY
117 rescale = NAN
118 elif WRAPPED_GRAPHICS_START_IN in line:
119 if not math.isnan(rescale):
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 outgoing.append(f'{WRAPPED_GRAPHICS_START_IN}{option}{inside}}}{rest}')
126 else:
127 outgoing.append(line)
128 modus = Modus.COPY
129 rescale = NAN
130 else:
131 outgoing.append(line)
133 return modus, rescale
136def scale(incoming: Iterable[str], lookup: Union[dict[str, str], None] = None) -> list[str]:
137 r"""Scan for scale command and if, apply it to the includegraphics LaTeX command.
139 Examples:
141 >>> in_lines = [r'\scale=80\%', '', r'\includegraphics{', '', 'quux']
142 >>> scaled = scale(in_lines)
143 >>> scaled
144 ['', '\\includegraphics[width=0.8\\textwidth,height=0.8\\textheight,keepaspectratio]{', '', 'quux']
147 >>> in_lines = ['foo', '', r'\includegraphics{', '', 'quux']
148 >>> scaled = scale(in_lines)
149 >>> scaled
150 ['foo', '', '\\includegraphics{', '', 'quux']
151 """
152 outgoing: list[str] = []
153 modus = Modus.COPY
154 rescale = NAN
155 for slot, line in enumerate(incoming):
156 line = line.rstrip('\n')
157 if modus == Modus.COPY:
158 modus, rescale = filter_seek_scale(line, slot, modus, rescale, outgoing)
159 else: # if modus == Modus.SCALE:
160 modus, rescale = filter_seek_figure(line, slot, modus, rescale, outgoing)
162 return outgoing