pyact-cli 0.7.3__tar.gz → 0.8.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.7.3
3
+ Version: 0.8.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
@@ -86,11 +86,30 @@ Run the compiler from your terminal to generate your final Markdown document:
86
86
  pyact path/to/your_file.pamd -o output.md
87
87
  ```
88
88
 
89
- ### 4. Compile to Word (DOCX)
89
+ ### 5. Compile to Word (DOCX)
90
90
  You can also compile straight to a native Microsoft Word Document, complete with CSS styling!
91
91
  ```bash
92
92
  pyact path/to/your_file.pamd -o output.md --docx output.docx
93
93
  ```
94
94
  *Note: If you have a `style.css` file in the same directory, PyAct will automatically load it to style your Word Document. You can also specify one manually using the `--css path/to/style.css` flag!*
95
95
 
96
+ ### 6. Configure Word Export via `pamd.conf`
97
+ Control document metadata, running headers, and footers with a `pamd.conf` file:
98
+
99
+ ```ini
100
+ [Header]
101
+ left = My Project
102
+ right = 2026
103
+
104
+ [Footer]
105
+ left = Confidential
106
+ center = Page {page} of {pages}
107
+
108
+ [Metadata]
109
+ author = Jane Doe
110
+ title = Monthly Report
111
+ ```
112
+
113
+ PyAct will automatically detect `pamd.conf` in your directory, inject dynamic page numbers (`{page}` / `{pages}`), configure tab stops, and populate document metadata!
114
+
96
115
  That's it! You now have documents ready to be shared, published, or distributed.
@@ -63,11 +63,30 @@ Run the compiler from your terminal to generate your final Markdown document:
63
63
  pyact path/to/your_file.pamd -o output.md
64
64
  ```
65
65
 
66
- ### 4. Compile to Word (DOCX)
66
+ ### 5. Compile to Word (DOCX)
67
67
  You can also compile straight to a native Microsoft Word Document, complete with CSS styling!
68
68
  ```bash
69
69
  pyact path/to/your_file.pamd -o output.md --docx output.docx
70
70
  ```
71
71
  *Note: If you have a `style.css` file in the same directory, PyAct will automatically load it to style your Word Document. You can also specify one manually using the `--css path/to/style.css` flag!*
72
72
 
73
+ ### 6. Configure Word Export via `pamd.conf`
74
+ Control document metadata, running headers, and footers with a `pamd.conf` file:
75
+
76
+ ```ini
77
+ [Header]
78
+ left = My Project
79
+ right = 2026
80
+
81
+ [Footer]
82
+ left = Confidential
83
+ center = Page {page} of {pages}
84
+
85
+ [Metadata]
86
+ author = Jane Doe
87
+ title = Monthly Report
88
+ ```
89
+
90
+ PyAct will automatically detect `pamd.conf` in your directory, inject dynamic page numbers (`{page}` / `{pages}`), configure tab stops, and populate document metadata!
91
+
73
92
  That's it! You now have documents ready to be shared, published, or distributed.
@@ -0,0 +1 @@
1
+ __version__ = "0.8.0"
@@ -43,6 +43,7 @@ def main():
43
43
  parser.add_argument("-o", "--output", help="Output file path (default prints to stdout)")
44
44
  parser.add_argument("--docx", help="Also generate a DOCX file at this path")
45
45
  parser.add_argument("--css", help="Optional CSS file path to style the DOCX")
46
+ parser.add_argument("--conf", help="Optional path to pamd.conf configuration file")
46
47
  parser.add_argument("--lint", action="store_true", help="Run the linter on the project to check for unused variables and files")
47
48
 
48
49
  args = parser.parse_args()
@@ -123,8 +124,25 @@ def main():
123
124
  default_css = os.path.join(os.path.dirname(__file__), "style.css")
124
125
  styles = style_parser(default_css)
125
126
 
127
+ # Load pamd.conf configuration
128
+ conf = {}
129
+ conf_path = None
130
+ if args.conf:
131
+ conf_path = os.path.abspath(args.conf)
132
+ else:
133
+ local_conf = os.path.join(directory, "pamd.conf")
134
+ cwd_conf = os.path.join(os.getcwd(), "pamd.conf")
135
+ if os.path.exists(local_conf):
136
+ conf_path = local_conf
137
+ elif os.path.exists(cwd_conf):
138
+ conf_path = cwd_conf
139
+
140
+ if conf_path and os.path.exists(conf_path):
141
+ from .conf_parser import conf_parser
142
+ conf = conf_parser(conf_path)
143
+
126
144
  blocks = markdown_parser(content)
127
- docx_writer(blocks, styles, args.docx)
145
+ docx_writer(blocks, styles, args.docx, config=conf)
128
146
  print(f"Successfully compiled DOCX to {args.docx}")
129
147
 
130
148
  except Exception as e:
@@ -0,0 +1,44 @@
1
+ import os
2
+
3
+ def conf_parser(conf_path: str) -> dict:
4
+ """
5
+ Parses a pamd.conf configuration file into a dictionary of sections and settings.
6
+
7
+ Why it is needed:
8
+ Allows PyAct projects to define global Word export settings such as Headers, Footers,
9
+ Page margins/orientations, and document Metadata without cluttering Markdown templates
10
+ or Python code cells.
11
+
12
+ Inputs:
13
+ conf_path (str): Path to the pamd.conf file.
14
+
15
+ Outputs:
16
+ dict: A nested dictionary of sections and their key-value options.
17
+ Example: {'Header': {'left': 'PyAct'}, 'Metadata': {'author': 'Radek'}}
18
+ """
19
+ if not conf_path or not os.path.exists(conf_path):
20
+ return {}
21
+
22
+ parsed_dict = {}
23
+ current_header = "General"
24
+
25
+ with open(conf_path, 'r', encoding='utf-8') as f:
26
+ for line in f:
27
+ stripped = line.strip()
28
+ # Skip empty lines and comment lines starting with # or ;
29
+ if not stripped or stripped.startswith(('#', ';')):
30
+ continue
31
+
32
+ if stripped.startswith('[') and stripped.endswith(']'):
33
+ current_header = stripped[1:-1].strip()
34
+ if current_header not in parsed_dict:
35
+ parsed_dict[current_header] = {}
36
+ elif '=' in stripped:
37
+ head, tail = stripped.split('=', 1)
38
+ key = head.strip()
39
+ val = tail.strip()
40
+ if current_header not in parsed_dict:
41
+ parsed_dict[current_header] = {}
42
+ parsed_dict[current_header][key] = val
43
+
44
+ return parsed_dict
@@ -1,12 +1,13 @@
1
1
  import re
2
2
  from docx import Document
3
- from docx.shared import Pt, RGBColor
4
- from docx.enum.text import WD_ALIGN_PARAGRAPH
5
- from docx.oxml import parse_xml
6
- from docx.oxml.ns import nsdecls
3
+ from docx.shared import Pt, RGBColor, Inches, Cm, Mm
4
+ from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_TAB_ALIGNMENT
5
+ from docx.oxml import parse_xml, OxmlElement
6
+ from docx.oxml.ns import nsdecls, qn
7
7
  import math2docx
8
8
  from . import css2json
9
9
 
10
+
10
11
  def style_parser(css_path: str) -> dict:
11
12
  """
12
13
  Parses a CSS file and converts it into a Python dictionary containing style maps.
@@ -425,7 +426,286 @@ def _restart_list_numbering(doc, p):
425
426
  except Exception:
426
427
  return None
427
428
 
428
- def docx_writer(blocks: list, styles: dict, output_file: str):
429
+ def _add_xml_field(run, field_name: str):
430
+ """Inserts a dynamic Word field (like PAGE or NUMPAGES) into a run."""
431
+ fldChar1 = OxmlElement('w:fldChar')
432
+ fldChar1.set(qn('w:fldCharType'), 'begin')
433
+ instrText = OxmlElement('w:instrText')
434
+ instrText.set(qn('xml:space'), 'preserve')
435
+ instrText.text = f" {field_name} "
436
+ fldChar2 = OxmlElement('w:fldChar')
437
+ fldChar2.set(qn('w:fldCharType'), 'separate')
438
+ t = OxmlElement('w:t')
439
+ t.text = "1"
440
+ fldChar3 = OxmlElement('w:fldChar')
441
+ fldChar3.set(qn('w:fldCharType'), 'end')
442
+
443
+ r = run._r
444
+ r.append(fldChar1)
445
+ r.append(instrText)
446
+ r.append(fldChar2)
447
+ r.append(t)
448
+ r.append(fldChar3)
449
+
450
+ def _add_styled_header_footer_text(paragraph, text: str, font_rules: dict):
451
+ """
452
+ Appends text to a header/footer paragraph, replacing {page} and {pages}
453
+ tokens with native Word dynamic field elements and applying font styling.
454
+ """
455
+ if not text:
456
+ return
457
+
458
+ pattern = r'(\{page\}|\{pages\}|\{page_number\}|\{total_pages\})'
459
+ tokens = re.split(pattern, text, flags=re.IGNORECASE)
460
+
461
+ for token in tokens:
462
+ if not token:
463
+ continue
464
+ tok_lower = token.lower()
465
+ if tok_lower in ('{page}', '{page_number}'):
466
+ run = paragraph.add_run()
467
+ _add_xml_field(run, 'PAGE')
468
+ if font_rules:
469
+ _apply_css_to_run(run, font_rules)
470
+ elif tok_lower in ('{pages}', '{total_pages}'):
471
+ run = paragraph.add_run()
472
+ _add_xml_field(run, 'NUMPAGES')
473
+ if font_rules:
474
+ _apply_css_to_run(run, font_rules)
475
+ else:
476
+ run = paragraph.add_run(token)
477
+ if font_rules:
478
+ _apply_css_to_run(run, font_rules)
479
+
480
+ def _configure_header_or_footer(hf_obj, conf_dict: dict, section):
481
+ """
482
+ Configures a header or footer object (section.header or section.footer).
483
+ Supports:
484
+ - text / content (with align = left|center|right)
485
+ - left / center / right 3-zone layout
486
+ - page_number = true
487
+ - font styling (font_name, font_size, color, italic, bold)
488
+ """
489
+ if not conf_dict:
490
+ return
491
+
492
+ p = hf_obj.paragraphs[0]
493
+ p.text = "" # Clear default empty paragraph
494
+
495
+ # Extract font styling options
496
+ font_rules = {}
497
+ if 'font_name' in conf_dict:
498
+ font_rules['font-family'] = conf_dict['font_name']
499
+ elif 'font' in conf_dict:
500
+ font_rules['font-family'] = conf_dict['font']
501
+
502
+ if 'font_size' in conf_dict:
503
+ sz = str(conf_dict['font_size']).strip()
504
+ if not sz.endswith('pt'): sz = f"{sz}pt"
505
+ font_rules['font-size'] = sz
506
+ elif 'size' in conf_dict:
507
+ sz = str(conf_dict['size']).strip()
508
+ if not sz.endswith('pt'): sz = f"{sz}pt"
509
+ font_rules['font-size'] = sz
510
+
511
+ if 'color' in conf_dict:
512
+ font_rules['color'] = conf_dict['color']
513
+
514
+ if 'bold' in conf_dict:
515
+ font_rules['font-weight'] = 'bold' if str(conf_dict['bold']).strip().lower() in ('true', 'yes', '1') else 'normal'
516
+ if 'italic' in conf_dict:
517
+ font_rules['font-style'] = 'italic' if str(conf_dict['italic']).strip().lower() in ('true', 'yes', '1') else 'normal'
518
+
519
+ left_val = conf_dict.get('left')
520
+ center_val = conf_dict.get('center')
521
+ right_val = conf_dict.get('right')
522
+
523
+ # Check if page_number is enabled
524
+ has_page_num = str(conf_dict.get('page_number', '')).strip().lower() in ('true', 'yes', '1') or \
525
+ str(conf_dict.get('page_numbers', '')).strip().lower() in ('true', 'yes', '1')
526
+
527
+ text_val = conf_dict.get('text') or conf_dict.get('content') or conf_dict.get('title')
528
+
529
+ if has_page_num and not (left_val or center_val or right_val or text_val):
530
+ center_val = "Page {page} of {pages}"
531
+
532
+ if left_val is not None or center_val is not None or right_val is not None:
533
+ # Multi-zone layout using tab stops
534
+ content_width = section.page_width - section.left_margin - section.right_margin
535
+ tab_center = int(content_width // 2)
536
+ tab_right = int(content_width)
537
+
538
+ p.paragraph_format.tab_stops.add_tab_stop(tab_center, WD_TAB_ALIGNMENT.CENTER)
539
+ p.paragraph_format.tab_stops.add_tab_stop(tab_right, WD_TAB_ALIGNMENT.RIGHT)
540
+
541
+ # Left zone
542
+ if left_val:
543
+ _add_styled_header_footer_text(p, left_val, font_rules)
544
+
545
+ # Center zone
546
+ if center_val:
547
+ p.add_run('\t')
548
+ _add_styled_header_footer_text(p, center_val, font_rules)
549
+ elif right_val:
550
+ p.add_run('\t')
551
+
552
+ # Right zone
553
+ if right_val:
554
+ p.add_run('\t')
555
+ _add_styled_header_footer_text(p, right_val, font_rules)
556
+
557
+ elif text_val:
558
+ align = conf_dict.get('align', 'left').lower()
559
+ if align == 'center':
560
+ p.alignment = WD_ALIGN_PARAGRAPH.CENTER
561
+ elif align == 'right':
562
+ p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
563
+ else:
564
+ p.alignment = WD_ALIGN_PARAGRAPH.LEFT
565
+
566
+ _add_styled_header_footer_text(p, text_val, font_rules)
567
+
568
+ def _apply_metadata(doc, meta_conf: dict):
569
+ if not meta_conf:
570
+ return
571
+
572
+ cp = doc.core_properties
573
+ meta_map = {
574
+ 'author': 'author',
575
+ 'autor': 'author',
576
+ 'creator': 'author',
577
+ 'title': 'title',
578
+ 'tytul': 'title',
579
+ 'subject': 'subject',
580
+ 'temat': 'subject',
581
+ 'keywords': 'keywords',
582
+ 'slowa_kluczowe': 'keywords',
583
+ 'tags': 'keywords',
584
+ 'category': 'category',
585
+ 'kategoria': 'category',
586
+ 'comments': 'comments',
587
+ 'komentarze': 'comments',
588
+ 'description': 'comments',
589
+ 'opis': 'comments',
590
+ 'status': 'content_status',
591
+ 'content_status': 'content_status',
592
+ 'language': 'language',
593
+ 'lang': 'language',
594
+ 'jezyk': 'language',
595
+ 'version': 'version',
596
+ 'wersja': 'version',
597
+ 'revision': 'revision',
598
+ 'last_modified_by': 'last_modified_by',
599
+ 'identifier': 'identifier',
600
+ 'id': 'identifier',
601
+ }
602
+
603
+ for k, v in meta_conf.items():
604
+ norm_k = k.strip().lower()
605
+ attr = meta_map.get(norm_k, norm_k)
606
+ if hasattr(cp, attr):
607
+ try:
608
+ setattr(cp, attr, str(v))
609
+ except Exception:
610
+ pass
611
+
612
+ def _parse_length(val_str: str):
613
+ if not val_str:
614
+ return None
615
+ s = str(val_str).strip().lower()
616
+ try:
617
+ if s.endswith('in') or s.endswith('"'):
618
+ return Inches(float(s.rstrip('in"').strip()))
619
+ elif s.endswith('cm'):
620
+ return Cm(float(s[:-2].strip()))
621
+ elif s.endswith('mm'):
622
+ return Mm(float(s[:-2].strip()))
623
+ elif s.endswith('pt'):
624
+ return Pt(float(s[:-2].strip()))
625
+ else:
626
+ return Inches(float(s))
627
+ except ValueError:
628
+ return None
629
+
630
+ def _apply_page_settings(doc, page_conf: dict):
631
+ if not page_conf:
632
+ return
633
+
634
+ for s in doc.sections:
635
+ if 'orientation' in page_conf:
636
+ orient = page_conf['orientation'].lower()
637
+ from docx.enum.section import WD_ORIENT
638
+ if orient == 'landscape':
639
+ s.orientation = WD_ORIENT.LANDSCAPE
640
+ if s.page_width < s.page_height:
641
+ s.page_width, s.page_height = s.page_height, s.page_width
642
+ elif orient == 'portrait':
643
+ s.orientation = WD_ORIENT.PORTRAIT
644
+ if s.page_width > s.page_height:
645
+ s.page_width, s.page_height = s.page_height, s.page_width
646
+
647
+ if 'size' in page_conf or 'paper_size' in page_conf:
648
+ size_name = str(page_conf.get('size') or page_conf.get('paper_size')).lower()
649
+ if size_name == 'a4':
650
+ s.page_width = Mm(210)
651
+ s.page_height = Mm(297)
652
+ elif size_name == 'letter':
653
+ s.page_width = Inches(8.5)
654
+ s.page_height = Inches(11)
655
+ elif size_name == 'legal':
656
+ s.page_width = Inches(8.5)
657
+ s.page_height = Inches(14)
658
+
659
+ # Margins
660
+ if 'margin' in page_conf or 'margins' in page_conf:
661
+ m = _parse_length(page_conf.get('margin') or page_conf.get('margins'))
662
+ if m:
663
+ s.top_margin = m
664
+ s.bottom_margin = m
665
+ s.left_margin = m
666
+ s.right_margin = m
667
+
668
+ if 'margin_top' in page_conf or 'top_margin' in page_conf:
669
+ m = _parse_length(page_conf.get('margin_top') or page_conf.get('top_margin'))
670
+ if m: s.top_margin = m
671
+ if 'margin_bottom' in page_conf or 'bottom_margin' in page_conf:
672
+ m = _parse_length(page_conf.get('margin_bottom') or page_conf.get('bottom_margin'))
673
+ if m: s.bottom_margin = m
674
+ if 'margin_left' in page_conf or 'left_margin' in page_conf:
675
+ m = _parse_length(page_conf.get('margin_left') or page_conf.get('left_margin'))
676
+ if m: s.left_margin = m
677
+ if 'margin_right' in page_conf or 'right_margin' in page_conf:
678
+ m = _parse_length(page_conf.get('margin_right') or page_conf.get('right_margin'))
679
+ if m: s.right_margin = m
680
+
681
+ def _apply_docx_config(doc: Document, config: dict):
682
+ """Applies global settings from pamd.conf to the Word document."""
683
+ if not config:
684
+ return
685
+
686
+ normalized_config = {k.lower(): v for k, v in config.items() if isinstance(v, dict)}
687
+
688
+ # 1. Page settings
689
+ page_conf = normalized_config.get('page') or normalized_config.get('margins') or normalized_config.get('document')
690
+ if page_conf:
691
+ _apply_page_settings(doc, page_conf)
692
+
693
+ # 2. Metadata
694
+ meta_conf = normalized_config.get('metadata')
695
+ if meta_conf:
696
+ _apply_metadata(doc, meta_conf)
697
+
698
+ # 3. Header & Footer
699
+ header_conf = normalized_config.get('header')
700
+ footer_conf = normalized_config.get('footer')
701
+
702
+ for section in doc.sections:
703
+ if header_conf:
704
+ _configure_header_or_footer(section.header, header_conf, section)
705
+ if footer_conf:
706
+ _configure_header_or_footer(section.footer, footer_conf, section)
707
+
708
+ def docx_writer(blocks: list, styles: dict, output_file: str, config: dict = None):
429
709
  """
430
710
  Translates parsed markdown blocks into a Word document and applies CSS styling.
431
711
 
@@ -438,6 +718,7 @@ def docx_writer(blocks: list, styles: dict, output_file: str):
438
718
  blocks (list): The list of parsed Markdown block tuples (from `markdown_parser`).
439
719
  styles (dict): The nested dictionary of CSS rules (from `style_parser`).
440
720
  output_file (str): The absolute or relative file path where the resulting .docx should be saved.
721
+ config (dict): Optional dictionary of global settings parsed from pamd.conf (header, footer, metadata).
441
722
 
442
723
  Outputs:
443
724
  None: This function does not return a value, but it writes a binary .docx file to disk.
@@ -445,6 +726,10 @@ def docx_writer(blocks: list, styles: dict, output_file: str):
445
726
  doc = Document()
446
727
  current_ordered_numId = None
447
728
 
729
+ # Global document settings from pamd.conf
730
+ if config:
731
+ _apply_docx_config(doc, config)
732
+
448
733
  # Global body style
449
734
  if 'body' in styles:
450
735
  normal_style = doc.styles['Normal']
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyact-cli
3
- Version: 0.7.3
3
+ Version: 0.8.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
@@ -86,11 +86,30 @@ Run the compiler from your terminal to generate your final Markdown document:
86
86
  pyact path/to/your_file.pamd -o output.md
87
87
  ```
88
88
 
89
- ### 4. Compile to Word (DOCX)
89
+ ### 5. Compile to Word (DOCX)
90
90
  You can also compile straight to a native Microsoft Word Document, complete with CSS styling!
91
91
  ```bash
92
92
  pyact path/to/your_file.pamd -o output.md --docx output.docx
93
93
  ```
94
94
  *Note: If you have a `style.css` file in the same directory, PyAct will automatically load it to style your Word Document. You can also specify one manually using the `--css path/to/style.css` flag!*
95
95
 
96
+ ### 6. Configure Word Export via `pamd.conf`
97
+ Control document metadata, running headers, and footers with a `pamd.conf` file:
98
+
99
+ ```ini
100
+ [Header]
101
+ left = My Project
102
+ right = 2026
103
+
104
+ [Footer]
105
+ left = Confidential
106
+ center = Page {page} of {pages}
107
+
108
+ [Metadata]
109
+ author = Jane Doe
110
+ title = Monthly Report
111
+ ```
112
+
113
+ PyAct will automatically detect `pamd.conf` in your directory, inject dynamic page numbers (`{page}` / `{pages}`), configure tab stops, and populate document metadata!
114
+
96
115
  That's it! You now have documents ready to be shared, published, or distributed.
@@ -3,6 +3,7 @@ setup.py
3
3
  pamd_helpers/__init__.py
4
4
  pyact/__init__.py
5
5
  pyact/cli.py
6
+ pyact/conf_parser.py
6
7
  pyact/core.py
7
8
  pyact/css2json.py
8
9
  pyact/linter.py
@@ -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.7.3",
8
+ version="0.8.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.6.1"
File without changes
File without changes
File without changes
File without changes
File without changes