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.
- {pyact_cli-0.7.3 → pyact_cli-0.9.0}/PKG-INFO +28 -2
- {pyact_cli-0.7.3 → pyact_cli-0.9.0}/README.md +27 -1
- {pyact_cli-0.7.3 → pyact_cli-0.9.0}/pamd_helpers/__init__.py +69 -0
- pyact_cli-0.9.0/pyact/__init__.py +1 -0
- {pyact_cli-0.7.3 → pyact_cli-0.9.0}/pyact/cli.py +33 -4
- pyact_cli-0.9.0/pyact/conf_parser.py +44 -0
- {pyact_cli-0.7.3 → pyact_cli-0.9.0}/pyact/core.py +80 -1
- {pyact_cli-0.7.3 → pyact_cli-0.9.0}/pyact/css2json.py +2 -1
- pyact_cli-0.9.0/pyact/mdTOword.py +1122 -0
- pyact_cli-0.9.0/pyact/scaffold.py +259 -0
- {pyact_cli-0.7.3 → pyact_cli-0.9.0}/pyact_cli.egg-info/PKG-INFO +28 -2
- {pyact_cli-0.7.3 → pyact_cli-0.9.0}/pyact_cli.egg-info/SOURCES.txt +2 -0
- {pyact_cli-0.7.3 → pyact_cli-0.9.0}/setup.py +1 -1
- pyact_cli-0.7.3/pyact/__init__.py +0 -1
- pyact_cli-0.7.3/pyact/mdTOword.py +0 -531
- {pyact_cli-0.7.3 → pyact_cli-0.9.0}/pyact/linter.py +0 -0
- {pyact_cli-0.7.3 → pyact_cli-0.9.0}/pyact/py2tex.py +0 -0
- {pyact_cli-0.7.3 → pyact_cli-0.9.0}/pyact_cli.egg-info/dependency_links.txt +0 -0
- {pyact_cli-0.7.3 → pyact_cli-0.9.0}/pyact_cli.egg-info/entry_points.txt +0 -0
- {pyact_cli-0.7.3 → pyact_cli-0.9.0}/pyact_cli.egg-info/requires.txt +0 -0
- {pyact_cli-0.7.3 → pyact_cli-0.9.0}/pyact_cli.egg-info/top_level.txt +0 -0
- {pyact_cli-0.7.3 → pyact_cli-0.9.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyact-cli
|
|
3
|
-
Version: 0.
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(','):
|