pyact-cli 0.2.0__tar.gz → 0.4.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.4.0/PKG-INFO +74 -0
- pyact_cli-0.4.0/README.md +54 -0
- pyact_cli-0.4.0/pyact/__init__.py +1 -0
- {pyact_cli-0.2.0 → pyact_cli-0.4.0}/pyact/cli.py +22 -0
- pyact_cli-0.4.0/pyact/css2json.py +54 -0
- pyact_cli-0.4.0/pyact/mdTOword.py +425 -0
- pyact_cli-0.4.0/pyact_cli.egg-info/PKG-INFO +74 -0
- {pyact_cli-0.2.0 → pyact_cli-0.4.0}/pyact_cli.egg-info/SOURCES.txt +2 -0
- {pyact_cli-0.2.0 → pyact_cli-0.4.0}/setup.py +4 -4
- pyact_cli-0.2.0/PKG-INFO +0 -44
- pyact_cli-0.2.0/README.md +0 -23
- pyact_cli-0.2.0/pyact/__init__.py +0 -1
- pyact_cli-0.2.0/pyact_cli.egg-info/PKG-INFO +0 -44
- {pyact_cli-0.2.0 → pyact_cli-0.4.0}/pamd_helpers/__init__.py +0 -0
- {pyact_cli-0.2.0 → pyact_cli-0.4.0}/pyact/core.py +0 -0
- {pyact_cli-0.2.0 → pyact_cli-0.4.0}/pyact/py2tex.py +0 -0
- {pyact_cli-0.2.0 → pyact_cli-0.4.0}/pyact_cli.egg-info/dependency_links.txt +0 -0
- {pyact_cli-0.2.0 → pyact_cli-0.4.0}/pyact_cli.egg-info/entry_points.txt +0 -0
- {pyact_cli-0.2.0 → pyact_cli-0.4.0}/pyact_cli.egg-info/top_level.txt +0 -0
- {pyact_cli-0.2.0 → pyact_cli-0.4.0}/setup.cfg +0 -0
pyact_cli-0.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyact-cli
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: A CLI tool to compile .pamd files to Markdown.
|
|
5
|
+
Home-page: https://github.com/Abstergo2003/PyAct
|
|
6
|
+
Author: Abstergo2003
|
|
7
|
+
Author-email:
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Dynamic: author
|
|
14
|
+
Dynamic: classifier
|
|
15
|
+
Dynamic: description
|
|
16
|
+
Dynamic: description-content-type
|
|
17
|
+
Dynamic: home-page
|
|
18
|
+
Dynamic: requires-python
|
|
19
|
+
Dynamic: summary
|
|
20
|
+
|
|
21
|
+
<div align="center">
|
|
22
|
+
<h1>⚡ PyAct CLI</h1>
|
|
23
|
+
<p><b>The engine behind dynamic Markdown.</b></p>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
The **PyAct CLI** allows you to compile `.pamd` files into standard Markdown (`.md`). It evaluates your Python logic, injects your variables, and builds complex markdown elements effortlessly.
|
|
27
|
+
|
|
28
|
+
## 🚀 Installation
|
|
29
|
+
|
|
30
|
+
Install it directly from PyPI:
|
|
31
|
+
```bash
|
|
32
|
+
pip install pyact-cli
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 📖 How it works
|
|
36
|
+
|
|
37
|
+
A `.pamd` file is split into a **Python cell** and a **Markdown cell**.
|
|
38
|
+
|
|
39
|
+
### 1. Write your Python logic
|
|
40
|
+
Define a `context()` function that returns the variables you want to inject. You can even use the built-in `pamd_helpers` to do heavy lifting!
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import pamd_helpers
|
|
44
|
+
|
|
45
|
+
def context():
|
|
46
|
+
# Automatically converts the lambda to LaTeX!
|
|
47
|
+
equation = pamd_helpers.equation(lambda x: x**2, [5])
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
"title": "My PyAct Document",
|
|
51
|
+
"math": equation
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 2. Write your Markdown
|
|
56
|
+
Use `<ctx>` tags to inject your Python variables anywhere in your text.
|
|
57
|
+
|
|
58
|
+
```markdown
|
|
59
|
+
# <ctx>title</ctx>
|
|
60
|
+
|
|
61
|
+
Here is my dynamically generated equation:
|
|
62
|
+
<ctx>math</ctx>
|
|
63
|
+
|
|
64
|
+
You can also include other pamd files as templates!
|
|
65
|
+
<tmp>path/to/another_file</tmp>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 3. Compile!
|
|
69
|
+
Run the compiler from your terminal to generate your final Markdown document:
|
|
70
|
+
```bash
|
|
71
|
+
pyact path/to/your_file.pamd -o output.md
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
That's it! You now have a standard Markdown file ready to be shared, published, or converted to PDF.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h1>⚡ PyAct CLI</h1>
|
|
3
|
+
<p><b>The engine behind dynamic Markdown.</b></p>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
The **PyAct CLI** allows you to compile `.pamd` files into standard Markdown (`.md`). It evaluates your Python logic, injects your variables, and builds complex markdown elements effortlessly.
|
|
7
|
+
|
|
8
|
+
## 🚀 Installation
|
|
9
|
+
|
|
10
|
+
Install it directly from PyPI:
|
|
11
|
+
```bash
|
|
12
|
+
pip install pyact-cli
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 📖 How it works
|
|
16
|
+
|
|
17
|
+
A `.pamd` file is split into a **Python cell** and a **Markdown cell**.
|
|
18
|
+
|
|
19
|
+
### 1. Write your Python logic
|
|
20
|
+
Define a `context()` function that returns the variables you want to inject. You can even use the built-in `pamd_helpers` to do heavy lifting!
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
import pamd_helpers
|
|
24
|
+
|
|
25
|
+
def context():
|
|
26
|
+
# Automatically converts the lambda to LaTeX!
|
|
27
|
+
equation = pamd_helpers.equation(lambda x: x**2, [5])
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
"title": "My PyAct Document",
|
|
31
|
+
"math": equation
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 2. Write your Markdown
|
|
36
|
+
Use `<ctx>` tags to inject your Python variables anywhere in your text.
|
|
37
|
+
|
|
38
|
+
```markdown
|
|
39
|
+
# <ctx>title</ctx>
|
|
40
|
+
|
|
41
|
+
Here is my dynamically generated equation:
|
|
42
|
+
<ctx>math</ctx>
|
|
43
|
+
|
|
44
|
+
You can also include other pamd files as templates!
|
|
45
|
+
<tmp>path/to/another_file</tmp>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 3. Compile!
|
|
49
|
+
Run the compiler from your terminal to generate your final Markdown document:
|
|
50
|
+
```bash
|
|
51
|
+
pyact path/to/your_file.pamd -o output.md
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
That's it! You now have a standard Markdown file ready to be shared, published, or converted to PDF.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0"
|
|
@@ -7,6 +7,8 @@ def main():
|
|
|
7
7
|
parser = argparse.ArgumentParser(description="PyAct CLI - PAMD to Markdown compiler")
|
|
8
8
|
parser.add_argument("input", help="Path to the main .pamd file")
|
|
9
9
|
parser.add_argument("-o", "--output", help="Output file path (default prints to stdout)")
|
|
10
|
+
parser.add_argument("--docx", help="Also generate a DOCX file at this path")
|
|
11
|
+
parser.add_argument("--css", help="Optional CSS file path to style the DOCX")
|
|
10
12
|
|
|
11
13
|
args = parser.parse_args()
|
|
12
14
|
|
|
@@ -27,6 +29,26 @@ def main():
|
|
|
27
29
|
print(f"Successfully compiled to {args.output}")
|
|
28
30
|
else:
|
|
29
31
|
print(content)
|
|
32
|
+
|
|
33
|
+
if args.docx:
|
|
34
|
+
from .mdTOword import style_parser, markdown_parser, docx_writer
|
|
35
|
+
styles = {}
|
|
36
|
+
if args.css:
|
|
37
|
+
styles = style_parser(args.css)
|
|
38
|
+
else:
|
|
39
|
+
# Try to look for style.css in the active directory
|
|
40
|
+
local_css = os.path.join(directory, "style.css")
|
|
41
|
+
if os.path.exists(local_css):
|
|
42
|
+
styles = style_parser(local_css)
|
|
43
|
+
else:
|
|
44
|
+
# Fallback to the default one packaged in pyact
|
|
45
|
+
default_css = os.path.join(os.path.dirname(__file__), "style.css")
|
|
46
|
+
styles = style_parser(default_css)
|
|
47
|
+
|
|
48
|
+
blocks = markdown_parser(content)
|
|
49
|
+
docx_writer(blocks, styles, args.docx)
|
|
50
|
+
print(f"Successfully compiled DOCX to {args.docx}")
|
|
51
|
+
|
|
30
52
|
except Exception as e:
|
|
31
53
|
print(f"Error: {e}", file=sys.stderr)
|
|
32
54
|
sys.exit(1)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
def css_to_dict(css_string: str) -> dict:
|
|
5
|
+
"""Parses a CSS string into a Python dictionary."""
|
|
6
|
+
# Remove CSS comments
|
|
7
|
+
css_string = re.sub(r'/\*[\s\S]*?\*/', '', css_string)
|
|
8
|
+
|
|
9
|
+
# Match selectors and their corresponding blocks
|
|
10
|
+
pattern = r'([^{]+)\{([^}]+)\}'
|
|
11
|
+
matches = re.findall(pattern, css_string)
|
|
12
|
+
|
|
13
|
+
css_dict = {}
|
|
14
|
+
for selector, block in matches:
|
|
15
|
+
selector = selector.strip()
|
|
16
|
+
|
|
17
|
+
# Parse individual CSS properties
|
|
18
|
+
rules = {}
|
|
19
|
+
for line in block.split(';'):
|
|
20
|
+
line = line.strip()
|
|
21
|
+
if not line:
|
|
22
|
+
continue
|
|
23
|
+
if ':' in line:
|
|
24
|
+
key, val = line.split(':', 1)
|
|
25
|
+
rules[key.strip()] = val.strip()
|
|
26
|
+
|
|
27
|
+
# Handle comma-separated selectors (e.g., 'h1, h2, h3')
|
|
28
|
+
for sel in selector.split(','):
|
|
29
|
+
sel = sel.strip()
|
|
30
|
+
if sel:
|
|
31
|
+
if sel not in css_dict:
|
|
32
|
+
css_dict[sel] = {}
|
|
33
|
+
css_dict[sel].update(rules)
|
|
34
|
+
|
|
35
|
+
return css_dict
|
|
36
|
+
|
|
37
|
+
def parse_css_file(file_path: str, as_json_string: bool = False):
|
|
38
|
+
"""
|
|
39
|
+
Reads a CSS file and converts it to a dictionary map.
|
|
40
|
+
If as_json_string is True, returns a formatted JSON string instead.
|
|
41
|
+
"""
|
|
42
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
43
|
+
css_string = f.read()
|
|
44
|
+
|
|
45
|
+
css_dict = css_to_dict(css_string)
|
|
46
|
+
|
|
47
|
+
if as_json_string:
|
|
48
|
+
return json.dumps(css_dict, indent=4)
|
|
49
|
+
return css_dict
|
|
50
|
+
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
# Example usage:
|
|
53
|
+
# print(parse_css_file("style.css", as_json_string=True))
|
|
54
|
+
pass
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import re
|
|
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
|
|
7
|
+
import math2docx
|
|
8
|
+
from . import css2json
|
|
9
|
+
|
|
10
|
+
def style_parser(css_path):
|
|
11
|
+
"""
|
|
12
|
+
Reads a CSS file and converts it into a style dictionary using css2json.
|
|
13
|
+
"""
|
|
14
|
+
try:
|
|
15
|
+
return css2json.parse_css_file(css_path)
|
|
16
|
+
except Exception as e:
|
|
17
|
+
print(f"Warning: Could not parse CSS ({e})")
|
|
18
|
+
return {}
|
|
19
|
+
|
|
20
|
+
def markdown_parser(md_text):
|
|
21
|
+
"""
|
|
22
|
+
Parses Markdown text into a list of structured blocks.
|
|
23
|
+
Supported blocks: heading, paragraph, list, table, math
|
|
24
|
+
"""
|
|
25
|
+
blocks = []
|
|
26
|
+
lines = md_text.split('\n')
|
|
27
|
+
|
|
28
|
+
current_table = []
|
|
29
|
+
|
|
30
|
+
for line in lines:
|
|
31
|
+
line_s = line.strip()
|
|
32
|
+
|
|
33
|
+
# Table Parsing
|
|
34
|
+
if line_s.startswith('|') and line_s.endswith('|'):
|
|
35
|
+
current_table.append(line_s)
|
|
36
|
+
continue
|
|
37
|
+
elif current_table:
|
|
38
|
+
blocks.append(('table', current_table))
|
|
39
|
+
current_table = []
|
|
40
|
+
|
|
41
|
+
if not line_s:
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
# Heading Parsing
|
|
45
|
+
if line_s.startswith('#'):
|
|
46
|
+
level = len(line_s) - len(line_s.lstrip('#'))
|
|
47
|
+
text = line_s.lstrip('#').strip()
|
|
48
|
+
blocks.append(('heading', level, text))
|
|
49
|
+
|
|
50
|
+
# List Parsing
|
|
51
|
+
elif line_s.startswith('- ') or line_s.startswith('* '):
|
|
52
|
+
text = line_s[2:].strip()
|
|
53
|
+
blocks.append(('list', text, 'unordered'))
|
|
54
|
+
|
|
55
|
+
elif re.match(r'^\d+\.\s+', line_s):
|
|
56
|
+
m = re.match(r'^\d+\.\s+(.*)', line_s)
|
|
57
|
+
blocks.append(('list', m.group(1), 'ordered'))
|
|
58
|
+
|
|
59
|
+
# Math Block Parsing
|
|
60
|
+
elif line_s.startswith('$$') and line_s.endswith('$$'):
|
|
61
|
+
blocks.append(('math', line_s.strip('$').strip()))
|
|
62
|
+
|
|
63
|
+
# Standard Paragraph
|
|
64
|
+
else:
|
|
65
|
+
blocks.append(('paragraph', line_s))
|
|
66
|
+
|
|
67
|
+
if current_table:
|
|
68
|
+
blocks.append(('table', current_table))
|
|
69
|
+
|
|
70
|
+
return blocks
|
|
71
|
+
|
|
72
|
+
def _apply_css_to_run(run, css_rules):
|
|
73
|
+
"""Helper to apply CSS rules to a docx run or font."""
|
|
74
|
+
font = run.font if hasattr(run, 'font') else run
|
|
75
|
+
|
|
76
|
+
if 'color' in css_rules:
|
|
77
|
+
hex_col = css_rules['color'].replace('#', '').strip()
|
|
78
|
+
if len(hex_col) == 6:
|
|
79
|
+
try:
|
|
80
|
+
r, g, b = tuple(int(hex_col[i:i+2], 16) for i in (0, 2, 4))
|
|
81
|
+
font.color.rgb = RGBColor(r, g, b)
|
|
82
|
+
except ValueError:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
if 'font-size' in css_rules:
|
|
86
|
+
size = css_rules['font-size'].replace('pt', '').strip()
|
|
87
|
+
try:
|
|
88
|
+
font.size = Pt(float(size))
|
|
89
|
+
except ValueError:
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
if 'font-family' in css_rules:
|
|
93
|
+
font.name = css_rules['font-family'].strip("'\"")
|
|
94
|
+
|
|
95
|
+
if 'font-weight' in css_rules:
|
|
96
|
+
font.bold = (css_rules['font-weight'].lower() == 'bold')
|
|
97
|
+
|
|
98
|
+
if 'font-style' in css_rules:
|
|
99
|
+
font.italic = (css_rules['font-style'].lower() == 'italic')
|
|
100
|
+
|
|
101
|
+
if 'text-decoration' in css_rules:
|
|
102
|
+
dec = css_rules['text-decoration'].lower()
|
|
103
|
+
if dec == 'underline':
|
|
104
|
+
font.underline = True
|
|
105
|
+
elif dec == 'line-through':
|
|
106
|
+
font.strike = True
|
|
107
|
+
elif dec == 'none':
|
|
108
|
+
font.underline = False
|
|
109
|
+
font.strike = False
|
|
110
|
+
|
|
111
|
+
if 'text-transform' in css_rules:
|
|
112
|
+
trans = css_rules['text-transform'].lower()
|
|
113
|
+
if trans == 'uppercase':
|
|
114
|
+
font.all_caps = True
|
|
115
|
+
elif trans == 'small-caps':
|
|
116
|
+
font.small_caps = True
|
|
117
|
+
|
|
118
|
+
if 'background-color' in css_rules:
|
|
119
|
+
# Maps text background to highlight color index
|
|
120
|
+
from docx.enum.text import WD_COLOR_INDEX
|
|
121
|
+
color_map = {
|
|
122
|
+
'auto': WD_COLOR_INDEX.AUTO, 'black': WD_COLOR_INDEX.BLACK, 'blue': WD_COLOR_INDEX.BLUE,
|
|
123
|
+
'bright-green': WD_COLOR_INDEX.BRIGHT_GREEN, 'dark-blue': WD_COLOR_INDEX.DARK_BLUE,
|
|
124
|
+
'dark-red': WD_COLOR_INDEX.DARK_RED, 'dark-yellow': WD_COLOR_INDEX.DARK_YELLOW,
|
|
125
|
+
'gray-25': WD_COLOR_INDEX.GRAY_25, 'gray-50': WD_COLOR_INDEX.GRAY_50,
|
|
126
|
+
'green': WD_COLOR_INDEX.GREEN, 'pink': WD_COLOR_INDEX.PINK, 'red': WD_COLOR_INDEX.RED,
|
|
127
|
+
'teal': WD_COLOR_INDEX.TEAL, 'turquoise': WD_COLOR_INDEX.TURQUOISE,
|
|
128
|
+
'violet': WD_COLOR_INDEX.VIOLET, 'white': WD_COLOR_INDEX.WHITE, 'yellow': WD_COLOR_INDEX.YELLOW
|
|
129
|
+
}
|
|
130
|
+
bg_col = css_rules['background-color'].lower()
|
|
131
|
+
if bg_col in color_map:
|
|
132
|
+
font.highlight_color = color_map[bg_col]
|
|
133
|
+
|
|
134
|
+
def _apply_paragraph_formatting(p, css_rules):
|
|
135
|
+
"""Helper to apply CSS rules to paragraph formatting."""
|
|
136
|
+
fmt = p.paragraph_format
|
|
137
|
+
|
|
138
|
+
if 'text-align' in css_rules:
|
|
139
|
+
align = css_rules['text-align'].lower()
|
|
140
|
+
if align == 'center':
|
|
141
|
+
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
142
|
+
elif align == 'right':
|
|
143
|
+
p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
|
|
144
|
+
elif align == 'justify':
|
|
145
|
+
p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
|
146
|
+
elif align == 'distribute':
|
|
147
|
+
p.alignment = WD_ALIGN_PARAGRAPH.DISTRIBUTE
|
|
148
|
+
elif align == 'left':
|
|
149
|
+
p.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
|
150
|
+
|
|
151
|
+
if 'margin-top' in css_rules:
|
|
152
|
+
val = css_rules['margin-top'].replace('pt', '').strip()
|
|
153
|
+
try:
|
|
154
|
+
fmt.space_before = Pt(float(val))
|
|
155
|
+
except ValueError:
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
if 'margin-bottom' in css_rules:
|
|
159
|
+
val = css_rules['margin-bottom'].replace('pt', '').strip()
|
|
160
|
+
try:
|
|
161
|
+
fmt.space_after = Pt(float(val))
|
|
162
|
+
except ValueError:
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
if 'line-height' in css_rules:
|
|
166
|
+
try:
|
|
167
|
+
fmt.line_spacing = float(css_rules['line-height'])
|
|
168
|
+
except ValueError:
|
|
169
|
+
pass
|
|
170
|
+
|
|
171
|
+
if 'page-break-before' in css_rules:
|
|
172
|
+
if css_rules['page-break-before'].lower() == 'always':
|
|
173
|
+
fmt.page_break_before = True
|
|
174
|
+
|
|
175
|
+
import docx.opc.constants
|
|
176
|
+
from docx.oxml.shared import OxmlElement, qn
|
|
177
|
+
import urllib.request
|
|
178
|
+
import io
|
|
179
|
+
import random
|
|
180
|
+
|
|
181
|
+
def _add_internal_hyperlink(paragraph, text, anchor):
|
|
182
|
+
"""Adds a clickable internal hyperlink to a bookmark anchor in the docx."""
|
|
183
|
+
hyperlink = OxmlElement('w:hyperlink')
|
|
184
|
+
hyperlink.set(qn('w:anchor'), anchor)
|
|
185
|
+
|
|
186
|
+
new_run = OxmlElement('w:r')
|
|
187
|
+
rPr = OxmlElement('w:rPr')
|
|
188
|
+
|
|
189
|
+
# Make it superscript since it's usually a footnote marker
|
|
190
|
+
vertAlign = OxmlElement('w:vertAlign')
|
|
191
|
+
vertAlign.set(qn('w:val'), 'superscript')
|
|
192
|
+
rPr.append(vertAlign)
|
|
193
|
+
|
|
194
|
+
# Make it blue
|
|
195
|
+
c = OxmlElement('w:color')
|
|
196
|
+
c.set(qn('w:val'), '0563C1')
|
|
197
|
+
rPr.append(c)
|
|
198
|
+
|
|
199
|
+
new_run.append(rPr)
|
|
200
|
+
|
|
201
|
+
text_elem = OxmlElement('w:t')
|
|
202
|
+
text_elem.text = text
|
|
203
|
+
new_run.append(text_elem)
|
|
204
|
+
|
|
205
|
+
hyperlink.append(new_run)
|
|
206
|
+
paragraph._p.append(hyperlink)
|
|
207
|
+
|
|
208
|
+
def _add_bookmark(paragraph, text, anchor):
|
|
209
|
+
"""Creates a bookmark anchor in the document and places text inside it."""
|
|
210
|
+
# We need a random ID to prevent collisions
|
|
211
|
+
bm_id = str(random.randint(10000, 99999))
|
|
212
|
+
|
|
213
|
+
bm_start = OxmlElement('w:bookmarkStart')
|
|
214
|
+
bm_start.set(qn('w:id'), bm_id)
|
|
215
|
+
bm_start.set(qn('w:name'), anchor)
|
|
216
|
+
paragraph._p.append(bm_start)
|
|
217
|
+
|
|
218
|
+
run = paragraph.add_run(text)
|
|
219
|
+
|
|
220
|
+
bm_end = OxmlElement('w:bookmarkEnd')
|
|
221
|
+
bm_end.set(qn('w:id'), bm_id)
|
|
222
|
+
paragraph._p.append(bm_end)
|
|
223
|
+
return run
|
|
224
|
+
|
|
225
|
+
def _add_hyperlink(paragraph, text, url, styles, tag):
|
|
226
|
+
"""Adds a real hyperlink to a docx paragraph."""
|
|
227
|
+
part = paragraph.part
|
|
228
|
+
r_id = part.relate_to(url, docx.opc.constants.RELATIONSHIP_TYPE.HYPERLINK, is_external=True)
|
|
229
|
+
|
|
230
|
+
hyperlink = OxmlElement('w:hyperlink')
|
|
231
|
+
hyperlink.set(qn('r:id'), r_id)
|
|
232
|
+
|
|
233
|
+
new_run = OxmlElement('w:r')
|
|
234
|
+
rPr = OxmlElement('w:rPr')
|
|
235
|
+
|
|
236
|
+
c = OxmlElement('w:color')
|
|
237
|
+
if 'hyperlink' in styles and 'color' in styles['hyperlink']:
|
|
238
|
+
color_hex = styles['hyperlink']['color'].replace('#', '').strip()
|
|
239
|
+
c.set(qn('w:val'), color_hex)
|
|
240
|
+
else:
|
|
241
|
+
c.set(qn('w:val'), '0563C1')
|
|
242
|
+
rPr.append(c)
|
|
243
|
+
|
|
244
|
+
u = OxmlElement('w:u')
|
|
245
|
+
if 'hyperlink' in styles and 'text-decoration' in styles['hyperlink'] and styles['hyperlink']['text-decoration'] == 'none':
|
|
246
|
+
pass
|
|
247
|
+
else:
|
|
248
|
+
u.set(qn('w:val'), 'single')
|
|
249
|
+
rPr.append(u)
|
|
250
|
+
|
|
251
|
+
new_run.append(rPr)
|
|
252
|
+
|
|
253
|
+
text_elem = OxmlElement('w:t')
|
|
254
|
+
text_elem.text = text
|
|
255
|
+
new_run.append(text_elem)
|
|
256
|
+
|
|
257
|
+
hyperlink.append(new_run)
|
|
258
|
+
paragraph._p.append(hyperlink)
|
|
259
|
+
|
|
260
|
+
def _add_image(paragraph, url, caption, styles):
|
|
261
|
+
"""Adds an inline image fetched from URL."""
|
|
262
|
+
try:
|
|
263
|
+
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
|
|
264
|
+
with urllib.request.urlopen(req) as response:
|
|
265
|
+
image_stream = io.BytesIO(response.read())
|
|
266
|
+
|
|
267
|
+
run = paragraph.add_run()
|
|
268
|
+
|
|
269
|
+
# Apply CSS dimensions if available
|
|
270
|
+
width = None
|
|
271
|
+
if 'image' in styles and 'width' in styles['image']:
|
|
272
|
+
w_str = styles['image']['width']
|
|
273
|
+
if 'in' in w_str: width = docx.shared.Inches(float(w_str.replace('in', '').strip()))
|
|
274
|
+
|
|
275
|
+
if width:
|
|
276
|
+
run.add_picture(image_stream, width=width)
|
|
277
|
+
else:
|
|
278
|
+
run.add_picture(image_stream, width=docx.shared.Inches(4))
|
|
279
|
+
|
|
280
|
+
except Exception as e:
|
|
281
|
+
paragraph.add_run(f"[Image Failed: {url}]")
|
|
282
|
+
|
|
283
|
+
def _process_inline(paragraph, text, styles, tag):
|
|
284
|
+
"""Parses text for images, links, math, and bold/italic."""
|
|
285
|
+
# Simple regex to split by math, image, or link. Captures the match as a group.
|
|
286
|
+
# Group 5: Footnote definition [^1]: ...
|
|
287
|
+
# Group 6: Footnote annotation [^1]
|
|
288
|
+
pattern = r'(\$\$.*?\$\$)|(!\[.*?\]\(.*?\))|(\[.*?\]\(.*?\))|(<span.*?>.*?</span>)|(\[\^.*?\]:.*?$)|(\[\^.*?\])'
|
|
289
|
+
parts = re.split(pattern, text)
|
|
290
|
+
|
|
291
|
+
for part in parts:
|
|
292
|
+
if not part: continue
|
|
293
|
+
|
|
294
|
+
if part.startswith('$$') and part.endswith('$$'):
|
|
295
|
+
math_text = part.strip('$').strip()
|
|
296
|
+
try:
|
|
297
|
+
math2docx.add_math(paragraph, math_text)
|
|
298
|
+
except Exception:
|
|
299
|
+
run = paragraph.add_run(part)
|
|
300
|
+
if tag in styles: _apply_css_to_run(run, styles[tag])
|
|
301
|
+
|
|
302
|
+
elif part.startswith('![') and part.endswith(')'):
|
|
303
|
+
m = re.match(r'!\[(.*?)\]\((.*?)\)', part)
|
|
304
|
+
if m:
|
|
305
|
+
_add_image(paragraph, m.group(2), m.group(1), styles)
|
|
306
|
+
|
|
307
|
+
elif part.startswith('[') and part.endswith(')'):
|
|
308
|
+
m = re.match(r'\[(.*?)\]\((.*?)\)', part)
|
|
309
|
+
if m:
|
|
310
|
+
_add_hyperlink(paragraph, m.group(1), m.group(2), styles, tag)
|
|
311
|
+
|
|
312
|
+
elif part.startswith('<span') and part.endswith('</span>'):
|
|
313
|
+
m = re.match(r'<span.*?>(.*?)</span>', part)
|
|
314
|
+
if m:
|
|
315
|
+
caption_text = m.group(1)
|
|
316
|
+
run = paragraph.add_run(caption_text)
|
|
317
|
+
run.italic = True
|
|
318
|
+
paragraph.alignment = docx.enum.text.WD_ALIGN_PARAGRAPH.CENTER
|
|
319
|
+
if tag in styles: _apply_css_to_run(run, styles[tag])
|
|
320
|
+
|
|
321
|
+
elif part.startswith('[^') and ']:' in part:
|
|
322
|
+
# Footnote definition e.g. [^1]: The text
|
|
323
|
+
m = re.match(r'\[\^(.*?)\]:\s*(.*)', part)
|
|
324
|
+
if m:
|
|
325
|
+
fn_id = m.group(1)
|
|
326
|
+
fn_text = m.group(2)
|
|
327
|
+
# Create a bookmark here for the annotation to jump to
|
|
328
|
+
run = _add_bookmark(paragraph, f"[{fn_id}]: {fn_text}", f"footnote_{fn_id}")
|
|
329
|
+
if tag in styles: _apply_css_to_run(run, styles[tag])
|
|
330
|
+
|
|
331
|
+
elif part.startswith('[^') and part.endswith(']'):
|
|
332
|
+
# Footnote annotation e.g. [^1]
|
|
333
|
+
m = re.match(r'\[\^(.*?)\]', part)
|
|
334
|
+
if m:
|
|
335
|
+
fn_id = m.group(1)
|
|
336
|
+
_add_internal_hyperlink(paragraph, f"[{fn_id}]", f"footnote_{fn_id}")
|
|
337
|
+
|
|
338
|
+
else:
|
|
339
|
+
# Handle plain text + bold/italic
|
|
340
|
+
subparts = re.split(r'(\*\*.*?\*\*|\*.*?\*)', part)
|
|
341
|
+
for subpart in subparts:
|
|
342
|
+
if not subpart: continue
|
|
343
|
+
if subpart.startswith('**') and subpart.endswith('**'):
|
|
344
|
+
run = paragraph.add_run(subpart[2:-2])
|
|
345
|
+
run.bold = True
|
|
346
|
+
if tag in styles: _apply_css_to_run(run, styles[tag])
|
|
347
|
+
elif subpart.startswith('*') and subpart.endswith('*'):
|
|
348
|
+
run = paragraph.add_run(subpart[1:-1])
|
|
349
|
+
run.italic = True
|
|
350
|
+
if tag in styles: _apply_css_to_run(run, styles[tag])
|
|
351
|
+
else:
|
|
352
|
+
run = paragraph.add_run(subpart)
|
|
353
|
+
if tag in styles: _apply_css_to_run(run, styles[tag])
|
|
354
|
+
|
|
355
|
+
def docx_writer(blocks: list, styles: dict, output_file: str):
|
|
356
|
+
"""
|
|
357
|
+
Translates parsed markdown blocks into a Word document and applies CSS styling.
|
|
358
|
+
"""
|
|
359
|
+
doc = Document()
|
|
360
|
+
|
|
361
|
+
# Global body style
|
|
362
|
+
if 'body' in styles:
|
|
363
|
+
normal_style = doc.styles['Normal']
|
|
364
|
+
_apply_css_to_run(normal_style, styles['body'])
|
|
365
|
+
_apply_paragraph_formatting(normal_style, styles['body'])
|
|
366
|
+
|
|
367
|
+
for block in blocks:
|
|
368
|
+
btype = block[0]
|
|
369
|
+
|
|
370
|
+
if btype == 'heading':
|
|
371
|
+
level, text = block[1], block[2]
|
|
372
|
+
p = doc.add_heading('', level=level)
|
|
373
|
+
_process_inline(p, text, styles, f'h{level}')
|
|
374
|
+
if f'h{level}' in styles:
|
|
375
|
+
_apply_paragraph_formatting(p, styles[f'h{level}'])
|
|
376
|
+
|
|
377
|
+
elif btype == 'paragraph':
|
|
378
|
+
text = block[1]
|
|
379
|
+
p = doc.add_paragraph()
|
|
380
|
+
if 'p' in styles:
|
|
381
|
+
_apply_paragraph_formatting(p, styles['p'])
|
|
382
|
+
_process_inline(p, text, styles, 'p')
|
|
383
|
+
|
|
384
|
+
elif btype == 'list':
|
|
385
|
+
text = block[1]
|
|
386
|
+
list_type = block[2] if len(block) > 2 else 'unordered'
|
|
387
|
+
style = 'List Number' if list_type == 'ordered' else 'List Bullet'
|
|
388
|
+
p = doc.add_paragraph(style=style)
|
|
389
|
+
if 'list' in styles:
|
|
390
|
+
_apply_paragraph_formatting(p, styles['list'])
|
|
391
|
+
_process_inline(p, text, styles, 'list')
|
|
392
|
+
|
|
393
|
+
elif btype == 'math':
|
|
394
|
+
math_text = block[1]
|
|
395
|
+
p = doc.add_paragraph()
|
|
396
|
+
try:
|
|
397
|
+
math2docx.add_math(p, math_text)
|
|
398
|
+
if 'equation' in styles:
|
|
399
|
+
_apply_paragraph_formatting(p, styles['equation'])
|
|
400
|
+
except Exception:
|
|
401
|
+
p.add_run(f"$$ {math_text} $$")
|
|
402
|
+
|
|
403
|
+
elif btype == 'table':
|
|
404
|
+
table_lines = block[1]
|
|
405
|
+
rows = [
|
|
406
|
+
[cell.strip() for cell in line.strip('|').split('|')]
|
|
407
|
+
for line in table_lines if '---' not in line
|
|
408
|
+
]
|
|
409
|
+
if rows:
|
|
410
|
+
table = doc.add_table(rows=len(rows), cols=len(rows[0]))
|
|
411
|
+
for r_idx, row in enumerate(rows):
|
|
412
|
+
for c_idx, val in enumerate(row):
|
|
413
|
+
table.cell(r_idx, c_idx).text = val
|
|
414
|
+
if 'table' in styles:
|
|
415
|
+
pass
|
|
416
|
+
|
|
417
|
+
doc.save(output_file)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
if __name__ == "__main__":
|
|
421
|
+
# Example Usage:
|
|
422
|
+
styles = style_parser('engine/style.css')
|
|
423
|
+
md_blocks = markdown_parser('## Hello World\nThis is a **bold** test.')
|
|
424
|
+
docx_writer(md_blocks, styles, 'output.docx')
|
|
425
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyact-cli
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: A CLI tool to compile .pamd files to Markdown.
|
|
5
|
+
Home-page: https://github.com/Abstergo2003/PyAct
|
|
6
|
+
Author: Abstergo2003
|
|
7
|
+
Author-email:
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Dynamic: author
|
|
14
|
+
Dynamic: classifier
|
|
15
|
+
Dynamic: description
|
|
16
|
+
Dynamic: description-content-type
|
|
17
|
+
Dynamic: home-page
|
|
18
|
+
Dynamic: requires-python
|
|
19
|
+
Dynamic: summary
|
|
20
|
+
|
|
21
|
+
<div align="center">
|
|
22
|
+
<h1>⚡ PyAct CLI</h1>
|
|
23
|
+
<p><b>The engine behind dynamic Markdown.</b></p>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
The **PyAct CLI** allows you to compile `.pamd` files into standard Markdown (`.md`). It evaluates your Python logic, injects your variables, and builds complex markdown elements effortlessly.
|
|
27
|
+
|
|
28
|
+
## 🚀 Installation
|
|
29
|
+
|
|
30
|
+
Install it directly from PyPI:
|
|
31
|
+
```bash
|
|
32
|
+
pip install pyact-cli
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 📖 How it works
|
|
36
|
+
|
|
37
|
+
A `.pamd` file is split into a **Python cell** and a **Markdown cell**.
|
|
38
|
+
|
|
39
|
+
### 1. Write your Python logic
|
|
40
|
+
Define a `context()` function that returns the variables you want to inject. You can even use the built-in `pamd_helpers` to do heavy lifting!
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import pamd_helpers
|
|
44
|
+
|
|
45
|
+
def context():
|
|
46
|
+
# Automatically converts the lambda to LaTeX!
|
|
47
|
+
equation = pamd_helpers.equation(lambda x: x**2, [5])
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
"title": "My PyAct Document",
|
|
51
|
+
"math": equation
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 2. Write your Markdown
|
|
56
|
+
Use `<ctx>` tags to inject your Python variables anywhere in your text.
|
|
57
|
+
|
|
58
|
+
```markdown
|
|
59
|
+
# <ctx>title</ctx>
|
|
60
|
+
|
|
61
|
+
Here is my dynamically generated equation:
|
|
62
|
+
<ctx>math</ctx>
|
|
63
|
+
|
|
64
|
+
You can also include other pamd files as templates!
|
|
65
|
+
<tmp>path/to/another_file</tmp>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 3. Compile!
|
|
69
|
+
Run the compiler from your terminal to generate your final Markdown document:
|
|
70
|
+
```bash
|
|
71
|
+
pyact path/to/your_file.pamd -o output.md
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
That's it! You now have a standard Markdown file ready to be shared, published, or converted to PDF.
|
|
@@ -5,13 +5,13 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
|
|
5
5
|
|
|
6
6
|
setup(
|
|
7
7
|
name="pyact-cli",
|
|
8
|
-
version="0.
|
|
9
|
-
author="
|
|
10
|
-
author_email="
|
|
8
|
+
version="0.4.0",
|
|
9
|
+
author="Abstergo2003",
|
|
10
|
+
author_email="",
|
|
11
11
|
description="A CLI tool to compile .pamd files to Markdown.",
|
|
12
12
|
long_description=long_description,
|
|
13
13
|
long_description_content_type="text/markdown",
|
|
14
|
-
url="https://github.com/
|
|
14
|
+
url="https://github.com/Abstergo2003/PyAct",
|
|
15
15
|
packages=find_packages(),
|
|
16
16
|
classifiers=[
|
|
17
17
|
"Programming Language :: Python :: 3",
|
pyact_cli-0.2.0/PKG-INFO
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: pyact-cli
|
|
3
|
-
Version: 0.2.0
|
|
4
|
-
Summary: A CLI tool to compile .pamd files to Markdown.
|
|
5
|
-
Home-page: https://github.com/yourusername/pyact
|
|
6
|
-
Author: Your Name
|
|
7
|
-
Author-email: your.email@example.com
|
|
8
|
-
Classifier: Programming Language :: Python :: 3
|
|
9
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
-
Classifier: Operating System :: OS Independent
|
|
11
|
-
Requires-Python: >=3.8
|
|
12
|
-
Description-Content-Type: text/markdown
|
|
13
|
-
Dynamic: author
|
|
14
|
-
Dynamic: author-email
|
|
15
|
-
Dynamic: classifier
|
|
16
|
-
Dynamic: description
|
|
17
|
-
Dynamic: description-content-type
|
|
18
|
-
Dynamic: home-page
|
|
19
|
-
Dynamic: requires-python
|
|
20
|
-
Dynamic: summary
|
|
21
|
-
|
|
22
|
-
# PyAct CLI
|
|
23
|
-
|
|
24
|
-
PyAct CLI is a tool designed to process `.pamd` files, which bundle Python logic and Markdown content together, similar to a Jupyter Notebook. This compiler resolves variables and dependencies to generate standard Markdown output.
|
|
25
|
-
|
|
26
|
-
## Installation
|
|
27
|
-
|
|
28
|
-
You can install it directly from source (or after uploading to PyPI):
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
pip install pyact-cli
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
## Usage
|
|
35
|
-
|
|
36
|
-
Compile a `.pamd` file and output to the terminal:
|
|
37
|
-
```bash
|
|
38
|
-
pyact path/to/your_file.pamd
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
Compile and save the output to a new Markdown file:
|
|
42
|
-
```bash
|
|
43
|
-
pyact path/to/your_file.pamd -o output.md
|
|
44
|
-
```
|
pyact_cli-0.2.0/README.md
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# PyAct CLI
|
|
2
|
-
|
|
3
|
-
PyAct CLI is a tool designed to process `.pamd` files, which bundle Python logic and Markdown content together, similar to a Jupyter Notebook. This compiler resolves variables and dependencies to generate standard Markdown output.
|
|
4
|
-
|
|
5
|
-
## Installation
|
|
6
|
-
|
|
7
|
-
You can install it directly from source (or after uploading to PyPI):
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
pip install pyact-cli
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
## Usage
|
|
14
|
-
|
|
15
|
-
Compile a `.pamd` file and output to the terminal:
|
|
16
|
-
```bash
|
|
17
|
-
pyact path/to/your_file.pamd
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
Compile and save the output to a new Markdown file:
|
|
21
|
-
```bash
|
|
22
|
-
pyact path/to/your_file.pamd -o output.md
|
|
23
|
-
```
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.0"
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: pyact-cli
|
|
3
|
-
Version: 0.2.0
|
|
4
|
-
Summary: A CLI tool to compile .pamd files to Markdown.
|
|
5
|
-
Home-page: https://github.com/yourusername/pyact
|
|
6
|
-
Author: Your Name
|
|
7
|
-
Author-email: your.email@example.com
|
|
8
|
-
Classifier: Programming Language :: Python :: 3
|
|
9
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
-
Classifier: Operating System :: OS Independent
|
|
11
|
-
Requires-Python: >=3.8
|
|
12
|
-
Description-Content-Type: text/markdown
|
|
13
|
-
Dynamic: author
|
|
14
|
-
Dynamic: author-email
|
|
15
|
-
Dynamic: classifier
|
|
16
|
-
Dynamic: description
|
|
17
|
-
Dynamic: description-content-type
|
|
18
|
-
Dynamic: home-page
|
|
19
|
-
Dynamic: requires-python
|
|
20
|
-
Dynamic: summary
|
|
21
|
-
|
|
22
|
-
# PyAct CLI
|
|
23
|
-
|
|
24
|
-
PyAct CLI is a tool designed to process `.pamd` files, which bundle Python logic and Markdown content together, similar to a Jupyter Notebook. This compiler resolves variables and dependencies to generate standard Markdown output.
|
|
25
|
-
|
|
26
|
-
## Installation
|
|
27
|
-
|
|
28
|
-
You can install it directly from source (or after uploading to PyPI):
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
pip install pyact-cli
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
## Usage
|
|
35
|
-
|
|
36
|
-
Compile a `.pamd` file and output to the terminal:
|
|
37
|
-
```bash
|
|
38
|
-
pyact path/to/your_file.pamd
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
Compile and save the output to a new Markdown file:
|
|
42
|
-
```bash
|
|
43
|
-
pyact path/to/your_file.pamd -o output.md
|
|
44
|
-
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|