pyact-cli 0.9.0__tar.gz → 1.1.0__tar.gz
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.
- {pyact_cli-0.9.0 → pyact_cli-1.1.0}/PKG-INFO +1 -1
- {pyact_cli-0.9.0 → pyact_cli-1.1.0}/pamd_helpers/__init__.py +213 -1
- pyact_cli-1.1.0/pyact/__init__.py +1 -0
- {pyact_cli-0.9.0 → pyact_cli-1.1.0}/pyact/mdTOword.py +248 -5
- {pyact_cli-0.9.0 → pyact_cli-1.1.0}/pyact_cli.egg-info/PKG-INFO +1 -1
- {pyact_cli-0.9.0 → pyact_cli-1.1.0}/setup.py +1 -1
- pyact_cli-0.9.0/pyact/__init__.py +0 -1
- {pyact_cli-0.9.0 → pyact_cli-1.1.0}/README.md +0 -0
- {pyact_cli-0.9.0 → pyact_cli-1.1.0}/pyact/cli.py +0 -0
- {pyact_cli-0.9.0 → pyact_cli-1.1.0}/pyact/conf_parser.py +0 -0
- {pyact_cli-0.9.0 → pyact_cli-1.1.0}/pyact/core.py +0 -0
- {pyact_cli-0.9.0 → pyact_cli-1.1.0}/pyact/css2json.py +0 -0
- {pyact_cli-0.9.0 → pyact_cli-1.1.0}/pyact/linter.py +0 -0
- {pyact_cli-0.9.0 → pyact_cli-1.1.0}/pyact/py2tex.py +0 -0
- {pyact_cli-0.9.0 → pyact_cli-1.1.0}/pyact/scaffold.py +0 -0
- {pyact_cli-0.9.0 → pyact_cli-1.1.0}/pyact_cli.egg-info/SOURCES.txt +0 -0
- {pyact_cli-0.9.0 → pyact_cli-1.1.0}/pyact_cli.egg-info/dependency_links.txt +0 -0
- {pyact_cli-0.9.0 → pyact_cli-1.1.0}/pyact_cli.egg-info/entry_points.txt +0 -0
- {pyact_cli-0.9.0 → pyact_cli-1.1.0}/pyact_cli.egg-info/requires.txt +0 -0
- {pyact_cli-0.9.0 → pyact_cli-1.1.0}/pyact_cli.egg-info/top_level.txt +0 -0
- {pyact_cli-0.9.0 → pyact_cli-1.1.0}/setup.cfg +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
from typing import Callable, List, Any
|
|
3
|
+
from enum import Enum
|
|
3
4
|
from pyact.py2tex import py2tex
|
|
4
5
|
|
|
5
6
|
def equation(func: Callable, values: List[Any]) -> str:
|
|
@@ -17,10 +18,13 @@ def equation(func: Callable, values: List[Any]) -> str:
|
|
|
17
18
|
Outputs:
|
|
18
19
|
str: A Markdown math string formatted as `$$ Name = Equation = Value $$`.
|
|
19
20
|
"""
|
|
21
|
+
print("Function 'equation()' is deprecated and will be removed, please use Calculation() class.")
|
|
20
22
|
source = inspect.getsource(func)
|
|
21
23
|
value = func(*values)
|
|
22
24
|
name, sep, tail = source.rpartition("=")
|
|
25
|
+
print(name, tail)
|
|
23
26
|
head, sep, equ = tail.rpartition(":")
|
|
27
|
+
print(head, equ)
|
|
24
28
|
|
|
25
29
|
name = name.strip()
|
|
26
30
|
if "_" in name:
|
|
@@ -139,6 +143,7 @@ def table(headers: List[str], values: List[List[Any]], caption: str, add_no: boo
|
|
|
139
143
|
Outputs:
|
|
140
144
|
str: A formatted Markdown string containing the caption and the table.
|
|
141
145
|
"""
|
|
146
|
+
print("Function 'table()' is deprecated and will be removed, please use Table() class.")
|
|
142
147
|
if add_no:
|
|
143
148
|
headers_string = f"|No.|"
|
|
144
149
|
breaker_line = "|---|"
|
|
@@ -192,6 +197,7 @@ def unordered_list(items: List[str]) -> str:
|
|
|
192
197
|
Outputs:
|
|
193
198
|
str: A Markdown string where each item is prefixed with `* `.
|
|
194
199
|
"""
|
|
200
|
+
print("Function 'unordered_list()' is deprecated and will be removed, please use CList() class.")
|
|
195
201
|
list_string = ""
|
|
196
202
|
for i in items:
|
|
197
203
|
list_string += f"* {i}\n"
|
|
@@ -210,6 +216,7 @@ def ordered_list(items: List[str]) -> str:
|
|
|
210
216
|
Outputs:
|
|
211
217
|
str: A Markdown string where each item is numbered sequentially (1. 2. 3.).
|
|
212
218
|
"""
|
|
219
|
+
print("Function 'ordered_list()' is deprecated and will be removed, please use CList() class.")
|
|
213
220
|
list_string = ""
|
|
214
221
|
for i, item in enumerate(items):
|
|
215
222
|
list_string += f"{i+1}. {item}\n"
|
|
@@ -228,6 +235,7 @@ def checklist(items: List[str]) -> str:
|
|
|
228
235
|
Outputs:
|
|
229
236
|
str: A Markdown string where each item is prefixed with `- [ ] `.
|
|
230
237
|
"""
|
|
238
|
+
print("Function 'checklist()' is deprecated and will be removed, please use CList() class.")
|
|
231
239
|
list_string = ""
|
|
232
240
|
for i in items:
|
|
233
241
|
list_string += f"- [ ] {i}\n"
|
|
@@ -282,6 +290,7 @@ def table_to_dxf(headers: List[str], values: List[List[Any]], filename: str, cel
|
|
|
282
290
|
Outputs:
|
|
283
291
|
str: A string confirming the export, formatted as a Markdown link.
|
|
284
292
|
"""
|
|
293
|
+
print("Function 'table_to_dfx()' is deprecated and will be removed, please use Table() class.")
|
|
285
294
|
try:
|
|
286
295
|
import ezdxf
|
|
287
296
|
except ImportError:
|
|
@@ -319,4 +328,207 @@ def table_to_dxf(headers: List[str], values: List[List[Any]], filename: str, cel
|
|
|
319
328
|
).set_placement((x, y), align=ezdxf.enums.TextEntityAlignment.MIDDLE_CENTER)
|
|
320
329
|
|
|
321
330
|
doc.saveas(filename)
|
|
322
|
-
return f"*[Successfully exported table to {filename}]({filename})*"
|
|
331
|
+
return f"*[Successfully exported table to {filename}]({filename})*"
|
|
332
|
+
|
|
333
|
+
class Calculation:
|
|
334
|
+
def __init__(self, func: Callable, values: List[float|int], repl: bool = False, precision: int = 3):
|
|
335
|
+
self.func = func
|
|
336
|
+
self.values = values
|
|
337
|
+
self.replace = repl
|
|
338
|
+
self.precision = precision
|
|
339
|
+
self.value = round(func(*values), self.precision)
|
|
340
|
+
self.name = None
|
|
341
|
+
self.vars = None
|
|
342
|
+
self.repr = self.get_representation()
|
|
343
|
+
|
|
344
|
+
def __repr__(self):
|
|
345
|
+
return self.repr
|
|
346
|
+
|
|
347
|
+
def recalculate(self, values, precision: int = None):
|
|
348
|
+
if precision is None:
|
|
349
|
+
return Calculation(self.func, values, self.replace, self.precision)
|
|
350
|
+
else:
|
|
351
|
+
return Calculation(self.func, values, self.replace, precision)
|
|
352
|
+
|
|
353
|
+
def get_representation(self):
|
|
354
|
+
source = inspect.getsource(self.func)
|
|
355
|
+
|
|
356
|
+
name, sep, tail = source.rpartition("=")
|
|
357
|
+
|
|
358
|
+
head, sep, equ = tail.rpartition(":")
|
|
359
|
+
self.vars = head.replace("lambda", "").replace(" ", "").split(",")
|
|
360
|
+
|
|
361
|
+
name = name.strip()
|
|
362
|
+
self.name = f"$ {py2tex(name)} $"
|
|
363
|
+
if "_" in name:
|
|
364
|
+
parts = name.split("_", 1)
|
|
365
|
+
name = f"{parts[0]}_{{{parts[1]}}}"
|
|
366
|
+
|
|
367
|
+
import re
|
|
368
|
+
greek_pattern = r'(?<![a-zA-Z])(alpha|beta|gamma|delta|epsilon|zeta|eta|theta|iota|kappa|lambda|mu|nu|xi|omicron|pi|rho|sigma|tau|upsilon|phi|chi|psi|omega|Alpha|Beta|Gamma|Delta|Epsilon|Zeta|Eta|Theta|Iota|Kappa|Lambda|Mu|Nu|Xi|Omicron|Pi|Rho|Sigma|Tau|Upsilon|Phi|Chi|Psi|Omega)(?![a-zA-Z])'
|
|
369
|
+
name = re.sub(greek_pattern, r'\\\1', name)
|
|
370
|
+
texified = py2tex(equ.replace(chr(10), ''))
|
|
371
|
+
replaced = texified
|
|
372
|
+
for i, vr in enumerate(self.vars):
|
|
373
|
+
replaced = replaced.replace(py2tex(vr.replace(",", "")), f"{self.values[i]}")
|
|
374
|
+
|
|
375
|
+
if self.replace:
|
|
376
|
+
return f"$$ {name} = {texified} = {replaced} = {self.value} $$"
|
|
377
|
+
else:
|
|
378
|
+
return f"$$ {name} = {texified} = {self.value} $$"
|
|
379
|
+
|
|
380
|
+
def _extract(self, other) -> float:
|
|
381
|
+
return float(other.value) if isinstance(other, Calculation) else float(other)
|
|
382
|
+
|
|
383
|
+
def __add__(self, other):
|
|
384
|
+
return float(self.value) + self._extract(other)
|
|
385
|
+
|
|
386
|
+
def __radd__(self, other):
|
|
387
|
+
return self._extract(other) + float(self.value)
|
|
388
|
+
|
|
389
|
+
def __sub__(self, other):
|
|
390
|
+
return float(self.value) - self._extract(other)
|
|
391
|
+
|
|
392
|
+
def __rsub__(self, other):
|
|
393
|
+
return self._extract(other) - float(self.value)
|
|
394
|
+
|
|
395
|
+
def __mul__(self, other):
|
|
396
|
+
return float(self.value) * self._extract(other)
|
|
397
|
+
|
|
398
|
+
def __rmul__(self, other):
|
|
399
|
+
return self._extract(other) * float(self.value)
|
|
400
|
+
|
|
401
|
+
def __truediv__(self, other):
|
|
402
|
+
return float(self.value) / self._extract(other)
|
|
403
|
+
|
|
404
|
+
def __rtruediv__(self, other):
|
|
405
|
+
return self._extract(other) / float(self.value)
|
|
406
|
+
|
|
407
|
+
def __pow__(self, power):
|
|
408
|
+
return float(self.value) ** self._extract(power)
|
|
409
|
+
|
|
410
|
+
def __neg__(self):
|
|
411
|
+
return -float(self.value)
|
|
412
|
+
|
|
413
|
+
def __abs__(self):
|
|
414
|
+
return abs(float(self.value))
|
|
415
|
+
|
|
416
|
+
class Table():
|
|
417
|
+
def __init__(self, headers: List[str], values: List[List[Any]], caption: str, add_no: bool = False, starting_number: int = 1):
|
|
418
|
+
self.headers = headers,
|
|
419
|
+
self.values = values
|
|
420
|
+
self.caption = caption
|
|
421
|
+
self.add_no = add_no
|
|
422
|
+
self.starting_number = starting_number
|
|
423
|
+
self.repr = self.get_representation()
|
|
424
|
+
|
|
425
|
+
def __repr__(self):
|
|
426
|
+
return self.repr
|
|
427
|
+
|
|
428
|
+
def get_representation(self):
|
|
429
|
+
if self.add_no:
|
|
430
|
+
headers_string = f"|No.|"
|
|
431
|
+
breaker_line = "|---|"
|
|
432
|
+
else:
|
|
433
|
+
headers_string = f"|"
|
|
434
|
+
breaker_line = "|"
|
|
435
|
+
|
|
436
|
+
for i in self.headers:
|
|
437
|
+
headers_string += f"{i}|"
|
|
438
|
+
breaker_line += "-"*len(i)+"|"
|
|
439
|
+
|
|
440
|
+
table_content = ""
|
|
441
|
+
for i, row in enumerate(self.values):
|
|
442
|
+
if self.add_no:
|
|
443
|
+
table_content += f"|{i+self.starting_number}|"
|
|
444
|
+
else:
|
|
445
|
+
table_content += "|"
|
|
446
|
+
for value in row:
|
|
447
|
+
table_content += f"{value}|"
|
|
448
|
+
table_content += "\n"
|
|
449
|
+
|
|
450
|
+
caption_html = f"<span style='text-align: center; display: block; font-style: italic;'>{self.caption}</span>"
|
|
451
|
+
return f"{caption_html} \n {headers_string}\n{breaker_line}\n{table_content}"
|
|
452
|
+
|
|
453
|
+
def toDXF(self, filename: str, cell_width: float = 50.0, cell_height: float = 10.0):
|
|
454
|
+
try:
|
|
455
|
+
import ezdxf
|
|
456
|
+
except ImportError:
|
|
457
|
+
return f"*(Error: ezdxf library is not installed. Cannot generate {filename})*"
|
|
458
|
+
|
|
459
|
+
doc = ezdxf.new('R2010')
|
|
460
|
+
msp = doc.modelspace()
|
|
461
|
+
|
|
462
|
+
# Construct full grid data
|
|
463
|
+
rows = [self.headers] + self.values
|
|
464
|
+
num_rows = len(rows)
|
|
465
|
+
num_cols = len(self.headers)
|
|
466
|
+
|
|
467
|
+
# Draw horizontal grid lines
|
|
468
|
+
for i in range(num_rows + 1):
|
|
469
|
+
y = -i * cell_height
|
|
470
|
+
msp.add_line((0, y), (num_cols * cell_width, y))
|
|
471
|
+
|
|
472
|
+
# Draw vertical grid lines
|
|
473
|
+
for j in range(num_cols + 1):
|
|
474
|
+
x = j * cell_width
|
|
475
|
+
msp.add_line((x, 0), (x, -num_rows * cell_height))
|
|
476
|
+
|
|
477
|
+
# Draw text centered in each cell
|
|
478
|
+
for i, row in enumerate(rows):
|
|
479
|
+
y = -i * cell_height - (cell_height / 2)
|
|
480
|
+
for j, cell_val in enumerate(row):
|
|
481
|
+
if j >= num_cols: continue # Safety bounds
|
|
482
|
+
x = j * cell_width + (cell_width / 2)
|
|
483
|
+
msp.add_text(
|
|
484
|
+
str(cell_val),
|
|
485
|
+
dxfattribs={
|
|
486
|
+
'height': cell_height * 0.4,
|
|
487
|
+
}
|
|
488
|
+
).set_placement((x, y), align=ezdxf.enums.TextEntityAlignment.MIDDLE_CENTER)
|
|
489
|
+
|
|
490
|
+
doc.saveas(filename)
|
|
491
|
+
return f"*[Successfully exported table to {filename}]({filename})*"
|
|
492
|
+
|
|
493
|
+
class ListType(Enum):
|
|
494
|
+
ORDERED = 1
|
|
495
|
+
UNORDERED = 2
|
|
496
|
+
CHECKLIST = 3
|
|
497
|
+
|
|
498
|
+
class CList():
|
|
499
|
+
def __init__(self, items: List[Any], type: ListType):
|
|
500
|
+
self.items = items
|
|
501
|
+
self.type = type
|
|
502
|
+
self.repr = self.get_representation()
|
|
503
|
+
|
|
504
|
+
def __repr__(self):
|
|
505
|
+
return self.repr
|
|
506
|
+
|
|
507
|
+
def get_representation(self):
|
|
508
|
+
if self.type == ListType.ORDERED:
|
|
509
|
+
return self.get_ordered()
|
|
510
|
+
elif self.type == ListType.UNORDERED:
|
|
511
|
+
return self.get_unordered()
|
|
512
|
+
elif self.type == ListType.CHECKLIST:
|
|
513
|
+
return self.get_checklist()
|
|
514
|
+
|
|
515
|
+
def get_ordered(self):
|
|
516
|
+
list_string = ""
|
|
517
|
+
for i, item in enumerate(self.items):
|
|
518
|
+
list_string += f"{i+1}. {item}\n"
|
|
519
|
+
return list_string
|
|
520
|
+
def get_unordered(self):
|
|
521
|
+
list_string = ""
|
|
522
|
+
for i in self.items:
|
|
523
|
+
list_string += f"* {i}\n"
|
|
524
|
+
return list_string
|
|
525
|
+
def get_checklist(self):
|
|
526
|
+
list_string = ""
|
|
527
|
+
for i in self.items:
|
|
528
|
+
list_string += f"- [ ] {i}\n"
|
|
529
|
+
return list_string
|
|
530
|
+
|
|
531
|
+
if __name__ == "__main__":
|
|
532
|
+
func_w = lambda x_1, y, z: x_1+y+z
|
|
533
|
+
func_eq = Calculation(func_w, [1,2,3], True)
|
|
534
|
+
print(func_eq.vars)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.1.0"
|
|
@@ -423,8 +423,8 @@ def _process_inline(paragraph, text, styles, tag):
|
|
|
423
423
|
_add_internal_hyperlink(paragraph, f"[{fn_id}]", f"footnote_{fn_id}")
|
|
424
424
|
|
|
425
425
|
else:
|
|
426
|
-
# Handle plain text + bold/italic
|
|
427
|
-
subparts = re.split(r'(
|
|
426
|
+
# Handle plain text + bold/italic + inline code
|
|
427
|
+
subparts = re.split(r'(\*\*.*?\*\*|\*.*?\*|`[^`]+`)', part)
|
|
428
428
|
for subpart in subparts:
|
|
429
429
|
if not subpart: continue
|
|
430
430
|
if subpart.startswith('**') and subpart.endswith('**'):
|
|
@@ -435,6 +435,13 @@ def _process_inline(paragraph, text, styles, tag):
|
|
|
435
435
|
run = paragraph.add_run(subpart[1:-1])
|
|
436
436
|
run.italic = True
|
|
437
437
|
if tag in styles: _apply_css_to_run(run, styles[tag])
|
|
438
|
+
elif subpart.startswith('`') and subpart.endswith('`') and len(subpart) >= 2:
|
|
439
|
+
run = paragraph.add_run(subpart[1:-1])
|
|
440
|
+
run.font.name = "Consolas"
|
|
441
|
+
run.font.size = Pt(9.5)
|
|
442
|
+
rPr = run._r.get_or_add_rPr()
|
|
443
|
+
rPr.append(parse_xml(f'<w:shd {nsdecls("w")} w:fill="F0F2F5"/>'))
|
|
444
|
+
if 'code' in styles: _apply_css_to_run(run, styles['code'])
|
|
438
445
|
else:
|
|
439
446
|
run = paragraph.add_run(subpart)
|
|
440
447
|
if tag in styles: _apply_css_to_run(run, styles[tag])
|
|
@@ -964,6 +971,237 @@ def _add_table_of_contents(doc, title="Table of Contents", headings=None, styles
|
|
|
964
971
|
fld_end.set(qn('w:fldCharType'), 'end')
|
|
965
972
|
r_end._r.append(fld_end)
|
|
966
973
|
|
|
974
|
+
def _parse_border_component(rules: dict, prefix: str = 'border'):
|
|
975
|
+
"""
|
|
976
|
+
Parses border component from rules dict (e.g. prefix='border' or 'border-top').
|
|
977
|
+
Returns (val, sz, color) or None if not specified.
|
|
978
|
+
"""
|
|
979
|
+
val = None
|
|
980
|
+
sz = None
|
|
981
|
+
color = None
|
|
982
|
+
|
|
983
|
+
shorthand = rules.get(prefix, '')
|
|
984
|
+
if shorthand:
|
|
985
|
+
for p in shorthand.split():
|
|
986
|
+
p_low = p.lower()
|
|
987
|
+
if p_low in ('none', 'hidden'):
|
|
988
|
+
val = 'none'
|
|
989
|
+
elif p_low in ('solid', 'dashed', 'dotted', 'double'):
|
|
990
|
+
val = 'single' if p_low == 'solid' else p_low
|
|
991
|
+
elif '#' in p:
|
|
992
|
+
hex_c = p.replace('#', '').strip()
|
|
993
|
+
if len(hex_c) == 3: hex_c = ''.join([c*2 for c in hex_c])
|
|
994
|
+
if len(hex_c) == 6: color = hex_c.upper()
|
|
995
|
+
elif 'pt' in p_low or 'px' in p_low or p_low.isdigit():
|
|
996
|
+
num = float(re.sub(r'[^\d.]', '', p_low) or '1')
|
|
997
|
+
sz = str(max(1, int(num * 8)))
|
|
998
|
+
|
|
999
|
+
style_key = f"{prefix}-style" if prefix != 'border' else 'border-style'
|
|
1000
|
+
if style_key in rules:
|
|
1001
|
+
bs = rules[style_key].lower()
|
|
1002
|
+
if bs in ('none', 'hidden'): val = 'none'
|
|
1003
|
+
elif bs == 'solid': val = 'single'
|
|
1004
|
+
elif bs in ('dashed', 'dotted', 'double'): val = bs
|
|
1005
|
+
|
|
1006
|
+
color_key = f"{prefix}-color" if prefix != 'border' else 'border-color'
|
|
1007
|
+
if color_key in rules:
|
|
1008
|
+
hex_c = rules[color_key].replace('#', '').strip()
|
|
1009
|
+
if len(hex_c) == 3: hex_c = ''.join([c*2 for c in hex_c])
|
|
1010
|
+
if len(hex_c) == 6: color = hex_c.upper()
|
|
1011
|
+
|
|
1012
|
+
width_key = f"{prefix}-width" if prefix != 'border' else 'border-width'
|
|
1013
|
+
if width_key in rules:
|
|
1014
|
+
num = float(re.sub(r'[^\d.]', '', rules[width_key]) or '1')
|
|
1015
|
+
sz = str(max(1, int(num * 8)))
|
|
1016
|
+
|
|
1017
|
+
return val, sz, color
|
|
1018
|
+
|
|
1019
|
+
def _apply_table_styling(table, table_lines: list, styles: dict):
|
|
1020
|
+
"""
|
|
1021
|
+
Applies CSS styling to a python-docx table, including borders, cell padding,
|
|
1022
|
+
header shading, alignments, and Word table properties.
|
|
1023
|
+
"""
|
|
1024
|
+
t_styles = styles.get('table', {})
|
|
1025
|
+
th_styles = styles.get('th', styles.get('table_header', {}))
|
|
1026
|
+
td_styles = styles.get('td', styles.get('table_cell', {}))
|
|
1027
|
+
zebra_styles = styles.get('tr:nth-child(even)', styles.get('table_row:nth-child(even)', {}))
|
|
1028
|
+
|
|
1029
|
+
# 1. Built-in Word table style if specified (e.g. style: "Table Grid")
|
|
1030
|
+
if 'style' in t_styles:
|
|
1031
|
+
s_name = t_styles['style'].strip("'\"")
|
|
1032
|
+
try:
|
|
1033
|
+
table.style = s_name
|
|
1034
|
+
except Exception:
|
|
1035
|
+
pass
|
|
1036
|
+
|
|
1037
|
+
tblPr = table._tbl.tblPr
|
|
1038
|
+
|
|
1039
|
+
# 2. Table Width
|
|
1040
|
+
width_val = t_styles.get('width', '100%').strip().lower()
|
|
1041
|
+
if width_val in ('100%', 'full'):
|
|
1042
|
+
tblPr.append(parse_xml(f'<w:tblW {nsdecls("w")} w:w="5000" w:type="pct"/>'))
|
|
1043
|
+
elif width_val == 'auto':
|
|
1044
|
+
tblPr.append(parse_xml(f'<w:tblW {nsdecls("w")} w:w="0" w:type="auto"/>'))
|
|
1045
|
+
else:
|
|
1046
|
+
m = re.search(r'([\d.]+)\s*(in|cm|mm|pt)?', width_val)
|
|
1047
|
+
if m:
|
|
1048
|
+
val, unit = float(m.group(1)), m.group(2)
|
|
1049
|
+
if unit == 'in': dxa = int(val * 1440)
|
|
1050
|
+
elif unit == 'cm': dxa = int(val * 567)
|
|
1051
|
+
elif unit == 'mm': dxa = int(val * 56.7)
|
|
1052
|
+
elif unit == 'pt': dxa = int(val * 20)
|
|
1053
|
+
else: dxa = int(val * 1440)
|
|
1054
|
+
tblPr.append(parse_xml(f'<w:tblW {nsdecls("w")} w:w="{dxa}" w:type="dxa"/>'))
|
|
1055
|
+
|
|
1056
|
+
# 3. Table Alignment (center, left, right)
|
|
1057
|
+
align_val = t_styles.get('text-align', t_styles.get('align', 'center')).strip().lower()
|
|
1058
|
+
if align_val in ('center', 'left', 'right'):
|
|
1059
|
+
tblPr.append(parse_xml(f'<w:jc {nsdecls("w")} w:val="{align_val}"/>'))
|
|
1060
|
+
|
|
1061
|
+
# 4. Table Borders
|
|
1062
|
+
def_val = "single"
|
|
1063
|
+
def_sz = "4" # 0.5 pt
|
|
1064
|
+
def_col = "D0D7DE"
|
|
1065
|
+
|
|
1066
|
+
b_val, b_sz, b_col = _parse_border_component(t_styles, 'border')
|
|
1067
|
+
if b_val is None: b_val = def_val
|
|
1068
|
+
if b_sz is None: b_sz = def_sz
|
|
1069
|
+
if b_col is None: b_col = def_col
|
|
1070
|
+
|
|
1071
|
+
sides = {}
|
|
1072
|
+
for side in ('top', 'left', 'bottom', 'right', 'insideH', 'insideV'):
|
|
1073
|
+
css_key = f"border-{side.lower()}"
|
|
1074
|
+
s_val, s_sz, s_col = _parse_border_component(t_styles, css_key)
|
|
1075
|
+
sides[side] = {
|
|
1076
|
+
'val': s_val if s_val is not None else b_val,
|
|
1077
|
+
'sz': s_sz if s_sz is not None else b_sz,
|
|
1078
|
+
'color': s_col if s_col is not None else b_col,
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
border_elements = []
|
|
1082
|
+
for side, cfg in sides.items():
|
|
1083
|
+
if cfg['val'] == 'none':
|
|
1084
|
+
border_elements.append(f'<w:{side} w:val="none"/>')
|
|
1085
|
+
else:
|
|
1086
|
+
border_elements.append(f'<w:{side} w:val="{cfg["val"]}" w:sz="{cfg["sz"]}" w:space="0" w:color="{cfg["color"]}"/>')
|
|
1087
|
+
|
|
1088
|
+
tblBorders = parse_xml(f'''
|
|
1089
|
+
<w:tblBorders {nsdecls("w")}>
|
|
1090
|
+
{''.join(border_elements)}
|
|
1091
|
+
</w:tblBorders>
|
|
1092
|
+
''')
|
|
1093
|
+
tblPr.append(tblBorders)
|
|
1094
|
+
|
|
1095
|
+
# 5. Cell Margins / Padding
|
|
1096
|
+
pad_val = t_styles.get('padding', t_styles.get('cell-padding', '')).strip().lower()
|
|
1097
|
+
top_dxa, bot_dxa, left_dxa, right_dxa = 120, 120, 160, 160
|
|
1098
|
+
if pad_val:
|
|
1099
|
+
nums = [float(n) for n in re.findall(r'[\d.]+', pad_val)]
|
|
1100
|
+
if len(nums) == 1:
|
|
1101
|
+
d = int(nums[0] * 20)
|
|
1102
|
+
top_dxa, bot_dxa, left_dxa, right_dxa = d, d, d, d
|
|
1103
|
+
elif len(nums) >= 2:
|
|
1104
|
+
tb = int(nums[0] * 20)
|
|
1105
|
+
lr = int(nums[1] * 20)
|
|
1106
|
+
top_dxa, bot_dxa, left_dxa, right_dxa = tb, tb, lr, lr
|
|
1107
|
+
|
|
1108
|
+
tblCellMar = parse_xml(f'''
|
|
1109
|
+
<w:tblCellMar {nsdecls("w")}>
|
|
1110
|
+
<w:top w:w="{top_dxa}" w:type="dxa"/>
|
|
1111
|
+
<w:bottom w:w="{bot_dxa}" w:type="dxa"/>
|
|
1112
|
+
<w:left w:w="{left_dxa}" w:type="dxa"/>
|
|
1113
|
+
<w:right w:w="{right_dxa}" w:type="dxa"/>
|
|
1114
|
+
</w:tblCellMar>
|
|
1115
|
+
''')
|
|
1116
|
+
tblPr.append(tblCellMar)
|
|
1117
|
+
|
|
1118
|
+
# 6. Parse Markdown Column Alignments
|
|
1119
|
+
col_aligns = []
|
|
1120
|
+
for line in table_lines:
|
|
1121
|
+
if '---' in line:
|
|
1122
|
+
parts = [c.strip() for c in line.strip('|').split('|')]
|
|
1123
|
+
for p in parts:
|
|
1124
|
+
if p.startswith(':') and p.endswith(':'):
|
|
1125
|
+
col_aligns.append(WD_ALIGN_PARAGRAPH.CENTER)
|
|
1126
|
+
elif p.endswith(':'):
|
|
1127
|
+
col_aligns.append(WD_ALIGN_PARAGRAPH.RIGHT)
|
|
1128
|
+
elif p.startswith(':'):
|
|
1129
|
+
col_aligns.append(WD_ALIGN_PARAGRAPH.LEFT)
|
|
1130
|
+
else:
|
|
1131
|
+
col_aligns.append(None)
|
|
1132
|
+
break
|
|
1133
|
+
|
|
1134
|
+
# 7. Rows: Repeat header on pages & prevent row splitting
|
|
1135
|
+
if len(table.rows) > 0:
|
|
1136
|
+
table.rows[0]._tr.get_or_add_trPr().append(parse_xml(f'<w:tblHeader {nsdecls("w")}/>'))
|
|
1137
|
+
for row in table.rows:
|
|
1138
|
+
row._tr.get_or_add_trPr().append(parse_xml(f'<w:cantSplit {nsdecls("w")}/>'))
|
|
1139
|
+
|
|
1140
|
+
# 8. Header background color
|
|
1141
|
+
th_bg = th_styles.get('background-color', th_styles.get('background', '')).replace('#', '').strip()
|
|
1142
|
+
if not th_bg and 'background-color' in styles.get('table_header', {}):
|
|
1143
|
+
th_bg = styles['table_header']['background-color'].replace('#', '').strip()
|
|
1144
|
+
if not th_bg:
|
|
1145
|
+
th_bg = 'F2F4F8'
|
|
1146
|
+
if len(th_bg) == 3: th_bg = ''.join([c*2 for c in th_bg])
|
|
1147
|
+
|
|
1148
|
+
# Apply to Header Row
|
|
1149
|
+
if len(table.rows) > 0:
|
|
1150
|
+
for c_idx, cell in enumerate(table.rows[0].cells):
|
|
1151
|
+
tcPr = cell._tc.get_or_add_tcPr()
|
|
1152
|
+
if len(th_bg) == 6:
|
|
1153
|
+
tcPr.append(parse_xml(f'<w:shd {nsdecls("w")} w:fill="{th_bg.upper()}"/>'))
|
|
1154
|
+
p = cell.paragraphs[0]
|
|
1155
|
+
if c_idx < len(col_aligns) and col_aligns[c_idx] is not None:
|
|
1156
|
+
p.alignment = col_aligns[c_idx]
|
|
1157
|
+
elif 'text-align' in th_styles:
|
|
1158
|
+
ta = th_styles['text-align'].lower()
|
|
1159
|
+
if ta == 'center': p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
1160
|
+
elif ta == 'right': p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
|
|
1161
|
+
elif ta == 'left': p.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
|
1162
|
+
else:
|
|
1163
|
+
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
1164
|
+
|
|
1165
|
+
for r in p.runs:
|
|
1166
|
+
if 'font-weight' not in th_styles or th_styles['font-weight'].lower() == 'bold':
|
|
1167
|
+
r.bold = True
|
|
1168
|
+
if 'color' in th_styles:
|
|
1169
|
+
hc = th_styles['color'].replace('#', '').strip()
|
|
1170
|
+
if len(hc) == 6:
|
|
1171
|
+
r.font.color.rgb = RGBColor(int(hc[0:2],16), int(hc[2:4],16), int(hc[4:6],16))
|
|
1172
|
+
if 'font-family' in th_styles:
|
|
1173
|
+
r.font.name = th_styles['font-family'].strip("'\"")
|
|
1174
|
+
if 'font-size' in th_styles:
|
|
1175
|
+
sz_num = re.sub(r'[^\d.]', '', th_styles['font-size'])
|
|
1176
|
+
if sz_num: r.font.size = Pt(float(sz_num))
|
|
1177
|
+
|
|
1178
|
+
# 9. Apply to Body Rows
|
|
1179
|
+
zebra_bg = zebra_styles.get('background-color', zebra_styles.get('background', '')).replace('#', '').strip()
|
|
1180
|
+
if len(zebra_bg) == 3: zebra_bg = ''.join([c*2 for c in zebra_bg])
|
|
1181
|
+
|
|
1182
|
+
for r_idx in range(1, len(table.rows)):
|
|
1183
|
+
is_even = (r_idx % 2 == 0)
|
|
1184
|
+
row_bg = zebra_bg if (is_even and len(zebra_bg) == 6) else None
|
|
1185
|
+
|
|
1186
|
+
for c_idx, cell in enumerate(table.rows[r_idx].cells):
|
|
1187
|
+
tcPr = cell._tc.get_or_add_tcPr()
|
|
1188
|
+
if row_bg:
|
|
1189
|
+
tcPr.append(parse_xml(f'<w:shd {nsdecls("w")} w:fill="{row_bg.upper()}"/>'))
|
|
1190
|
+
elif 'background-color' in td_styles:
|
|
1191
|
+
td_bg = td_styles['background-color'].replace('#', '').strip()
|
|
1192
|
+
if len(td_bg) == 3: td_bg = ''.join([c*2 for c in td_bg])
|
|
1193
|
+
if len(td_bg) == 6:
|
|
1194
|
+
tcPr.append(parse_xml(f'<w:shd {nsdecls("w")} w:fill="{td_bg.upper()}"/>'))
|
|
1195
|
+
|
|
1196
|
+
p = cell.paragraphs[0]
|
|
1197
|
+
if c_idx < len(col_aligns) and col_aligns[c_idx] is not None:
|
|
1198
|
+
p.alignment = col_aligns[c_idx]
|
|
1199
|
+
elif 'text-align' in td_styles:
|
|
1200
|
+
ta = td_styles['text-align'].lower()
|
|
1201
|
+
if ta == 'center': p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
1202
|
+
elif ta == 'right': p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
|
|
1203
|
+
elif ta == 'left': p.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
|
1204
|
+
|
|
967
1205
|
def docx_writer(blocks: list, styles: dict, output_file: str, config: dict = None):
|
|
968
1206
|
"""
|
|
969
1207
|
Translates parsed markdown blocks into a Word document and applies CSS styling.
|
|
@@ -1084,9 +1322,14 @@ def docx_writer(blocks: list, styles: dict, output_file: str, config: dict = Non
|
|
|
1084
1322
|
table = doc.add_table(rows=len(rows), cols=len(rows[0]))
|
|
1085
1323
|
for r_idx, row in enumerate(rows):
|
|
1086
1324
|
for c_idx, val in enumerate(row):
|
|
1087
|
-
table.cell(r_idx, c_idx)
|
|
1088
|
-
|
|
1089
|
-
|
|
1325
|
+
cell = table.cell(r_idx, c_idx)
|
|
1326
|
+
p = cell.paragraphs[0]
|
|
1327
|
+
p.text = ""
|
|
1328
|
+
p.paragraph_format.space_before = Pt(0)
|
|
1329
|
+
p.paragraph_format.space_after = Pt(0)
|
|
1330
|
+
tag = 'th' if r_idx == 0 else 'td'
|
|
1331
|
+
_process_inline(p, val, styles, tag)
|
|
1332
|
+
_apply_table_styling(table, table_lines, styles)
|
|
1090
1333
|
|
|
1091
1334
|
elif btype == 'toc':
|
|
1092
1335
|
toc_seen = True
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.9.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|