pyact-cli 0.7.3__tar.gz → 0.9.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.9.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,37 @@ 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
+
115
+ ### 7. Scaffolding New Projects with `pyact init`
116
+ Quickly spin up a new project ready to compile:
117
+ ```bash
118
+ pyact init my_project --template engineering
119
+ ```
120
+ Templates: `default`, `engineering`, `academic`.
121
+
96
122
  That's it! You now have documents ready to be shared, published, or distributed.
@@ -63,11 +63,37 @@ 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
+
92
+ ### 7. Scaffolding New Projects with `pyact init`
93
+ Quickly spin up a new project ready to compile:
94
+ ```bash
95
+ pyact init my_project --template engineering
96
+ ```
97
+ Templates: `default`, `engineering`, `academic`.
98
+
73
99
  That's it! You now have documents ready to be shared, published, or distributed.
@@ -52,6 +52,75 @@ def image(link: str, caption: str) -> str:
52
52
  caption_html = f"<span style='text-align: center; display: block; font-style: italic;'>{caption}</span>"
53
53
  return f"{image_link} \n {caption_html}"
54
54
 
55
+ def plot(fig_or_plt: Any, caption: str = "", filename: str = None, dpi: int = 300, output_dir: str = "images") -> str:
56
+ """
57
+ Saves a Matplotlib/Seaborn figure or pyplot module to an image file and returns a styled Markdown image string.
58
+
59
+ Why it is needed:
60
+ Allows data analysts and engineers to generate plots directly in Python code cells and
61
+ seamlessly embed them into both Markdown and Word (DOCX) documents without manual file saving.
62
+
63
+ Inputs:
64
+ fig_or_plt: A matplotlib Figure object or the matplotlib.pyplot module.
65
+ caption (str): Caption text to display below the plot.
66
+ filename (str): Optional file name (e.g. 'beam_deflection.png'). If omitted, a name is automatically generated.
67
+ dpi (int): Resolution of the exported image (default 300 dpi for sharp printing).
68
+ output_dir (str): Directory where the image should be saved (default 'images').
69
+
70
+ Outputs:
71
+ str: A Markdown image tag with an italicized caption span, ready for injection into the document.
72
+ """
73
+ import os
74
+ import time
75
+
76
+ if not os.path.exists(output_dir):
77
+ os.makedirs(output_dir, exist_ok=True)
78
+
79
+ if not filename:
80
+ if caption:
81
+ import re
82
+ clean_name = re.sub(r'[^a-zA-Z0-9_-]', '_', caption.strip().lower())[:30].strip('_')
83
+ filename = f"{clean_name}.png" if clean_name else f"plot_{int(time.time()*1000)}.png"
84
+ else:
85
+ filename = f"plot_{int(time.time()*1000)}.png"
86
+
87
+ if not filename.endswith(('.png', '.jpg', '.jpeg', '.svg')):
88
+ filename += '.png'
89
+
90
+ filepath = os.path.join(output_dir, filename)
91
+ norm_path = filepath.replace('\\', '/')
92
+
93
+ if hasattr(fig_or_plt, 'savefig'):
94
+ fig_or_plt.savefig(filepath, dpi=dpi, bbox_inches='tight')
95
+ elif hasattr(fig_or_plt, 'figure') and hasattr(fig_or_plt.figure, 'savefig'):
96
+ fig_or_plt.figure.savefig(filepath, dpi=dpi, bbox_inches='tight')
97
+ elif hasattr(fig_or_plt, 'save'):
98
+ fig_or_plt.save(filepath)
99
+ else:
100
+ raise TypeError(f"Expected a matplotlib Figure or image object with a 'savefig' or 'save' method, got {type(fig_or_plt)}")
101
+
102
+ return image(norm_path, caption)
103
+
104
+ def callout(text: str, callout_type: str = "note", title: str = None) -> str:
105
+ """
106
+ Generates a GitHub-flavored Markdown alert/callout box.
107
+
108
+ Inputs:
109
+ text (str): The body text of the alert.
110
+ callout_type (str): 'note', 'tip', 'important', 'warning', or 'caution'.
111
+ title (str): Optional title overriding the default alert header.
112
+
113
+ Outputs:
114
+ str: A blockquote formatted alert.
115
+ """
116
+ c_type = callout_type.upper()
117
+ lines = text.strip().split('\n')
118
+ header = f"> [!{c_type}]"
119
+ if title:
120
+ header += f" {title}"
121
+ body = "\n".join(f"> {line}" for line in lines)
122
+ return f"{header}\n{body}\n"
123
+
55
124
  def table(headers: List[str], values: List[List[Any]], caption: str, add_no: bool = False, starting_number: int = 1) -> str:
56
125
  """
57
126
  Generates a Markdown table from Python lists, including a caption.
@@ -0,0 +1 @@
1
+ __version__ = "0.9.0"
@@ -2,7 +2,7 @@ import argparse
2
2
  import sys
3
3
  import os
4
4
  import time
5
- from .core import map_content, process_content, build_logs
5
+ from .core import map_content, process_content, build_logs, generate_markdown_toc
6
6
 
7
7
  def is_cache_valid(build_tree, cache_mtime, root_dir=None):
8
8
  if root_dir is not None:
@@ -38,11 +38,22 @@ def main():
38
38
  Writes the generated `.md` and `.docx` files to the filesystem and prints
39
39
  status messages to stdout/stderr.
40
40
  """
41
+ if len(sys.argv) > 1 and sys.argv[1] == "init":
42
+ init_parser = argparse.ArgumentParser(description="PyAct Project Initializer")
43
+ init_parser.add_argument("command", choices=["init"])
44
+ init_parser.add_argument("name", nargs="?", default=".", help="Project folder name (default: current directory)")
45
+ init_parser.add_argument("--template", choices=["default", "engineering", "academic"], default="default", help="Project template to scaffold")
46
+ init_args = init_parser.parse_args()
47
+ from .scaffold import init_project
48
+ init_project(init_args.name, init_args.template)
49
+ return
50
+
41
51
  parser = argparse.ArgumentParser(description="PyAct CLI - PAMD to Markdown compiler")
42
52
  parser.add_argument("input", help="Path to the main .pamd file")
43
53
  parser.add_argument("-o", "--output", help="Output file path (default prints to stdout)")
44
54
  parser.add_argument("--docx", help="Also generate a DOCX file at this path")
45
55
  parser.add_argument("--css", help="Optional CSS file path to style the DOCX")
56
+ parser.add_argument("--conf", help="Optional path to pamd.conf configuration file")
46
57
  parser.add_argument("--lint", action="store_true", help="Run the linter on the project to check for unused variables and files")
47
58
 
48
59
  args = parser.parse_args()
@@ -100,13 +111,14 @@ def main():
100
111
  f.write(f"- {log['file']}: {log['time_ms']:.2f} ms\n")
101
112
  f.write(f"\nTotal Build Time: {total_ms:.2f} ms\n")
102
113
 
114
+ md_output = generate_markdown_toc(content)
103
115
  if args.output:
104
116
  with open(args.output, "w", encoding="utf-8") as f:
105
- f.write(content)
117
+ f.write(md_output)
106
118
  print(f"Successfully compiled to {args.output}")
107
119
  else:
108
120
  if not args.docx:
109
- print(content)
121
+ print(md_output)
110
122
 
111
123
  if args.docx:
112
124
  from .mdTOword import style_parser, markdown_parser, docx_writer
@@ -123,8 +135,25 @@ def main():
123
135
  default_css = os.path.join(os.path.dirname(__file__), "style.css")
124
136
  styles = style_parser(default_css)
125
137
 
138
+ # Load pamd.conf configuration
139
+ conf = {}
140
+ conf_path = None
141
+ if args.conf:
142
+ conf_path = os.path.abspath(args.conf)
143
+ else:
144
+ local_conf = os.path.join(directory, "pamd.conf")
145
+ cwd_conf = os.path.join(os.getcwd(), "pamd.conf")
146
+ if os.path.exists(local_conf):
147
+ conf_path = local_conf
148
+ elif os.path.exists(cwd_conf):
149
+ conf_path = cwd_conf
150
+
151
+ if conf_path and os.path.exists(conf_path):
152
+ from .conf_parser import conf_parser
153
+ conf = conf_parser(conf_path)
154
+
126
155
  blocks = markdown_parser(content)
127
- docx_writer(blocks, styles, args.docx)
156
+ docx_writer(blocks, styles, args.docx, config=conf)
128
157
  print(f"Successfully compiled DOCX to {args.docx}")
129
158
 
130
159
  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
@@ -257,4 +257,83 @@ def process_content(build_tree: dict, globals_dict: dict = None):
257
257
  "time_ms": elapsed_ms
258
258
  })
259
259
 
260
- return file_text
260
+ return file_text
261
+
262
+ def make_github_slug(heading: str) -> str:
263
+ """Creates a GitHub-compatible anchor slug from heading text."""
264
+ clean = re.sub(r'\[([^\]]+)\]\([^\)]+\)', r'\1', heading)
265
+ clean = re.sub(r'[*_`]', '', clean)
266
+ clean = clean.lower().strip()
267
+ clean = re.sub(r'[^\w\s-]', '', clean, flags=re.UNICODE)
268
+ clean = re.sub(r'\s', '-', clean)
269
+ return clean
270
+
271
+ def generate_markdown_toc(text: str) -> str:
272
+ """
273
+ Replaces `<toc/>`, `<toc>`, `[toc]`, or `<toc title="..."/>` with a GitHub-compatible
274
+ Markdown Table of Contents.
275
+ """
276
+ toc_pattern = r'<(?:toc|TOC)(?:\s+title=["\'](.*?)["\'])?\s*/?>|\[toc\]|\[TOC\]'
277
+ m = re.search(toc_pattern, text)
278
+ if not m:
279
+ return text
280
+
281
+ toc_pos = m.start()
282
+ title_override = m.group(1)
283
+
284
+ # Scan headings after the TOC tag
285
+ text_after = text[toc_pos:]
286
+ lines = text_after.split('\n')
287
+
288
+ in_code = False
289
+ headings = []
290
+ for line in lines:
291
+ ls = line.strip()
292
+ if ls.startswith('```'):
293
+ in_code = not in_code
294
+ continue
295
+ if in_code:
296
+ continue
297
+ h_match = re.match(r'^(#{1,6})\s+(.*)$', ls)
298
+ if h_match:
299
+ level = len(h_match.group(1))
300
+ h_text = h_match.group(2).strip()
301
+ headings.append((level, h_text))
302
+
303
+ if not headings:
304
+ # Fallback to scanning whole document if no headings were found after TOC tag
305
+ lines = text.split('\n')
306
+ in_code = False
307
+ for line in lines:
308
+ ls = line.strip()
309
+ if ls.startswith('```'):
310
+ in_code = not in_code
311
+ continue
312
+ if in_code:
313
+ continue
314
+ h_match = re.match(r'^(#{1,6})\s+(.*)$', ls)
315
+ if h_match:
316
+ level = len(h_match.group(1))
317
+ h_text = h_match.group(2).strip()
318
+ headings.append((level, h_text))
319
+
320
+ if not headings:
321
+ return re.sub(toc_pattern, '', text, count=1)
322
+
323
+ base_level = min(h[0] for h in headings)
324
+ toc_lines = []
325
+ for level, h_text in headings:
326
+ indent = " " * (level - base_level)
327
+ clean_title = re.sub(r'[*_`]', '', h_text).strip()
328
+ slug = make_github_slug(h_text)
329
+ toc_lines.append(f"{indent}- [{clean_title}](#{slug})")
330
+
331
+ toc_header = ""
332
+ if title_override:
333
+ if title_override.lower() not in ('none', 'false', 'no'):
334
+ toc_header = f"## {title_override}\n"
335
+ else:
336
+ toc_header = "## Table of Contents\n"
337
+
338
+ full_toc = toc_header + "\n".join(toc_lines) + "\n"
339
+ return re.sub(toc_pattern, lambda _: full_toc, text, count=1)
@@ -36,7 +36,8 @@ def css_to_dict(css_string: str) -> dict:
36
36
  continue
37
37
  if ':' in line:
38
38
  key, val = line.split(':', 1)
39
- rules[key.strip()] = val.strip()
39
+ val_clean = re.sub(r'\s*!important\s*$', '', val.strip(), flags=re.IGNORECASE).strip()
40
+ rules[key.strip()] = val_clean
40
41
 
41
42
  # Handle comma-separated selectors (e.g., 'h1, h2, h3')
42
43
  for sel in selector.split(','):