pyact-cli 0.9.0__tar.gz → 1.0.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyact-cli
3
- Version: 0.9.0
3
+ Version: 1.0.0
4
4
  Summary: A CLI tool to compile .pamd files to Markdown.
5
5
  Home-page: https://github.com/Abstergo2003/PyAct
6
6
  Author: Abstergo2003
@@ -0,0 +1 @@
1
+ __version__ = "1.0.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'(\*\*.*?\*\*|\*.*?\*)', part)
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).text = val
1088
- if 'table' in styles:
1089
- pass
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyact-cli
3
- Version: 0.9.0
3
+ Version: 1.0.0
4
4
  Summary: A CLI tool to compile .pamd files to Markdown.
5
5
  Home-page: https://github.com/Abstergo2003/PyAct
6
6
  Author: Abstergo2003
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
5
5
 
6
6
  setup(
7
7
  name="pyact-cli",
8
- version="0.9.0",
8
+ version="1.0.0",
9
9
  author="Abstergo2003",
10
10
  author_email="",
11
11
  description="A CLI tool to compile .pamd files to Markdown.",
@@ -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