pyact-cli 0.8.0__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.8.0 → pyact_cli-0.9.0}/PKG-INFO +8 -1
- {pyact_cli-0.8.0 → pyact_cli-0.9.0}/README.md +7 -0
- {pyact_cli-0.8.0 → pyact_cli-0.9.0}/pamd_helpers/__init__.py +69 -0
- pyact_cli-0.9.0/pyact/__init__.py +1 -0
- {pyact_cli-0.8.0 → pyact_cli-0.9.0}/pyact/cli.py +14 -3
- {pyact_cli-0.8.0 → pyact_cli-0.9.0}/pyact/core.py +80 -1
- {pyact_cli-0.8.0 → pyact_cli-0.9.0}/pyact/css2json.py +2 -1
- {pyact_cli-0.8.0 → pyact_cli-0.9.0}/pyact/mdTOword.py +348 -42
- pyact_cli-0.9.0/pyact/scaffold.py +259 -0
- {pyact_cli-0.8.0 → pyact_cli-0.9.0}/pyact_cli.egg-info/PKG-INFO +8 -1
- {pyact_cli-0.8.0 → pyact_cli-0.9.0}/pyact_cli.egg-info/SOURCES.txt +1 -0
- {pyact_cli-0.8.0 → pyact_cli-0.9.0}/setup.py +1 -1
- pyact_cli-0.8.0/pyact/__init__.py +0 -1
- {pyact_cli-0.8.0 → pyact_cli-0.9.0}/pyact/conf_parser.py +0 -0
- {pyact_cli-0.8.0 → pyact_cli-0.9.0}/pyact/linter.py +0 -0
- {pyact_cli-0.8.0 → pyact_cli-0.9.0}/pyact/py2tex.py +0 -0
- {pyact_cli-0.8.0 → pyact_cli-0.9.0}/pyact_cli.egg-info/dependency_links.txt +0 -0
- {pyact_cli-0.8.0 → pyact_cli-0.9.0}/pyact_cli.egg-info/entry_points.txt +0 -0
- {pyact_cli-0.8.0 → pyact_cli-0.9.0}/pyact_cli.egg-info/requires.txt +0 -0
- {pyact_cli-0.8.0 → pyact_cli-0.9.0}/pyact_cli.egg-info/top_level.txt +0 -0
- {pyact_cli-0.8.0 → 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
|
|
@@ -112,4 +112,11 @@ title = Monthly Report
|
|
|
112
112
|
|
|
113
113
|
PyAct will automatically detect `pamd.conf` in your directory, inject dynamic page numbers (`{page}` / `{pages}`), configure tab stops, and populate document metadata!
|
|
114
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
|
+
|
|
115
122
|
That's it! You now have documents ready to be shared, published, or distributed.
|
|
@@ -89,4 +89,11 @@ title = Monthly Report
|
|
|
89
89
|
|
|
90
90
|
PyAct will automatically detect `pamd.conf` in your directory, inject dynamic page numbers (`{page}` / `{pages}`), configure tab stops, and populate document metadata!
|
|
91
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
|
+
|
|
92
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,6 +38,16 @@ 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)")
|
|
@@ -101,13 +111,14 @@ def main():
|
|
|
101
111
|
f.write(f"- {log['file']}: {log['time_ms']:.2f} ms\n")
|
|
102
112
|
f.write(f"\nTotal Build Time: {total_ms:.2f} ms\n")
|
|
103
113
|
|
|
114
|
+
md_output = generate_markdown_toc(content)
|
|
104
115
|
if args.output:
|
|
105
116
|
with open(args.output, "w", encoding="utf-8") as f:
|
|
106
|
-
f.write(
|
|
117
|
+
f.write(md_output)
|
|
107
118
|
print(f"Successfully compiled to {args.output}")
|
|
108
119
|
else:
|
|
109
120
|
if not args.docx:
|
|
110
|
-
print(
|
|
121
|
+
print(md_output)
|
|
111
122
|
|
|
112
123
|
if args.docx:
|
|
113
124
|
from .mdTOword import style_parser, markdown_parser, docx_writer
|
|
@@ -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(','):
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import re
|
|
2
3
|
from docx import Document
|
|
3
4
|
from docx.shared import Pt, RGBColor, Inches, Cm, Mm
|
|
4
|
-
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_TAB_ALIGNMENT
|
|
5
|
+
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_TAB_ALIGNMENT, WD_TAB_LEADER
|
|
6
|
+
from docx.enum.section import WD_SECTION, WD_ORIENT
|
|
5
7
|
from docx.oxml import parse_xml, OxmlElement
|
|
6
8
|
from docx.oxml.ns import nsdecls, qn
|
|
7
9
|
import math2docx
|
|
@@ -31,35 +33,48 @@ def style_parser(css_path: str) -> dict:
|
|
|
31
33
|
print(f"Warning: Could not parse CSS ({e})")
|
|
32
34
|
return {}
|
|
33
35
|
|
|
36
|
+
def _parse_quote_block(quote_lines: list) -> tuple:
|
|
37
|
+
"""Parses a group of quote lines into an alert callout or standard blockquote."""
|
|
38
|
+
clean_lines = [re.sub(r'^>\s?', '', ql).strip() for ql in quote_lines]
|
|
39
|
+
first_line = clean_lines[0] if clean_lines else ""
|
|
40
|
+
m = re.match(r'^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\](?:\s+(.*))?$', first_line, re.IGNORECASE)
|
|
41
|
+
if m:
|
|
42
|
+
c_type = m.group(1).upper()
|
|
43
|
+
header_text = m.group(2).strip() if m.group(2) else ""
|
|
44
|
+
if len(clean_lines) > 1:
|
|
45
|
+
title = header_text if header_text else c_type.capitalize()
|
|
46
|
+
body = clean_lines[1:]
|
|
47
|
+
else:
|
|
48
|
+
title = c_type.capitalize()
|
|
49
|
+
body = [header_text] if header_text else []
|
|
50
|
+
return ('callout', c_type, title, body)
|
|
51
|
+
else:
|
|
52
|
+
return ('callout', 'BLOCKQUOTE', '', clean_lines)
|
|
53
|
+
|
|
34
54
|
def markdown_parser(md_text: str) -> list:
|
|
35
55
|
"""
|
|
36
56
|
Parses raw Markdown text into a structured list of block tuples.
|
|
37
|
-
|
|
38
|
-
Why it is needed:
|
|
39
|
-
Because python-docx generates documents block-by-block (paragraphs, tables, headings),
|
|
40
|
-
we need to break down the raw string of Markdown text into distinct, classified chunks
|
|
41
|
-
that our DOCX writer can loop through and translate into native Word elements.
|
|
42
|
-
|
|
43
|
-
Inputs:
|
|
44
|
-
md_text (str): The complete raw Markdown text string.
|
|
45
|
-
|
|
46
|
-
Outputs:
|
|
47
|
-
list: A list of tuples where each tuple represents a block element.
|
|
48
|
-
Examples:
|
|
49
|
-
- ('heading', 2, 'Title Text')
|
|
50
|
-
- ('paragraph', 'Some plain text')
|
|
51
|
-
- ('list', 'List item text', 'ordered')
|
|
52
|
-
- ('math', 'x = 2')
|
|
53
|
-
- ('table', ['|Header|', '|---|', '|Data|'])
|
|
54
57
|
"""
|
|
55
58
|
blocks = []
|
|
56
59
|
lines = md_text.split('\n')
|
|
57
60
|
|
|
58
61
|
current_table = []
|
|
62
|
+
current_quote = []
|
|
59
63
|
|
|
60
64
|
for line in lines:
|
|
61
65
|
line_s = line.strip()
|
|
62
66
|
|
|
67
|
+
# Blockquote / Callout Parsing
|
|
68
|
+
if line_s.startswith('>'):
|
|
69
|
+
if current_table:
|
|
70
|
+
blocks.append(('table', current_table))
|
|
71
|
+
current_table = []
|
|
72
|
+
current_quote.append(line_s)
|
|
73
|
+
continue
|
|
74
|
+
elif current_quote:
|
|
75
|
+
blocks.append(_parse_quote_block(current_quote))
|
|
76
|
+
current_quote = []
|
|
77
|
+
|
|
63
78
|
# Table Parsing
|
|
64
79
|
if line_s.startswith('|') and line_s.endswith('|'):
|
|
65
80
|
current_table.append(line_s)
|
|
@@ -71,8 +86,31 @@ def markdown_parser(md_text: str) -> list:
|
|
|
71
86
|
if not line_s:
|
|
72
87
|
continue
|
|
73
88
|
|
|
89
|
+
# Table of Contents Parsing
|
|
90
|
+
if line_s.lower() in ('<toc/>', '<toc>', '[toc]'):
|
|
91
|
+
blocks.append(('toc', 'Table of Contents'))
|
|
92
|
+
|
|
93
|
+
elif re.match(r'^<toc\s+title=["\'](.*?)["\']\s*/?>', line_s, re.IGNORECASE):
|
|
94
|
+
m = re.match(r'^<toc\s+title=["\'](.*?)["\']\s*/?>', line_s, re.IGNORECASE)
|
|
95
|
+
blocks.append(('toc', m.group(1)))
|
|
96
|
+
|
|
97
|
+
# Page Break Parsing
|
|
98
|
+
elif line_s.lower() in ('<pagebreak/>', '<pagebreak>', '<page-break/>', '<pb/>', '<pb>', '<div style="page-break-after: always;"></div>'):
|
|
99
|
+
blocks.append(('pagebreak',))
|
|
100
|
+
|
|
101
|
+
# Section Break Parsing
|
|
102
|
+
elif line_s.lower() in ('<sectionbreak/>', '<sectionbreak>', '<sb/>', '<sb>'):
|
|
103
|
+
blocks.append(('sectionbreak',))
|
|
104
|
+
|
|
105
|
+
# Landscape Orientation Blocks
|
|
106
|
+
elif line_s.lower() in ('<landscape>', '<landscape/>'):
|
|
107
|
+
blocks.append(('landscape_start',))
|
|
108
|
+
|
|
109
|
+
elif line_s.lower() in ('</landscape>',):
|
|
110
|
+
blocks.append(('landscape_end',))
|
|
111
|
+
|
|
74
112
|
# Heading Parsing
|
|
75
|
-
|
|
113
|
+
elif line_s.startswith('#'):
|
|
76
114
|
level = len(line_s) - len(line_s.lstrip('#'))
|
|
77
115
|
text = line_s.lstrip('#').strip()
|
|
78
116
|
blocks.append(('heading', level, text))
|
|
@@ -96,6 +134,8 @@ def markdown_parser(md_text: str) -> list:
|
|
|
96
134
|
|
|
97
135
|
if current_table:
|
|
98
136
|
blocks.append(('table', current_table))
|
|
137
|
+
if current_quote:
|
|
138
|
+
blocks.append(_parse_quote_block(current_quote))
|
|
99
139
|
|
|
100
140
|
return blocks
|
|
101
141
|
|
|
@@ -288,24 +328,39 @@ def _add_hyperlink(paragraph, text, url, styles, tag):
|
|
|
288
328
|
paragraph._p.append(hyperlink)
|
|
289
329
|
|
|
290
330
|
def _add_image(paragraph, url, caption, styles):
|
|
291
|
-
"""Adds an inline image fetched from URL."""
|
|
331
|
+
"""Adds an inline image fetched from URL or local file path."""
|
|
292
332
|
try:
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
333
|
+
image_stream = None
|
|
334
|
+
if url.startswith(('http://', 'https://')):
|
|
335
|
+
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
|
|
336
|
+
with urllib.request.urlopen(req) as response:
|
|
337
|
+
image_stream = io.BytesIO(response.read())
|
|
338
|
+
elif os.path.exists(url):
|
|
339
|
+
with open(url, 'rb') as f:
|
|
340
|
+
image_stream = io.BytesIO(f.read())
|
|
341
|
+
elif os.path.exists(os.path.abspath(url)):
|
|
342
|
+
with open(os.path.abspath(url), 'rb') as f:
|
|
343
|
+
image_stream = io.BytesIO(f.read())
|
|
344
|
+
|
|
345
|
+
if image_stream is None:
|
|
346
|
+
paragraph.add_run(f"[Image Failed: {url}]")
|
|
347
|
+
return
|
|
348
|
+
|
|
297
349
|
run = paragraph.add_run()
|
|
298
350
|
|
|
299
351
|
# Apply CSS dimensions if available
|
|
300
352
|
width = None
|
|
301
353
|
if 'image' in styles and 'width' in styles['image']:
|
|
302
|
-
w_str = styles['image']['width']
|
|
303
|
-
if 'in' in w_str: width =
|
|
354
|
+
w_str = str(styles['image']['width']).strip().lower()
|
|
355
|
+
if 'in' in w_str: width = Inches(float(w_str.replace('in', '').strip()))
|
|
356
|
+
elif 'cm' in w_str: width = Cm(float(w_str.replace('cm', '').strip()))
|
|
357
|
+
elif 'mm' in w_str: width = Mm(float(w_str.replace('mm', '').strip()))
|
|
358
|
+
elif 'pt' in w_str: width = Pt(float(w_str.replace('pt', '').strip()))
|
|
304
359
|
|
|
305
360
|
if width:
|
|
306
361
|
run.add_picture(image_stream, width=width)
|
|
307
362
|
else:
|
|
308
|
-
run.add_picture(image_stream, width=
|
|
363
|
+
run.add_picture(image_stream, width=Inches(4))
|
|
309
364
|
|
|
310
365
|
except Exception as e:
|
|
311
366
|
paragraph.add_run(f"[Image Failed: {url}]")
|
|
@@ -705,23 +760,213 @@ def _apply_docx_config(doc: Document, config: dict):
|
|
|
705
760
|
if footer_conf:
|
|
706
761
|
_configure_header_or_footer(section.footer, footer_conf, section)
|
|
707
762
|
|
|
708
|
-
def
|
|
709
|
-
"""
|
|
710
|
-
|
|
763
|
+
def _add_callout_box(doc, callout_type: str, title: str, body_lines: list, styles: dict):
|
|
764
|
+
"""Renders an alert callout box or blockquote as a styled 1x1 table."""
|
|
765
|
+
table = doc.add_table(rows=1, cols=1)
|
|
766
|
+
table.autofit = False
|
|
711
767
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
768
|
+
theme = {
|
|
769
|
+
'NOTE': {'border': '0969DA', 'bg': 'F0F7FF', 'title_col': (9, 105, 218)},
|
|
770
|
+
'TIP': {'border': '1A7F37', 'bg': 'F0FFF4', 'title_col': (26, 127, 55)},
|
|
771
|
+
'IMPORTANT': {'border': '8250DF', 'bg': 'F8F5FF', 'title_col': (130, 80, 223)},
|
|
772
|
+
'WARNING': {'border': '9A6700', 'bg': 'FFFBEB', 'title_col': (154, 103, 0)},
|
|
773
|
+
'CAUTION': {'border': 'CF222E', 'bg': 'FFF1F2', 'title_col': (207, 34, 46)},
|
|
774
|
+
'BLOCKQUOTE': {'border': 'D0D7DE', 'bg': 'F6F8FA', 'title_col': (87, 96, 106)},
|
|
775
|
+
}
|
|
776
|
+
cfg = theme.get(callout_type.upper(), theme['NOTE'])
|
|
716
777
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
778
|
+
tblPr = table._tbl.tblPr
|
|
779
|
+
tblW = parse_xml(f'<w:tblW {nsdecls("w")} w:w="5000" w:type="pct"/>')
|
|
780
|
+
tblPr.append(tblW)
|
|
781
|
+
|
|
782
|
+
tblBorders = parse_xml(f'''
|
|
783
|
+
<w:tblBorders {nsdecls("w")}>
|
|
784
|
+
<w:top w:val="none"/>
|
|
785
|
+
<w:left w:val="none"/>
|
|
786
|
+
<w:bottom w:val="none"/>
|
|
787
|
+
<w:right w:val="none"/>
|
|
788
|
+
<w:insideH w:val="none"/>
|
|
789
|
+
<w:insideV w:val="none"/>
|
|
790
|
+
</w:tblBorders>
|
|
791
|
+
''')
|
|
792
|
+
tblPr.append(tblBorders)
|
|
793
|
+
|
|
794
|
+
cell = table.cell(0, 0)
|
|
795
|
+
tcPr = cell._tc.get_or_add_tcPr()
|
|
796
|
+
|
|
797
|
+
shd = parse_xml(f'<w:shd {nsdecls("w")} w:fill="{cfg["bg"]}"/>')
|
|
798
|
+
tcPr.append(shd)
|
|
799
|
+
|
|
800
|
+
tcBorders = parse_xml(f'''
|
|
801
|
+
<w:tcBorders {nsdecls("w")}>
|
|
802
|
+
<w:top w:val="none"/>
|
|
803
|
+
<w:left w:val="single" w:sz="36" w:space="0" w:color="{cfg['border']}"/>
|
|
804
|
+
<w:bottom w:val="none"/>
|
|
805
|
+
<w:right w:val="none"/>
|
|
806
|
+
</w:tcBorders>
|
|
807
|
+
''')
|
|
808
|
+
tcPr.append(tcBorders)
|
|
809
|
+
|
|
810
|
+
tcMar = parse_xml(f'''
|
|
811
|
+
<w:tcMar {nsdecls("w")}>
|
|
812
|
+
<w:top w:w="120" w:type="dxa"/>
|
|
813
|
+
<w:bottom w:w="120" w:type="dxa"/>
|
|
814
|
+
<w:left w:w="180" w:type="dxa"/>
|
|
815
|
+
<w:right w:w="180" w:type="dxa"/>
|
|
816
|
+
</w:tcMar>
|
|
817
|
+
''')
|
|
818
|
+
tcPr.append(tcMar)
|
|
819
|
+
|
|
820
|
+
p = cell.paragraphs[0]
|
|
821
|
+
p.paragraph_format.space_before = Pt(2)
|
|
822
|
+
p.paragraph_format.space_after = Pt(3)
|
|
823
|
+
|
|
824
|
+
if title:
|
|
825
|
+
run_title = p.add_run()
|
|
826
|
+
if callout_type != 'BLOCKQUOTE':
|
|
827
|
+
if title.upper() == callout_type:
|
|
828
|
+
run_title.text = callout_type
|
|
829
|
+
else:
|
|
830
|
+
run_title.text = f"{callout_type}: {title}"
|
|
831
|
+
else:
|
|
832
|
+
run_title.text = title
|
|
833
|
+
run_title.bold = True
|
|
834
|
+
r, g, b = cfg['title_col']
|
|
835
|
+
run_title.font.color.rgb = RGBColor(r, g, b)
|
|
722
836
|
|
|
723
|
-
|
|
724
|
-
|
|
837
|
+
for idx, line in enumerate(body_lines):
|
|
838
|
+
if not title and idx == 0:
|
|
839
|
+
p_body = p
|
|
840
|
+
else:
|
|
841
|
+
p_body = cell.add_paragraph()
|
|
842
|
+
p_body.paragraph_format.space_before = Pt(0)
|
|
843
|
+
p_body.paragraph_format.space_after = Pt(2)
|
|
844
|
+
_process_inline(p_body, line, styles, 'p')
|
|
845
|
+
|
|
846
|
+
p_spacer = doc.add_paragraph()
|
|
847
|
+
p_spacer.paragraph_format.space_before = Pt(0)
|
|
848
|
+
p_spacer.paragraph_format.space_after = Pt(4)
|
|
849
|
+
|
|
850
|
+
def _add_table_of_contents(doc, title="Table of Contents", headings=None, styles=None):
|
|
851
|
+
"""Inserts a native Word Table of Contents with dynamic updating and pre-populated entries."""
|
|
852
|
+
if headings is None:
|
|
853
|
+
headings = []
|
|
854
|
+
if styles is None:
|
|
855
|
+
styles = {}
|
|
856
|
+
|
|
857
|
+
try:
|
|
858
|
+
settings = doc.settings.element
|
|
859
|
+
if not settings.xpath('w:updateFields'):
|
|
860
|
+
update_fields = OxmlElement('w:updateFields')
|
|
861
|
+
update_fields.set(qn('w:val'), 'true')
|
|
862
|
+
settings.append(update_fields)
|
|
863
|
+
except Exception:
|
|
864
|
+
pass
|
|
865
|
+
|
|
866
|
+
if title and title.lower() not in ('none', 'false', 'no'):
|
|
867
|
+
p_title = doc.add_heading(title, level=1)
|
|
868
|
+
if 'h1' in styles:
|
|
869
|
+
_apply_paragraph_formatting(p_title, styles['h1'])
|
|
870
|
+
|
|
871
|
+
p_first = doc.add_paragraph()
|
|
872
|
+
p_first.paragraph_format.space_before = Pt(2)
|
|
873
|
+
p_first.paragraph_format.space_after = Pt(2)
|
|
874
|
+
|
|
875
|
+
r_begin = p_first.add_run()
|
|
876
|
+
fld1 = OxmlElement('w:fldChar')
|
|
877
|
+
fld1.set(qn('w:fldCharType'), 'begin')
|
|
878
|
+
r_begin._r.append(fld1)
|
|
879
|
+
|
|
880
|
+
r_instr = p_first.add_run()
|
|
881
|
+
instr = OxmlElement('w:instrText')
|
|
882
|
+
instr.set(qn('xml:space'), 'preserve')
|
|
883
|
+
instr.text = ' TOC \\o "1-3" \\h \\z \\u '
|
|
884
|
+
r_instr._r.append(instr)
|
|
885
|
+
|
|
886
|
+
r_sep = p_first.add_run()
|
|
887
|
+
fld2 = OxmlElement('w:fldChar')
|
|
888
|
+
fld2.set(qn('w:fldCharType'), 'separate')
|
|
889
|
+
r_sep._r.append(fld2)
|
|
890
|
+
|
|
891
|
+
if headings:
|
|
892
|
+
base_level = min(h[1] for h in headings)
|
|
893
|
+
section = doc.sections[-1]
|
|
894
|
+
content_width = section.page_width - section.left_margin - section.right_margin
|
|
895
|
+
|
|
896
|
+
for i, h_block in enumerate(headings):
|
|
897
|
+
h_level, h_text = h_block[1], h_block[2]
|
|
898
|
+
bm_name = f"_Toc_{i}"
|
|
899
|
+
rel_level = h_level - base_level
|
|
900
|
+
indent_in = max(0, rel_level * 0.25)
|
|
901
|
+
|
|
902
|
+
p = p_first if i == 0 else doc.add_paragraph()
|
|
903
|
+
p.paragraph_format.space_before = Pt(1)
|
|
904
|
+
p.paragraph_format.space_after = Pt(2)
|
|
905
|
+
if indent_in > 0:
|
|
906
|
+
p.paragraph_format.left_indent = Inches(indent_in)
|
|
907
|
+
|
|
908
|
+
tab_pos = content_width - Inches(indent_in)
|
|
909
|
+
p.paragraph_format.tab_stops.add_tab_stop(tab_pos, WD_TAB_ALIGNMENT.RIGHT, WD_TAB_LEADER.DOTS)
|
|
910
|
+
|
|
911
|
+
hyperlink = OxmlElement('w:hyperlink')
|
|
912
|
+
hyperlink.set(qn('w:anchor'), bm_name)
|
|
913
|
+
|
|
914
|
+
r_text = OxmlElement('w:r')
|
|
915
|
+
t = OxmlElement('w:t')
|
|
916
|
+
t.text = h_text
|
|
917
|
+
r_text.append(t)
|
|
918
|
+
hyperlink.append(r_text)
|
|
919
|
+
|
|
920
|
+
r_tab = OxmlElement('w:r')
|
|
921
|
+
tab = OxmlElement('w:tab')
|
|
922
|
+
r_tab.append(tab)
|
|
923
|
+
hyperlink.append(r_tab)
|
|
924
|
+
|
|
925
|
+
fld_p1 = OxmlElement('w:fldChar')
|
|
926
|
+
fld_p1.set(qn('w:fldCharType'), 'begin')
|
|
927
|
+
r_p1 = OxmlElement('w:r')
|
|
928
|
+
r_p1.append(fld_p1)
|
|
929
|
+
|
|
930
|
+
instr_p = OxmlElement('w:instrText')
|
|
931
|
+
instr_p.set(qn('xml:space'), 'preserve')
|
|
932
|
+
instr_p.text = f' PAGEREF {bm_name} \\h '
|
|
933
|
+
r_pinstr = OxmlElement('w:r')
|
|
934
|
+
r_pinstr.append(instr_p)
|
|
935
|
+
|
|
936
|
+
fld_p2 = OxmlElement('w:fldChar')
|
|
937
|
+
fld_p2.set(qn('w:fldCharType'), 'separate')
|
|
938
|
+
r_p2 = OxmlElement('w:r')
|
|
939
|
+
r_p2.append(fld_p2)
|
|
940
|
+
|
|
941
|
+
r_pnum = OxmlElement('w:r')
|
|
942
|
+
t_pnum = OxmlElement('w:t')
|
|
943
|
+
t_pnum.text = str(max(1, h_level))
|
|
944
|
+
r_pnum.append(t_pnum)
|
|
945
|
+
|
|
946
|
+
fld_p3 = OxmlElement('w:fldChar')
|
|
947
|
+
fld_p3.set(qn('w:fldCharType'), 'end')
|
|
948
|
+
r_p3 = OxmlElement('w:r')
|
|
949
|
+
r_p3.append(fld_p3)
|
|
950
|
+
|
|
951
|
+
hyperlink.append(r_p1)
|
|
952
|
+
hyperlink.append(r_pinstr)
|
|
953
|
+
hyperlink.append(r_p2)
|
|
954
|
+
hyperlink.append(r_pnum)
|
|
955
|
+
hyperlink.append(r_p3)
|
|
956
|
+
|
|
957
|
+
p._p.append(hyperlink)
|
|
958
|
+
|
|
959
|
+
p_end = doc.add_paragraph()
|
|
960
|
+
p_end.paragraph_format.space_before = Pt(0)
|
|
961
|
+
p_end.paragraph_format.space_after = Pt(4)
|
|
962
|
+
r_end = p_end.add_run()
|
|
963
|
+
fld_end = OxmlElement('w:fldChar')
|
|
964
|
+
fld_end.set(qn('w:fldCharType'), 'end')
|
|
965
|
+
r_end._r.append(fld_end)
|
|
966
|
+
|
|
967
|
+
def docx_writer(blocks: list, styles: dict, output_file: str, config: dict = None):
|
|
968
|
+
"""
|
|
969
|
+
Translates parsed markdown blocks into a Word document and applies CSS styling.
|
|
725
970
|
"""
|
|
726
971
|
doc = Document()
|
|
727
972
|
current_ordered_numId = None
|
|
@@ -736,13 +981,47 @@ def docx_writer(blocks: list, styles: dict, output_file: str, config: dict = Non
|
|
|
736
981
|
_apply_css_to_run(normal_style, styles['body'])
|
|
737
982
|
_apply_paragraph_formatting(normal_style, styles['body'])
|
|
738
983
|
|
|
984
|
+
# Pre-scan headings for Table of Contents
|
|
985
|
+
headings_for_toc = []
|
|
986
|
+
toc_seen_scan = False
|
|
987
|
+
for b in blocks:
|
|
988
|
+
if b[0] == 'toc':
|
|
989
|
+
toc_seen_scan = True
|
|
990
|
+
continue
|
|
991
|
+
if b[0] == 'heading' and toc_seen_scan:
|
|
992
|
+
headings_for_toc.append(b)
|
|
993
|
+
|
|
994
|
+
if not headings_for_toc:
|
|
995
|
+
headings_for_toc = [b for b in blocks if b[0] == 'heading']
|
|
996
|
+
|
|
997
|
+
toc_seen = False
|
|
998
|
+
heading_counter = 0
|
|
999
|
+
|
|
739
1000
|
for block in blocks:
|
|
740
1001
|
btype = block[0]
|
|
741
1002
|
|
|
742
1003
|
if btype == 'heading':
|
|
743
1004
|
level, text = block[1], block[2]
|
|
744
1005
|
p = doc.add_heading('', level=level)
|
|
745
|
-
|
|
1006
|
+
|
|
1007
|
+
if toc_seen:
|
|
1008
|
+
bm_name = f"_Toc_{heading_counter}"
|
|
1009
|
+
bm_id = str(2000 + heading_counter)
|
|
1010
|
+
heading_counter += 1
|
|
1011
|
+
|
|
1012
|
+
bm_start = OxmlElement('w:bookmarkStart')
|
|
1013
|
+
bm_start.set(qn('w:id'), bm_id)
|
|
1014
|
+
bm_start.set(qn('w:name'), bm_name)
|
|
1015
|
+
p._p.append(bm_start)
|
|
1016
|
+
|
|
1017
|
+
_process_inline(p, text, styles, f'h{level}')
|
|
1018
|
+
|
|
1019
|
+
bm_end = OxmlElement('w:bookmarkEnd')
|
|
1020
|
+
bm_end.set(qn('w:id'), bm_id)
|
|
1021
|
+
p._p.append(bm_end)
|
|
1022
|
+
else:
|
|
1023
|
+
_process_inline(p, text, styles, f'h{level}')
|
|
1024
|
+
|
|
746
1025
|
if f'h{level}' in styles:
|
|
747
1026
|
_apply_paragraph_formatting(p, styles[f'h{level}'])
|
|
748
1027
|
|
|
@@ -752,6 +1031,10 @@ def docx_writer(blocks: list, styles: dict, output_file: str, config: dict = Non
|
|
|
752
1031
|
if 'p' in styles:
|
|
753
1032
|
_apply_paragraph_formatting(p, styles['p'])
|
|
754
1033
|
_process_inline(p, text, styles, 'p')
|
|
1034
|
+
|
|
1035
|
+
elif btype == 'callout':
|
|
1036
|
+
c_type, title, body_lines = block[1], block[2], block[3]
|
|
1037
|
+
_add_callout_box(doc, c_type, title, body_lines, styles)
|
|
755
1038
|
|
|
756
1039
|
elif btype == 'list':
|
|
757
1040
|
text = block[1]
|
|
@@ -805,6 +1088,29 @@ def docx_writer(blocks: list, styles: dict, output_file: str, config: dict = Non
|
|
|
805
1088
|
if 'table' in styles:
|
|
806
1089
|
pass
|
|
807
1090
|
|
|
1091
|
+
elif btype == 'toc':
|
|
1092
|
+
toc_seen = True
|
|
1093
|
+
title = block[1]
|
|
1094
|
+
_add_table_of_contents(doc, title, headings_for_toc, styles)
|
|
1095
|
+
|
|
1096
|
+
elif btype == 'pagebreak':
|
|
1097
|
+
doc.add_page_break()
|
|
1098
|
+
|
|
1099
|
+
elif btype == 'sectionbreak':
|
|
1100
|
+
doc.add_section(WD_SECTION.NEW_PAGE)
|
|
1101
|
+
|
|
1102
|
+
elif btype == 'landscape_start':
|
|
1103
|
+
new_sec = doc.add_section(WD_SECTION.NEW_PAGE)
|
|
1104
|
+
new_sec.orientation = WD_ORIENT.LANDSCAPE
|
|
1105
|
+
if new_sec.page_width < new_sec.page_height:
|
|
1106
|
+
new_sec.page_width, new_sec.page_height = new_sec.page_height, new_sec.page_width
|
|
1107
|
+
|
|
1108
|
+
elif btype == 'landscape_end':
|
|
1109
|
+
new_sec = doc.add_section(WD_SECTION.NEW_PAGE)
|
|
1110
|
+
new_sec.orientation = WD_ORIENT.PORTRAIT
|
|
1111
|
+
if new_sec.page_width > new_sec.page_height:
|
|
1112
|
+
new_sec.page_width, new_sec.page_height = new_sec.page_height, new_sec.page_width
|
|
1113
|
+
|
|
808
1114
|
doc.save(output_file)
|
|
809
1115
|
|
|
810
1116
|
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
def init_project(target_dir: str = ".", template: str = "default"):
|
|
5
|
+
"""
|
|
6
|
+
Scaffolds a new PyAct project with boilerplate files.
|
|
7
|
+
|
|
8
|
+
Why it is needed:
|
|
9
|
+
Enables users to instantly create a ready-to-run PyAct project with main.pamd,
|
|
10
|
+
global.py, pamd.conf, style.css, and .gitignore without manual setup.
|
|
11
|
+
|
|
12
|
+
Inputs:
|
|
13
|
+
target_dir (str): Destination folder path.
|
|
14
|
+
template (str): 'default', 'engineering', or 'academic'.
|
|
15
|
+
"""
|
|
16
|
+
if target_dir != "." and not os.path.exists(target_dir):
|
|
17
|
+
os.makedirs(target_dir, exist_ok=True)
|
|
18
|
+
|
|
19
|
+
project_name = os.path.basename(os.path.abspath(target_dir))
|
|
20
|
+
if project_name == "" or project_name == ".":
|
|
21
|
+
project_name = "My PyAct Project"
|
|
22
|
+
|
|
23
|
+
# 1. global.py
|
|
24
|
+
global_py_content = f'''def global_context():
|
|
25
|
+
"""
|
|
26
|
+
Global variables accessible across all templates using <glb>var_name</glb>.
|
|
27
|
+
"""
|
|
28
|
+
return {{
|
|
29
|
+
"author": "Author Name",
|
|
30
|
+
"project_name": "{project_name}",
|
|
31
|
+
"company": "Company Name",
|
|
32
|
+
"version": "1.0.0"
|
|
33
|
+
}}
|
|
34
|
+
'''
|
|
35
|
+
|
|
36
|
+
# 2. pamd.conf
|
|
37
|
+
pamd_conf_content = f'''[Header]
|
|
38
|
+
left = {project_name}
|
|
39
|
+
right = 2026
|
|
40
|
+
font_size = 9pt
|
|
41
|
+
color = #666666
|
|
42
|
+
|
|
43
|
+
[Footer]
|
|
44
|
+
left = Confidential
|
|
45
|
+
center = Page {{page}} of {{pages}}
|
|
46
|
+
font_size = 9pt
|
|
47
|
+
|
|
48
|
+
[Metadata]
|
|
49
|
+
author = Author Name
|
|
50
|
+
title = {project_name}
|
|
51
|
+
subject = Technical Report
|
|
52
|
+
|
|
53
|
+
[Page]
|
|
54
|
+
size = A4
|
|
55
|
+
margin = 1in
|
|
56
|
+
'''
|
|
57
|
+
|
|
58
|
+
# 3. style.css
|
|
59
|
+
style_css_content = '''body {
|
|
60
|
+
font-family: Calibri, sans-serif;
|
|
61
|
+
font-size: 11pt;
|
|
62
|
+
line-height: 1.25;
|
|
63
|
+
color: #222222;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
h1 {
|
|
67
|
+
color: #1a365d;
|
|
68
|
+
font-size: 22pt;
|
|
69
|
+
font-weight: bold;
|
|
70
|
+
margin-bottom: 14pt;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
h2 {
|
|
74
|
+
color: #2b6cb0;
|
|
75
|
+
font-size: 16pt;
|
|
76
|
+
font-weight: bold;
|
|
77
|
+
margin-top: 14pt;
|
|
78
|
+
margin-bottom: 8pt;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
h3 {
|
|
82
|
+
color: #4a5568;
|
|
83
|
+
font-size: 13pt;
|
|
84
|
+
font-weight: bold;
|
|
85
|
+
margin-top: 10pt;
|
|
86
|
+
margin-bottom: 6pt;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
p {
|
|
90
|
+
margin-bottom: 8pt;
|
|
91
|
+
}
|
|
92
|
+
'''
|
|
93
|
+
|
|
94
|
+
# 4. .gitignore
|
|
95
|
+
gitignore_content = '''.pamd-cache/
|
|
96
|
+
__pycache__/
|
|
97
|
+
*.pyc
|
|
98
|
+
'''
|
|
99
|
+
|
|
100
|
+
# 5. main.pamd based on template
|
|
101
|
+
if template == "engineering":
|
|
102
|
+
cells = [
|
|
103
|
+
{
|
|
104
|
+
"cell_type": "code",
|
|
105
|
+
"source": [
|
|
106
|
+
"import pamd_helpers\n",
|
|
107
|
+
"\n",
|
|
108
|
+
"def context():\n",
|
|
109
|
+
" # 1. Design formula with Greek symbols and subscripts\n",
|
|
110
|
+
" f_cd = lambda f_ck, gamma_c: f_ck / gamma_c\n",
|
|
111
|
+
" design_strength = pamd_helpers.equation(f_cd, [30, 1.5])\n",
|
|
112
|
+
" \n",
|
|
113
|
+
" # 2. Table of material properties\n",
|
|
114
|
+
" headers = [\"Property\", \"Symbol\", \"Value\", \"Unit\"]\n",
|
|
115
|
+
" rows = [\n",
|
|
116
|
+
" [\"Char. Compressive Strength\", \"f_ck\", \"30\", \"MPa\"],\n",
|
|
117
|
+
" [\"Partial Safety Factor\", \"gamma_c\", \"1.5\", \"-\"],\n",
|
|
118
|
+
" [\"Design Compressive Strength\", \"f_cd\", \"20\", \"MPa\"]\n",
|
|
119
|
+
" ]\n",
|
|
120
|
+
" mat_table = pamd_helpers.table(headers, rows, \"Concrete Material Properties\", add_no=True)\n",
|
|
121
|
+
" \n",
|
|
122
|
+
" return {\n",
|
|
123
|
+
" \"strength_calc\": design_strength,\n",
|
|
124
|
+
" \"material_table\": mat_table\n",
|
|
125
|
+
" }\n"
|
|
126
|
+
]
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"cell_type": "markdown",
|
|
130
|
+
"source": [
|
|
131
|
+
"# <glb>project_name</glb>\n",
|
|
132
|
+
"Author: **<glb>author</glb>** | Organization: **<glb>company</glb>**\n",
|
|
133
|
+
"\n",
|
|
134
|
+
"<toc/>\n",
|
|
135
|
+
"\n",
|
|
136
|
+
"<pagebreak/>\n",
|
|
137
|
+
"\n",
|
|
138
|
+
"## 1. Material Specifications\n",
|
|
139
|
+
"Below are the design material parameters:\n",
|
|
140
|
+
"<ctx>material_table</ctx>\n",
|
|
141
|
+
"\n",
|
|
142
|
+
"## 2. Design Strength Verification\n",
|
|
143
|
+
"The design compressive strength is calculated in accordance with Eurocode 2:\n",
|
|
144
|
+
"<ctx>strength_calc</ctx>\n"
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
]
|
|
148
|
+
elif template == "academic":
|
|
149
|
+
cells = [
|
|
150
|
+
{
|
|
151
|
+
"cell_type": "code",
|
|
152
|
+
"source": [
|
|
153
|
+
"import pamd_helpers\n",
|
|
154
|
+
"\n",
|
|
155
|
+
"def context():\n",
|
|
156
|
+
" fn1 = pamd_helpers.Footnote(1, \"Comprehensive study published in Journal of Applied Sciences, 2026.\")\n",
|
|
157
|
+
" hyp = pamd_helpers.hyperlink(\"Research Data Repository\", \"https://example.org/dataset\")\n",
|
|
158
|
+
" \n",
|
|
159
|
+
" return {\n",
|
|
160
|
+
" \"fn_ref\": fn1.adnotation(),\n",
|
|
161
|
+
" \"fn_def\": fn1.define_string(),\n",
|
|
162
|
+
" \"data_link\": hyp\n",
|
|
163
|
+
" }\n"
|
|
164
|
+
]
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"cell_type": "markdown",
|
|
168
|
+
"source": [
|
|
169
|
+
"# <glb>project_name</glb>\n",
|
|
170
|
+
"**<glb>author</glb>** (<glb>company</glb>)\n",
|
|
171
|
+
"\n",
|
|
172
|
+
"### Abstract\n",
|
|
173
|
+
"This document explores computational report generation with PyAct.\n",
|
|
174
|
+
"\n",
|
|
175
|
+
"<toc/>\n",
|
|
176
|
+
"\n",
|
|
177
|
+
"<pagebreak/>\n",
|
|
178
|
+
"\n",
|
|
179
|
+
"## 1. Introduction\n",
|
|
180
|
+
"Recent advancements in dynamic markdown have bridged computational logic with formal report generation<ctx>fn_ref</ctx>.\n",
|
|
181
|
+
"\n",
|
|
182
|
+
"The primary datasets are hosted at <ctx>data_link</ctx>.\n",
|
|
183
|
+
"\n",
|
|
184
|
+
"---\n",
|
|
185
|
+
"<ctx>fn_def</ctx>\n"
|
|
186
|
+
]
|
|
187
|
+
}
|
|
188
|
+
]
|
|
189
|
+
else: # default template
|
|
190
|
+
cells = [
|
|
191
|
+
{
|
|
192
|
+
"cell_type": "code",
|
|
193
|
+
"source": [
|
|
194
|
+
"import pamd_helpers\n",
|
|
195
|
+
"\n",
|
|
196
|
+
"def context():\n",
|
|
197
|
+
" eq = pamd_helpers.equation(lambda x, y: x**2 + y, [4, 5])\n",
|
|
198
|
+
" tasks = pamd_helpers.checklist([\"Setup project\", \"Write logic\", \"Export to Word\"])\n",
|
|
199
|
+
" \n",
|
|
200
|
+
" return {\n",
|
|
201
|
+
" \"math\": eq,\n",
|
|
202
|
+
" \"todo\": tasks\n",
|
|
203
|
+
" }\n"
|
|
204
|
+
]
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"cell_type": "markdown",
|
|
208
|
+
"source": [
|
|
209
|
+
"# <glb>project_name</glb>\n",
|
|
210
|
+
"Prepared by: **<glb>author</glb>**\n",
|
|
211
|
+
"\n",
|
|
212
|
+
"<toc/>\n",
|
|
213
|
+
"\n",
|
|
214
|
+
"<pagebreak/>\n",
|
|
215
|
+
"\n",
|
|
216
|
+
"## 1. Overview\n",
|
|
217
|
+
"Welcome to PyAct! This project was initialized with `pyact init`.\n",
|
|
218
|
+
"\n",
|
|
219
|
+
"## 2. Dynamic Computation\n",
|
|
220
|
+
"Here is a mathematical calculation:\n",
|
|
221
|
+
"<ctx>math</ctx>\n",
|
|
222
|
+
"\n",
|
|
223
|
+
"## 3. Project Tasks\n",
|
|
224
|
+
"<ctx>todo</ctx>\n"
|
|
225
|
+
]
|
|
226
|
+
}
|
|
227
|
+
]
|
|
228
|
+
|
|
229
|
+
pamd_content = json.dumps({"cells": cells}, indent=2)
|
|
230
|
+
|
|
231
|
+
files_to_create = {
|
|
232
|
+
"global.py": global_py_content,
|
|
233
|
+
"pamd.conf": pamd_conf_content,
|
|
234
|
+
"style.css": style_css_content,
|
|
235
|
+
".gitignore": gitignore_content,
|
|
236
|
+
"main.pamd": pamd_content
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
created = []
|
|
240
|
+
skipped = []
|
|
241
|
+
for fname, content in files_to_create.items():
|
|
242
|
+
fpath = os.path.join(target_dir, fname)
|
|
243
|
+
if os.path.exists(fpath):
|
|
244
|
+
skipped.append(fname)
|
|
245
|
+
else:
|
|
246
|
+
with open(fpath, "w", encoding="utf-8") as f:
|
|
247
|
+
f.write(content)
|
|
248
|
+
created.append(fname)
|
|
249
|
+
|
|
250
|
+
target_display = target_dir if target_dir != "." else "current directory"
|
|
251
|
+
print(f"Initialized PyAct project in {target_display} (template: {template}):")
|
|
252
|
+
for f in created:
|
|
253
|
+
print(f" + Created {f}")
|
|
254
|
+
for f in skipped:
|
|
255
|
+
print(f" ~ Skipped {f} (already exists)")
|
|
256
|
+
print("\nNext steps:")
|
|
257
|
+
if target_dir != ".":
|
|
258
|
+
print(f" cd {target_dir}")
|
|
259
|
+
print(" pyact main.pamd -o output.md --docx output.docx")
|
|
@@ -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
|
|
@@ -112,4 +112,11 @@ title = Monthly Report
|
|
|
112
112
|
|
|
113
113
|
PyAct will automatically detect `pamd.conf` in your directory, inject dynamic page numbers (`{page}` / `{pages}`), configure tab stops, and populate document metadata!
|
|
114
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
|
+
|
|
115
122
|
That's it! You now have documents ready to be shared, published, or distributed.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.8.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
|