pyact-cli 0.3.0__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyact-cli
3
- Version: 0.3.0
3
+ Version: 0.5.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
@@ -65,10 +65,17 @@ You can also include other pamd files as templates!
65
65
  <tmp>path/to/another_file</tmp>
66
66
  ```
67
67
 
68
- ### 3. Compile!
68
+ ### 3. Compile to Markdown
69
69
  Run the compiler from your terminal to generate your final Markdown document:
70
70
  ```bash
71
71
  pyact path/to/your_file.pamd -o output.md
72
72
  ```
73
73
 
74
- That's it! You now have a standard Markdown file ready to be shared, published, or converted to PDF.
74
+ ### 4. Compile to Word (DOCX)
75
+ You can also compile straight to a native Microsoft Word Document, complete with CSS styling!
76
+ ```bash
77
+ pyact path/to/your_file.pamd -o output.md --docx output.docx
78
+ ```
79
+ *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!*
80
+
81
+ That's it! You now have documents ready to be shared, published, or distributed.
@@ -45,10 +45,17 @@ You can also include other pamd files as templates!
45
45
  <tmp>path/to/another_file</tmp>
46
46
  ```
47
47
 
48
- ### 3. Compile!
48
+ ### 3. Compile to Markdown
49
49
  Run the compiler from your terminal to generate your final Markdown document:
50
50
  ```bash
51
51
  pyact path/to/your_file.pamd -o output.md
52
52
  ```
53
53
 
54
- That's it! You now have a standard Markdown file ready to be shared, published, or converted to PDF.
54
+ ### 4. Compile to Word (DOCX)
55
+ You can also compile straight to a native Microsoft Word Document, complete with CSS styling!
56
+ ```bash
57
+ pyact path/to/your_file.pamd -o output.md --docx output.docx
58
+ ```
59
+ *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!*
60
+
61
+ That's it! You now have documents ready to be shared, published, or distributed.
@@ -0,0 +1,185 @@
1
+ import inspect
2
+ from typing import Callable, List, Any
3
+ from pyact.py2tex import py2tex
4
+
5
+ def equation(func: Callable, values: List[Any]) -> str:
6
+ """
7
+ Converts a Python lambda or simple function into a LaTeX math equation string.
8
+
9
+ Why it is needed:
10
+ To allow users to define equations in Python syntax and automatically render them
11
+ as mathematical formulas in the final document, without needing to hand-write LaTeX.
12
+
13
+ Inputs:
14
+ func (Callable): A simple Python function (e.g., `lambda x: x**2`).
15
+ values (List[Any]): The arguments to pass to the function to compute the result.
16
+
17
+ Outputs:
18
+ str: A Markdown math string formatted as `$$ Name = Equation = Value $$`.
19
+ """
20
+ source = inspect.getsource(func)
21
+ value = func(*values)
22
+ name, sep, tail = source.rpartition("=")
23
+ head, sep, equ = tail.rpartition(":")
24
+ return f"$$ {name} = {py2tex(equ.replace(chr(10), ''))} = {value} $$"
25
+
26
+ def image(link: str, caption: str) -> str:
27
+ """
28
+ Generates a Markdown image string with an embedded HTML caption.
29
+
30
+ Why it is needed:
31
+ Standard Markdown images `![caption](url)` don't display visible captions below the image.
32
+ This helper injects a styled HTML `<span>` below the image to force a visual caption.
33
+
34
+ Inputs:
35
+ link (str): The URL or local path to the image.
36
+ caption (str): The text to display as the caption.
37
+
38
+ Outputs:
39
+ str: A formatted string containing the Markdown image and HTML span caption.
40
+ """
41
+ image_link = f"![{caption}]({link})"
42
+ caption_html = f"<span style='text-align: center; display: block; font-style: italic;'>{caption}</span>"
43
+ return f"{image_link} \n {caption_html}"
44
+
45
+ def table(headers: List[str], values: List[List[Any]], caption: str, add_no: bool = False, starting_number: int = 1) -> str:
46
+ """
47
+ Generates a Markdown table from Python lists, including a caption.
48
+
49
+ Why it is needed:
50
+ Writing Markdown tables by hand is tedious. This function allows users to programmatically
51
+ build tables from 2D data arrays, inject automatic numbering, and append a styled caption.
52
+
53
+ Inputs:
54
+ headers (List[str]): List of column header names.
55
+ values (List[List[Any]]): A 2D list containing the row data.
56
+ caption (str): The text to display above the table.
57
+ add_no (bool): If True, adds an auto-incrementing "No." column to the left.
58
+ starting_number (int): The starting index for the "No." column.
59
+
60
+ Outputs:
61
+ str: A formatted Markdown string containing the caption and the table.
62
+ """
63
+ if add_no:
64
+ headers_string = f"|No.|"
65
+ breaker_line = "|---|"
66
+ else:
67
+ headers_string = f"|"
68
+ breaker_line = "|"
69
+
70
+ for i in headers:
71
+ headers_string += f"{i}|"
72
+ breaker_line += "-"*len(i)+"|"
73
+
74
+ table_content = ""
75
+ for i, row in enumerate(values):
76
+ if add_no:
77
+ table_content += f"|{i+starting_number}|"
78
+ else:
79
+ table_content += "|"
80
+ for value in row:
81
+ table_content += f"{value}|"
82
+ table_content += "\n"
83
+
84
+ caption_html = f"<span style='text-align: center; display: block; font-style: italic;'>{caption}</span>"
85
+ return f"{caption_html} \n {headers_string}\n{breaker_line}\n{table_content}"
86
+
87
+ def hyperlink(caption: str, link: str) -> str:
88
+ """
89
+ Generates a simple Markdown hyperlink.
90
+
91
+ Why it is needed:
92
+ Provides a standardized helper method in the PyAct ecosystem for injecting links.
93
+
94
+ Inputs:
95
+ caption (str): The clickable text to display.
96
+ link (str): The URL destination.
97
+
98
+ Outputs:
99
+ str: The Markdown hyperlink string `[caption](link)`.
100
+ """
101
+ return f"[{caption}]({link})"
102
+
103
+ def unordered_list(items: List[str]) -> str:
104
+ """
105
+ Generates a Markdown unordered (bullet) list from a Python list.
106
+
107
+ Why it is needed:
108
+ Allows programmatic generation of bullet points from array data.
109
+
110
+ Inputs:
111
+ items (List[str]): A list of string items.
112
+
113
+ Outputs:
114
+ str: A Markdown string where each item is prefixed with `* `.
115
+ """
116
+ list_string = ""
117
+ for i in items:
118
+ list_string += f"* {i}\n"
119
+ return list_string
120
+
121
+ def ordered_list(items: List[str]) -> str:
122
+ """
123
+ Generates a Markdown ordered (numbered) list from a Python list.
124
+
125
+ Why it is needed:
126
+ Allows programmatic generation of numbered lists from array data.
127
+
128
+ Inputs:
129
+ items (List[str]): A list of string items.
130
+
131
+ Outputs:
132
+ str: A Markdown string where each item is numbered sequentially (1. 2. 3.).
133
+ """
134
+ list_string = ""
135
+ for i, item in enumerate(items):
136
+ list_string += f"{i+1}. {item}\n"
137
+ return list_string
138
+
139
+ def checklist(items: List[str]) -> str:
140
+ """
141
+ Generates a Markdown task checklist from a Python list.
142
+
143
+ Why it is needed:
144
+ Allows programmatic generation of checkable task items.
145
+
146
+ Inputs:
147
+ items (List[str]): A list of string items.
148
+
149
+ Outputs:
150
+ str: A Markdown string where each item is prefixed with `- [ ] `.
151
+ """
152
+ list_string = ""
153
+ for i in items:
154
+ list_string += f"- [ ] {i}\n"
155
+ return list_string
156
+
157
+ class Footnote:
158
+ """
159
+ A helper class for managing Markdown footnotes.
160
+
161
+ Why it is needed:
162
+ Footnotes require two separate parts in Markdown: the annotation mark in the text `[^1]`,
163
+ and the definition at the bottom of the document `[^1]: The text`. This class links
164
+ the two together to prevent numbering mismatches.
165
+ """
166
+ def __init__(self, number: int, text: str):
167
+ """
168
+ Inputs:
169
+ number (int): The footnote reference number.
170
+ text (str): The expanded text definition of the footnote.
171
+ """
172
+ self.number = number
173
+ self.text = text
174
+
175
+ def define_string(self) -> str:
176
+ """
177
+ Outputs (str): The full footnote definition block to place at the bottom of the document.
178
+ """
179
+ return f"[^{self.number}]: {self.text}"
180
+
181
+ def adnotation(self) -> str:
182
+ """
183
+ Outputs (str): The short inline reference marker to place in the paragraph text.
184
+ """
185
+ return f"[^{self.number}]\n"
@@ -0,0 +1 @@
1
+ __version__ = "0.5.0"
@@ -0,0 +1,75 @@
1
+ import argparse
2
+ import sys
3
+ import os
4
+ from .core import map_content, process_content
5
+
6
+ def main():
7
+ """
8
+ Main entry point for the PyAct Command Line Interface.
9
+
10
+ Why it is needed:
11
+ This acts as the bridge between the terminal and the PyAct compilation engine.
12
+ It parses command-line arguments, triggers the document mapping/compilation process,
13
+ and handles file I/O for outputting the compiled Markdown and DOCX files.
14
+
15
+ Inputs (via sys.argv):
16
+ input (str): Positional argument for the target `.pamd` file path.
17
+ --output (str): Optional path to save the generated `.md` file.
18
+ --docx (str): Optional path to save the generated `.docx` file.
19
+ --css (str): Optional path to a `.css` file for styling the DOCX output.
20
+
21
+ Outputs:
22
+ Writes the generated `.md` and `.docx` files to the filesystem and prints
23
+ status messages to stdout/stderr.
24
+ """
25
+ parser = argparse.ArgumentParser(description="PyAct CLI - PAMD to Markdown compiler")
26
+ parser.add_argument("input", help="Path to the main .pamd file")
27
+ parser.add_argument("-o", "--output", help="Output file path (default prints to stdout)")
28
+ parser.add_argument("--docx", help="Also generate a DOCX file at this path")
29
+ parser.add_argument("--css", help="Optional CSS file path to style the DOCX")
30
+
31
+ args = parser.parse_args()
32
+
33
+ input_path = os.path.abspath(args.input)
34
+ directory = os.path.dirname(input_path)
35
+ filename = os.path.basename(input_path)
36
+
37
+ if filename.endswith(".pamd"):
38
+ filename = filename[:-5]
39
+
40
+ try:
41
+ build_tree = map_content(filename, directory)
42
+ content = process_content(build_tree)
43
+
44
+ if args.output:
45
+ with open(args.output, "w", encoding="utf-8") as f:
46
+ f.write(content)
47
+ print(f"Successfully compiled to {args.output}")
48
+ else:
49
+ print(content)
50
+
51
+ if args.docx:
52
+ from .mdTOword import style_parser, markdown_parser, docx_writer
53
+ styles = {}
54
+ if args.css:
55
+ styles = style_parser(args.css)
56
+ else:
57
+ # Try to look for style.css in the active directory
58
+ local_css = os.path.join(directory, "style.css")
59
+ if os.path.exists(local_css):
60
+ styles = style_parser(local_css)
61
+ else:
62
+ # Fallback to the default one packaged in pyact
63
+ default_css = os.path.join(os.path.dirname(__file__), "style.css")
64
+ styles = style_parser(default_css)
65
+
66
+ blocks = markdown_parser(content)
67
+ docx_writer(blocks, styles, args.docx)
68
+ print(f"Successfully compiled DOCX to {args.docx}")
69
+
70
+ except Exception as e:
71
+ print(f"Error: {e}", file=sys.stderr)
72
+ sys.exit(1)
73
+
74
+ if __name__ == "__main__":
75
+ main()
@@ -0,0 +1,198 @@
1
+ import re
2
+ import json
3
+
4
+ def get_directory(path: str) -> list:
5
+ """
6
+ Splits a full file path into its directory component and its filename component.
7
+
8
+ Why it is needed:
9
+ When resolving nested templates or generating output files, the engine needs to
10
+ know the base directory of the current `.pamd` file so it can correctly locate
11
+ sibling template files.
12
+
13
+ Inputs:
14
+ path (str): The full path to a file (e.g., './main/retro.pamd').
15
+
16
+ Outputs:
17
+ list: A two-element list `[directory_path, filename]`.
18
+ """
19
+ head, sep, tail = path.rpartition('/')
20
+ return [head + sep, tail]
21
+
22
+ def read_pamd_cells(file_path: str):
23
+ """
24
+ Reads a .pamd (JSON Notebook) file and extracts the Markdown and Python code.
25
+
26
+ Why it is needed:
27
+ `.pamd` files are stored exactly like Jupyter Notebooks (a list of cell objects).
28
+ This function parses the JSON structure and concatenates all 'code' cells into a
29
+ single executable Python script, and all 'markdown' cells into a single Markdown document.
30
+
31
+ Inputs:
32
+ file_path (str): Path to the `.pamd` file to read.
33
+
34
+ Outputs:
35
+ tuple (str, str): A tuple containing `(code_content, markdown_content)`.
36
+ """
37
+ with open(file_path, 'r', encoding='utf-8') as f:
38
+ data = json.load(f)
39
+
40
+ code_content = ""
41
+ markdown_content = ""
42
+
43
+ for cell in data.get("cells", []):
44
+ cell_type = cell.get("cell_type", "")
45
+ source = cell.get("source", "")
46
+ if isinstance(source, list):
47
+ source = "".join(source)
48
+
49
+ if cell_type == "code":
50
+ code_content += source + "\n"
51
+ elif cell_type == "markdown":
52
+ markdown_content += source + "\n"
53
+
54
+ return code_content, markdown_content
55
+
56
+ def get_imports(file: str, code_text: str):
57
+ """
58
+ Executes the Python code from a .pamd file and retrieves the context variables.
59
+
60
+ Why it is needed:
61
+ To inject dynamic data into the Markdown document, PyAct executes the user's Python
62
+ script. The user must define a `context()` function that returns a dictionary mapping
63
+ variable names (like 'title') to their computed values. This function safely executes
64
+ that code and extracts the resulting dictionary.
65
+
66
+ Inputs:
67
+ file (str): The name of the file being executed (for error reporting).
68
+ code_text (str): The raw Python code extracted from the `.pamd` file.
69
+
70
+ Outputs:
71
+ dict: The dictionary of variables returned by the user's `context()` function.
72
+ """
73
+ file_namespace = {"__file__": f"{file}.pamd"}
74
+ if not code_text.strip():
75
+ raise ValueError(f"The file {file}.pamd needs a 'context()' function but has no code cell.")
76
+
77
+ try:
78
+ exec(code_text, file_namespace)
79
+ except Exception as e:
80
+ raise RuntimeError(f"Error executing code in {file}.pamd: {e}")
81
+
82
+ if "context" not in file_namespace:
83
+ raise ValueError(f"The file {file}.pamd is missing the required 'context()' function.")
84
+
85
+ context_func = file_namespace["context"]
86
+
87
+ if not callable(context_func):
88
+ raise TypeError(f"In {file}.pamd, 'context' was found but it is not a function!")
89
+
90
+ result_dict = context_func()
91
+
92
+ if not isinstance(result_dict, dict):
93
+ raise TypeError(f"The 'context()' function in {file}.pamd must return a dictionary.")
94
+
95
+ return result_dict
96
+
97
+ def find_imports(text: str):
98
+ """
99
+ Finds all `<ctx>var_name</ctx>` tags in a markdown string.
100
+
101
+ Why it is needed:
102
+ Identifies which variables the Markdown document is requesting from the Python context.
103
+
104
+ Inputs:
105
+ text (str): The raw Markdown text.
106
+
107
+ Outputs:
108
+ list: A list of variable names extracted from the tags.
109
+ """
110
+ pattern = r"<ctx>(.*?)</ctx>"
111
+ return re.findall(pattern, text)
112
+
113
+
114
+ def find_templates(text: str):
115
+ """
116
+ Finds all `<tmp>file_name</tmp>` tags in a markdown string.
117
+
118
+ Why it is needed:
119
+ Identifies nested `.pamd` template files that need to be recursively compiled
120
+ and injected into the current document.
121
+
122
+ Inputs:
123
+ text (str): The raw Markdown text.
124
+
125
+ Outputs:
126
+ list: A list of template file names extracted from the tags.
127
+ """
128
+ pattern = r"<tmp>(.*?)</tmp>"
129
+ return re.findall(pattern, text)
130
+
131
+ def map_content(file: str, path: str):
132
+ """
133
+ Recursively maps the dependency tree of a `.pamd` file.
134
+
135
+ Why it is needed:
136
+ Before compiling, PyAct needs to build an Abstract Syntax Tree (AST) of the document,
137
+ mapping out all required context variables and any nested child templates that need
138
+ to be resolved. This allows for deep, nested project structures.
139
+
140
+ Inputs:
141
+ file (str): The target `.pamd` file name (without extension).
142
+ path (str): The directory where the file is located.
143
+
144
+ Outputs:
145
+ dict: A recursive "build tree" dictionary containing metadata, required imports,
146
+ and the build trees of all nested templates.
147
+ """
148
+ if path and not path.endswith('/'):
149
+ full_path = f"{path}/{file}"
150
+ elif path.endswith('/'):
151
+ full_path = f"{path}{file}"
152
+ else:
153
+ full_path = file
154
+
155
+ code_text, main_file = read_pamd_cells(f"{full_path}.pamd")
156
+ templates = find_templates(main_file)
157
+ imports = find_imports(main_file)
158
+ meta = get_directory(full_path)
159
+
160
+ ready_t = []
161
+ for i in templates:
162
+ ready_t.append(map_content(i, meta[0]))
163
+
164
+ return {
165
+ "tag_name": file,
166
+ "name": meta[1],
167
+ "path": meta[0],
168
+ "imports": imports,
169
+ "templates": ready_t
170
+ }
171
+
172
+ def process_content(build_tree: dict):
173
+ """
174
+ Compiles a mapped build tree into a final flat Markdown string.
175
+
176
+ Why it is needed:
177
+ This is the core renderer. It reads the `.pamd` files, executes their Python code
178
+ to fetch the context dictionary, replaces all `<ctx>` tags with dynamic data,
179
+ and recursively replaces all `<tmp>` tags with their fully compiled child documents.
180
+
181
+ Inputs:
182
+ build_tree (dict): The AST mapping generated by `map_content()`.
183
+
184
+ Outputs:
185
+ str: The final, fully compiled Markdown text string ready for export.
186
+ """
187
+ file_path_base = build_tree.get("path", "") + build_tree.get("name", "")
188
+ code_text, file_text = read_pamd_cells(file_path_base + ".pamd")
189
+
190
+ needed_imports = build_tree.get("imports", [])
191
+ if needed_imports:
192
+ imports = get_imports(file_path_base, code_text)
193
+ for i in needed_imports:
194
+ file_text = file_text.replace(f"<ctx>{i}</ctx>", str(imports.get(i, '')))
195
+
196
+ for i in build_tree.get("templates", []):
197
+ file_text = file_text.replace(f"<tmp>{i.get('tag_name')}</tmp>", process_content(i))
198
+ return file_text
@@ -0,0 +1,79 @@
1
+ import re
2
+ import json
3
+
4
+ def css_to_dict(css_string: str) -> dict:
5
+ """
6
+ Parses a raw CSS string into a nested Python dictionary.
7
+
8
+ Why it is needed:
9
+ CSS is text-based and formatted with selectors, curly braces, and semicolons.
10
+ To programmatically apply these styles to python-docx elements, we need to
11
+ convert the syntax into a highly structured, queryable dictionary map.
12
+
13
+ Inputs:
14
+ css_string (str): The raw contents of a CSS file as a string.
15
+
16
+ Outputs:
17
+ dict: A dictionary where keys are CSS selectors (e.g., 'p', 'h1') and
18
+ values are dictionaries of style properties (e.g., {'color': '#000'}).
19
+ """
20
+ # Remove CSS comments
21
+ css_string = re.sub(r'/\*[\s\S]*?\*/', '', css_string)
22
+
23
+ # Match selectors and their corresponding blocks
24
+ pattern = r'([^{]+)\{([^}]+)\}'
25
+ matches = re.findall(pattern, css_string)
26
+
27
+ css_dict = {}
28
+ for selector, block in matches:
29
+ selector = selector.strip()
30
+
31
+ # Parse individual CSS properties
32
+ rules = {}
33
+ for line in block.split(';'):
34
+ line = line.strip()
35
+ if not line:
36
+ continue
37
+ if ':' in line:
38
+ key, val = line.split(':', 1)
39
+ rules[key.strip()] = val.strip()
40
+
41
+ # Handle comma-separated selectors (e.g., 'h1, h2, h3')
42
+ for sel in selector.split(','):
43
+ sel = sel.strip()
44
+ if sel:
45
+ if sel not in css_dict:
46
+ css_dict[sel] = {}
47
+ css_dict[sel].update(rules)
48
+
49
+ return css_dict
50
+
51
+ def parse_css_file(file_path: str, as_json_string: bool = False):
52
+ """
53
+ Reads a CSS file from disk and parses it into a dictionary or JSON string.
54
+
55
+ Why it is needed:
56
+ Provides a clean interface for reading a physical file from the OS, extracting
57
+ its contents, and piping it into the core `css_to_dict` parser.
58
+
59
+ Inputs:
60
+ file_path (str): The path to the `.css` file on disk.
61
+ as_json_string (bool): If True, returns the output as a serialized JSON string
62
+ instead of a Python dictionary. Defaults to False.
63
+
64
+ Outputs:
65
+ dict or str: The parsed CSS dictionary, or a JSON-formatted string if requested.
66
+ """
67
+ with open(file_path, 'r', encoding='utf-8') as f:
68
+ css_string = f.read()
69
+
70
+ css_dict = css_to_dict(css_string)
71
+
72
+ if as_json_string:
73
+ return json.dumps(css_dict, indent=4)
74
+ return css_dict
75
+
76
+ if __name__ == "__main__":
77
+ # Example usage:
78
+ # print(parse_css_file("style.css", as_json_string=True))
79
+ pass
@@ -0,0 +1,467 @@
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: str) -> dict:
11
+ """
12
+ Parses a CSS file and converts it into a Python dictionary containing style maps.
13
+
14
+ Why it is needed:
15
+ To allow users to style their exported Word Documents using standard CSS syntax,
16
+ we must first translate that CSS into a structured Python dictionary that the
17
+ DOCX generation engine can query.
18
+
19
+ Inputs:
20
+ css_path (str): The absolute or relative file path to the target .css file.
21
+
22
+ Outputs:
23
+ dict: A nested dictionary where keys are CSS selectors (e.g., 'h1', 'p')
24
+ and values are dictionaries of property-value pairs (e.g., {'color': '#000', 'font-size': '12pt'}).
25
+ Returns an empty dictionary {} if parsing fails.
26
+ """
27
+ try:
28
+ return css2json.parse_css_file(css_path)
29
+ except Exception as e:
30
+ print(f"Warning: Could not parse CSS ({e})")
31
+ return {}
32
+
33
+ def markdown_parser(md_text: str) -> list:
34
+ """
35
+ Parses raw Markdown text into a structured list of block tuples.
36
+
37
+ Why it is needed:
38
+ Because python-docx generates documents block-by-block (paragraphs, tables, headings),
39
+ we need to break down the raw string of Markdown text into distinct, classified chunks
40
+ that our DOCX writer can loop through and translate into native Word elements.
41
+
42
+ Inputs:
43
+ md_text (str): The complete raw Markdown text string.
44
+
45
+ Outputs:
46
+ list: A list of tuples where each tuple represents a block element.
47
+ Examples:
48
+ - ('heading', 2, 'Title Text')
49
+ - ('paragraph', 'Some plain text')
50
+ - ('list', 'List item text', 'ordered')
51
+ - ('math', 'x = 2')
52
+ - ('table', ['|Header|', '|---|', '|Data|'])
53
+ """
54
+ blocks = []
55
+ lines = md_text.split('\n')
56
+
57
+ current_table = []
58
+
59
+ for line in lines:
60
+ line_s = line.strip()
61
+
62
+ # Table Parsing
63
+ if line_s.startswith('|') and line_s.endswith('|'):
64
+ current_table.append(line_s)
65
+ continue
66
+ elif current_table:
67
+ blocks.append(('table', current_table))
68
+ current_table = []
69
+
70
+ if not line_s:
71
+ continue
72
+
73
+ # Heading Parsing
74
+ if line_s.startswith('#'):
75
+ level = len(line_s) - len(line_s.lstrip('#'))
76
+ text = line_s.lstrip('#').strip()
77
+ blocks.append(('heading', level, text))
78
+
79
+ # List Parsing
80
+ elif line_s.startswith('- ') or line_s.startswith('* '):
81
+ text = line_s[2:].strip()
82
+ blocks.append(('list', text, 'unordered'))
83
+
84
+ elif re.match(r'^\d+\.\s+', line_s):
85
+ m = re.match(r'^\d+\.\s+(.*)', line_s)
86
+ blocks.append(('list', m.group(1), 'ordered'))
87
+
88
+ # Math Block Parsing
89
+ elif line_s.startswith('$$') and line_s.endswith('$$'):
90
+ blocks.append(('math', line_s.strip('$').strip()))
91
+
92
+ # Standard Paragraph
93
+ else:
94
+ blocks.append(('paragraph', line_s))
95
+
96
+ if current_table:
97
+ blocks.append(('table', current_table))
98
+
99
+ return blocks
100
+
101
+ def _apply_css_to_run(run, css_rules):
102
+ """Helper to apply CSS rules to a docx run or font."""
103
+ font = run.font if hasattr(run, 'font') else run
104
+
105
+ if 'color' in css_rules:
106
+ hex_col = css_rules['color'].replace('#', '').strip()
107
+ if len(hex_col) == 6:
108
+ try:
109
+ r, g, b = tuple(int(hex_col[i:i+2], 16) for i in (0, 2, 4))
110
+ font.color.rgb = RGBColor(r, g, b)
111
+ except ValueError:
112
+ pass
113
+
114
+ if 'font-size' in css_rules:
115
+ size = css_rules['font-size'].replace('pt', '').strip()
116
+ try:
117
+ font.size = Pt(float(size))
118
+ except ValueError:
119
+ pass
120
+
121
+ if 'font-family' in css_rules:
122
+ font.name = css_rules['font-family'].strip("'\"")
123
+
124
+ if 'font-weight' in css_rules:
125
+ font.bold = (css_rules['font-weight'].lower() == 'bold')
126
+
127
+ if 'font-style' in css_rules:
128
+ font.italic = (css_rules['font-style'].lower() == 'italic')
129
+
130
+ if 'text-decoration' in css_rules:
131
+ dec = css_rules['text-decoration'].lower()
132
+ if dec == 'underline':
133
+ font.underline = True
134
+ elif dec == 'line-through':
135
+ font.strike = True
136
+ elif dec == 'none':
137
+ font.underline = False
138
+ font.strike = False
139
+
140
+ if 'text-transform' in css_rules:
141
+ trans = css_rules['text-transform'].lower()
142
+ if trans == 'uppercase':
143
+ font.all_caps = True
144
+ elif trans == 'small-caps':
145
+ font.small_caps = True
146
+
147
+ if 'background-color' in css_rules:
148
+ # Maps text background to highlight color index
149
+ from docx.enum.text import WD_COLOR_INDEX
150
+ color_map = {
151
+ 'auto': WD_COLOR_INDEX.AUTO, 'black': WD_COLOR_INDEX.BLACK, 'blue': WD_COLOR_INDEX.BLUE,
152
+ 'bright-green': WD_COLOR_INDEX.BRIGHT_GREEN, 'dark-blue': WD_COLOR_INDEX.DARK_BLUE,
153
+ 'dark-red': WD_COLOR_INDEX.DARK_RED, 'dark-yellow': WD_COLOR_INDEX.DARK_YELLOW,
154
+ 'gray-25': WD_COLOR_INDEX.GRAY_25, 'gray-50': WD_COLOR_INDEX.GRAY_50,
155
+ 'green': WD_COLOR_INDEX.GREEN, 'pink': WD_COLOR_INDEX.PINK, 'red': WD_COLOR_INDEX.RED,
156
+ 'teal': WD_COLOR_INDEX.TEAL, 'turquoise': WD_COLOR_INDEX.TURQUOISE,
157
+ 'violet': WD_COLOR_INDEX.VIOLET, 'white': WD_COLOR_INDEX.WHITE, 'yellow': WD_COLOR_INDEX.YELLOW
158
+ }
159
+ bg_col = css_rules['background-color'].lower()
160
+ if bg_col in color_map:
161
+ font.highlight_color = color_map[bg_col]
162
+
163
+ def _apply_paragraph_formatting(p, css_rules):
164
+ """Helper to apply CSS rules to paragraph formatting."""
165
+ fmt = p.paragraph_format
166
+
167
+ if 'text-align' in css_rules:
168
+ align = css_rules['text-align'].lower()
169
+ if align == 'center':
170
+ p.alignment = WD_ALIGN_PARAGRAPH.CENTER
171
+ elif align == 'right':
172
+ p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
173
+ elif align == 'justify':
174
+ p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
175
+ elif align == 'distribute':
176
+ p.alignment = WD_ALIGN_PARAGRAPH.DISTRIBUTE
177
+ elif align == 'left':
178
+ p.alignment = WD_ALIGN_PARAGRAPH.LEFT
179
+
180
+ if 'margin-top' in css_rules:
181
+ val = css_rules['margin-top'].replace('pt', '').strip()
182
+ try:
183
+ fmt.space_before = Pt(float(val))
184
+ except ValueError:
185
+ pass
186
+
187
+ if 'margin-bottom' in css_rules:
188
+ val = css_rules['margin-bottom'].replace('pt', '').strip()
189
+ try:
190
+ fmt.space_after = Pt(float(val))
191
+ except ValueError:
192
+ pass
193
+
194
+ if 'line-height' in css_rules:
195
+ try:
196
+ fmt.line_spacing = float(css_rules['line-height'])
197
+ except ValueError:
198
+ pass
199
+
200
+ if 'page-break-before' in css_rules:
201
+ if css_rules['page-break-before'].lower() == 'always':
202
+ fmt.page_break_before = True
203
+
204
+ import docx.opc.constants
205
+ from docx.oxml.shared import OxmlElement, qn
206
+ import urllib.request
207
+ import io
208
+ import random
209
+
210
+ def _add_internal_hyperlink(paragraph, text, anchor):
211
+ """Adds a clickable internal hyperlink to a bookmark anchor in the docx."""
212
+ hyperlink = OxmlElement('w:hyperlink')
213
+ hyperlink.set(qn('w:anchor'), anchor)
214
+
215
+ new_run = OxmlElement('w:r')
216
+ rPr = OxmlElement('w:rPr')
217
+
218
+ # Make it superscript since it's usually a footnote marker
219
+ vertAlign = OxmlElement('w:vertAlign')
220
+ vertAlign.set(qn('w:val'), 'superscript')
221
+ rPr.append(vertAlign)
222
+
223
+ # Make it blue
224
+ c = OxmlElement('w:color')
225
+ c.set(qn('w:val'), '0563C1')
226
+ rPr.append(c)
227
+
228
+ new_run.append(rPr)
229
+
230
+ text_elem = OxmlElement('w:t')
231
+ text_elem.text = text
232
+ new_run.append(text_elem)
233
+
234
+ hyperlink.append(new_run)
235
+ paragraph._p.append(hyperlink)
236
+
237
+ def _add_bookmark(paragraph, text, anchor):
238
+ """Creates a bookmark anchor in the document and places text inside it."""
239
+ # We need a random ID to prevent collisions
240
+ bm_id = str(random.randint(10000, 99999))
241
+
242
+ bm_start = OxmlElement('w:bookmarkStart')
243
+ bm_start.set(qn('w:id'), bm_id)
244
+ bm_start.set(qn('w:name'), anchor)
245
+ paragraph._p.append(bm_start)
246
+
247
+ run = paragraph.add_run(text)
248
+
249
+ bm_end = OxmlElement('w:bookmarkEnd')
250
+ bm_end.set(qn('w:id'), bm_id)
251
+ paragraph._p.append(bm_end)
252
+ return run
253
+
254
+ def _add_hyperlink(paragraph, text, url, styles, tag):
255
+ """Adds a real hyperlink to a docx paragraph."""
256
+ part = paragraph.part
257
+ r_id = part.relate_to(url, docx.opc.constants.RELATIONSHIP_TYPE.HYPERLINK, is_external=True)
258
+
259
+ hyperlink = OxmlElement('w:hyperlink')
260
+ hyperlink.set(qn('r:id'), r_id)
261
+
262
+ new_run = OxmlElement('w:r')
263
+ rPr = OxmlElement('w:rPr')
264
+
265
+ c = OxmlElement('w:color')
266
+ if 'hyperlink' in styles and 'color' in styles['hyperlink']:
267
+ color_hex = styles['hyperlink']['color'].replace('#', '').strip()
268
+ c.set(qn('w:val'), color_hex)
269
+ else:
270
+ c.set(qn('w:val'), '0563C1')
271
+ rPr.append(c)
272
+
273
+ u = OxmlElement('w:u')
274
+ if 'hyperlink' in styles and 'text-decoration' in styles['hyperlink'] and styles['hyperlink']['text-decoration'] == 'none':
275
+ pass
276
+ else:
277
+ u.set(qn('w:val'), 'single')
278
+ rPr.append(u)
279
+
280
+ new_run.append(rPr)
281
+
282
+ text_elem = OxmlElement('w:t')
283
+ text_elem.text = text
284
+ new_run.append(text_elem)
285
+
286
+ hyperlink.append(new_run)
287
+ paragraph._p.append(hyperlink)
288
+
289
+ def _add_image(paragraph, url, caption, styles):
290
+ """Adds an inline image fetched from URL."""
291
+ try:
292
+ req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
293
+ with urllib.request.urlopen(req) as response:
294
+ image_stream = io.BytesIO(response.read())
295
+
296
+ run = paragraph.add_run()
297
+
298
+ # Apply CSS dimensions if available
299
+ width = None
300
+ if 'image' in styles and 'width' in styles['image']:
301
+ w_str = styles['image']['width']
302
+ if 'in' in w_str: width = docx.shared.Inches(float(w_str.replace('in', '').strip()))
303
+
304
+ if width:
305
+ run.add_picture(image_stream, width=width)
306
+ else:
307
+ run.add_picture(image_stream, width=docx.shared.Inches(4))
308
+
309
+ except Exception as e:
310
+ paragraph.add_run(f"[Image Failed: {url}]")
311
+
312
+ def _process_inline(paragraph, text, styles, tag):
313
+ """Parses text for images, links, math, and bold/italic."""
314
+ # Simple regex to split by math, image, or link. Captures the match as a group.
315
+ # Group 5: Footnote definition [^1]: ...
316
+ # Group 6: Footnote annotation [^1]
317
+ pattern = r'(\$\$.*?\$\$)|(!\[.*?\]\(.*?\))|(\[.*?\]\(.*?\))|(<span.*?>.*?</span>)|(\[\^.*?\]:.*?$)|(\[\^.*?\])'
318
+ parts = re.split(pattern, text)
319
+
320
+ for part in parts:
321
+ if not part: continue
322
+
323
+ if part.startswith('$$') and part.endswith('$$'):
324
+ math_text = part.strip('$').strip()
325
+ try:
326
+ math2docx.add_math(paragraph, math_text)
327
+ except Exception:
328
+ run = paragraph.add_run(part)
329
+ if tag in styles: _apply_css_to_run(run, styles[tag])
330
+
331
+ elif part.startswith('![') and part.endswith(')'):
332
+ m = re.match(r'!\[(.*?)\]\((.*?)\)', part)
333
+ if m:
334
+ _add_image(paragraph, m.group(2), m.group(1), styles)
335
+
336
+ elif part.startswith('[') and part.endswith(')'):
337
+ m = re.match(r'\[(.*?)\]\((.*?)\)', part)
338
+ if m:
339
+ _add_hyperlink(paragraph, m.group(1), m.group(2), styles, tag)
340
+
341
+ elif part.startswith('<span') and part.endswith('</span>'):
342
+ m = re.match(r'<span.*?>(.*?)</span>', part)
343
+ if m:
344
+ caption_text = m.group(1)
345
+ run = paragraph.add_run(caption_text)
346
+ run.italic = True
347
+ paragraph.alignment = docx.enum.text.WD_ALIGN_PARAGRAPH.CENTER
348
+ if tag in styles: _apply_css_to_run(run, styles[tag])
349
+
350
+ elif part.startswith('[^') and ']:' in part:
351
+ # Footnote definition e.g. [^1]: The text
352
+ m = re.match(r'\[\^(.*?)\]:\s*(.*)', part)
353
+ if m:
354
+ fn_id = m.group(1)
355
+ fn_text = m.group(2)
356
+ # Create a bookmark here for the annotation to jump to
357
+ run = _add_bookmark(paragraph, f"[{fn_id}]: {fn_text}", f"footnote_{fn_id}")
358
+ if tag in styles: _apply_css_to_run(run, styles[tag])
359
+
360
+ elif part.startswith('[^') and part.endswith(']'):
361
+ # Footnote annotation e.g. [^1]
362
+ m = re.match(r'\[\^(.*?)\]', part)
363
+ if m:
364
+ fn_id = m.group(1)
365
+ _add_internal_hyperlink(paragraph, f"[{fn_id}]", f"footnote_{fn_id}")
366
+
367
+ else:
368
+ # Handle plain text + bold/italic
369
+ subparts = re.split(r'(\*\*.*?\*\*|\*.*?\*)', part)
370
+ for subpart in subparts:
371
+ if not subpart: continue
372
+ if subpart.startswith('**') and subpart.endswith('**'):
373
+ run = paragraph.add_run(subpart[2:-2])
374
+ run.bold = True
375
+ if tag in styles: _apply_css_to_run(run, styles[tag])
376
+ elif subpart.startswith('*') and subpart.endswith('*'):
377
+ run = paragraph.add_run(subpart[1:-1])
378
+ run.italic = True
379
+ if tag in styles: _apply_css_to_run(run, styles[tag])
380
+ else:
381
+ run = paragraph.add_run(subpart)
382
+ if tag in styles: _apply_css_to_run(run, styles[tag])
383
+
384
+ def docx_writer(blocks: list, styles: dict, output_file: str):
385
+ """
386
+ Translates parsed markdown blocks into a Word document and applies CSS styling.
387
+
388
+ Why it is needed:
389
+ This is the core generation engine. It takes the abstract syntax structure generated
390
+ by `markdown_parser()` and the style rules from `style_parser()`, loops through them,
391
+ and issues the corresponding `python-docx` commands to physically build the `.docx` file.
392
+
393
+ Inputs:
394
+ blocks (list): The list of parsed Markdown block tuples (from `markdown_parser`).
395
+ styles (dict): The nested dictionary of CSS rules (from `style_parser`).
396
+ output_file (str): The absolute or relative file path where the resulting .docx should be saved.
397
+
398
+ Outputs:
399
+ None: This function does not return a value, but it writes a binary .docx file to disk.
400
+ """
401
+ doc = Document()
402
+
403
+ # Global body style
404
+ if 'body' in styles:
405
+ normal_style = doc.styles['Normal']
406
+ _apply_css_to_run(normal_style, styles['body'])
407
+ _apply_paragraph_formatting(normal_style, styles['body'])
408
+
409
+ for block in blocks:
410
+ btype = block[0]
411
+
412
+ if btype == 'heading':
413
+ level, text = block[1], block[2]
414
+ p = doc.add_heading('', level=level)
415
+ _process_inline(p, text, styles, f'h{level}')
416
+ if f'h{level}' in styles:
417
+ _apply_paragraph_formatting(p, styles[f'h{level}'])
418
+
419
+ elif btype == 'paragraph':
420
+ text = block[1]
421
+ p = doc.add_paragraph()
422
+ if 'p' in styles:
423
+ _apply_paragraph_formatting(p, styles['p'])
424
+ _process_inline(p, text, styles, 'p')
425
+
426
+ elif btype == 'list':
427
+ text = block[1]
428
+ list_type = block[2] if len(block) > 2 else 'unordered'
429
+ style = 'List Number' if list_type == 'ordered' else 'List Bullet'
430
+ p = doc.add_paragraph(style=style)
431
+ if 'list' in styles:
432
+ _apply_paragraph_formatting(p, styles['list'])
433
+ _process_inline(p, text, styles, 'list')
434
+
435
+ elif btype == 'math':
436
+ math_text = block[1]
437
+ p = doc.add_paragraph()
438
+ try:
439
+ math2docx.add_math(p, math_text)
440
+ if 'equation' in styles:
441
+ _apply_paragraph_formatting(p, styles['equation'])
442
+ except Exception:
443
+ p.add_run(f"$$ {math_text} $$")
444
+
445
+ elif btype == 'table':
446
+ table_lines = block[1]
447
+ rows = [
448
+ [cell.strip() for cell in line.strip('|').split('|')]
449
+ for line in table_lines if '---' not in line
450
+ ]
451
+ if rows:
452
+ table = doc.add_table(rows=len(rows), cols=len(rows[0]))
453
+ for r_idx, row in enumerate(rows):
454
+ for c_idx, val in enumerate(row):
455
+ table.cell(r_idx, c_idx).text = val
456
+ if 'table' in styles:
457
+ pass
458
+
459
+ doc.save(output_file)
460
+
461
+
462
+ if __name__ == "__main__":
463
+ # Example Usage:
464
+ styles = style_parser('engine/style.css')
465
+ md_blocks = markdown_parser('## Hello World\nThis is a **bold** test.')
466
+ docx_writer(md_blocks, styles, 'output.docx')
467
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyact-cli
3
- Version: 0.3.0
3
+ Version: 0.5.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
@@ -65,10 +65,17 @@ You can also include other pamd files as templates!
65
65
  <tmp>path/to/another_file</tmp>
66
66
  ```
67
67
 
68
- ### 3. Compile!
68
+ ### 3. Compile to Markdown
69
69
  Run the compiler from your terminal to generate your final Markdown document:
70
70
  ```bash
71
71
  pyact path/to/your_file.pamd -o output.md
72
72
  ```
73
73
 
74
- That's it! You now have a standard Markdown file ready to be shared, published, or converted to PDF.
74
+ ### 4. Compile to Word (DOCX)
75
+ You can also compile straight to a native Microsoft Word Document, complete with CSS styling!
76
+ ```bash
77
+ pyact path/to/your_file.pamd -o output.md --docx output.docx
78
+ ```
79
+ *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!*
80
+
81
+ That's it! You now have documents ready to be shared, published, or distributed.
@@ -4,6 +4,8 @@ pamd_helpers/__init__.py
4
4
  pyact/__init__.py
5
5
  pyact/cli.py
6
6
  pyact/core.py
7
+ pyact/css2json.py
8
+ pyact/mdTOword.py
7
9
  pyact/py2tex.py
8
10
  pyact_cli.egg-info/PKG-INFO
9
11
  pyact_cli.egg-info/SOURCES.txt
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
5
5
 
6
6
  setup(
7
7
  name="pyact-cli",
8
- version="0.3.0",
8
+ version="0.5.0",
9
9
  author="Abstergo2003",
10
10
  author_email="",
11
11
  description="A CLI tool to compile .pamd files to Markdown.",
@@ -1,76 +0,0 @@
1
- import inspect
2
- from pyact.py2tex import py2tex
3
-
4
- def equation(func: function, values: list) -> str:
5
- source = inspect.getsource(func)
6
- value = func(*values)
7
- name, sep, tail = source.rpartition("=")
8
- head, sep, equ = tail.rpartition(":")
9
- return f"$$ {name} = {py2tex(equ.replace("\n", ""))} = {value} $$"
10
-
11
-
12
- def image(link: str, caption: str) -> str:
13
- image_link = f"![{caption}]({link})"
14
- caption = f"<span style='text-align: center; display: block; font-style: italic;'>{caption}</span>"
15
- return f"{image_link} \n {caption}"
16
-
17
- def table(headers: list, values: list, caption: str, add_no: bool = False, starting_number: int = 1) -> str:
18
- if add_no:
19
- headers_string = f"|No.|"
20
- breaker_line = "|---|"
21
- else:
22
- headers_string = f"|"
23
- breaker_line = "|"
24
-
25
-
26
- for i in headers:
27
- headers_string += f"{i}|"
28
- breaker_line += "-"*len(i)+"|"
29
-
30
- table_content = ""
31
-
32
- for i, row in enumerate(values):
33
- if add_no:
34
- table_content += f"|{i+starting_number}|"
35
- else:
36
- table_content += "|"
37
-
38
- for value in row:
39
- table_content += f"{value}|"
40
- table_content += "\n"
41
-
42
- caption = f"<span style='text-align: center; display: block; font-style: italic;'>{caption}</span>"
43
- return f"{caption} \n {headers_string}\n{breaker_line}\n{table_content}"
44
-
45
-
46
- def hyperlink(caption: str, link: str) -> str:
47
- return f"[{caption}]({link})"
48
-
49
- def unordered_list(items: list) -> str:
50
- list_string = ""
51
- for i in items:
52
- list_string += f"* {i}\n"
53
- return list_string
54
-
55
- def ordered_list(items: list) -> str:
56
- list_string = ""
57
- for i, item in enumerate(items):
58
- list_string += f"{i+1}. {item}\n"
59
- return list_string
60
-
61
- def checklist(items: list) -> str:
62
- list_string = ""
63
- for i in items:
64
- list_string += f"- [ ] {i}\n"
65
- return list_string
66
-
67
- class Footnote:
68
- def __init__(self, number, text):
69
- self.number = number
70
- self.text = text
71
-
72
- def define_string(self) -> str:
73
- return f"[^{self.number}]: {self.text}"
74
-
75
- def adnotation(self) -> str:
76
- return f"[^{self.number}]\n"
@@ -1 +0,0 @@
1
- __version__ = "0.3.0"
@@ -1,35 +0,0 @@
1
- import argparse
2
- import sys
3
- import os
4
- from .core import map_content, process_content
5
-
6
- def main():
7
- parser = argparse.ArgumentParser(description="PyAct CLI - PAMD to Markdown compiler")
8
- parser.add_argument("input", help="Path to the main .pamd file")
9
- parser.add_argument("-o", "--output", help="Output file path (default prints to stdout)")
10
-
11
- args = parser.parse_args()
12
-
13
- input_path = os.path.abspath(args.input)
14
- directory = os.path.dirname(input_path)
15
- filename = os.path.basename(input_path)
16
-
17
- if filename.endswith(".pamd"):
18
- filename = filename[:-5]
19
-
20
- try:
21
- build_tree = map_content(filename, directory)
22
- content = process_content(build_tree)
23
-
24
- if args.output:
25
- with open(args.output, "w", encoding="utf-8") as f:
26
- f.write(content)
27
- print(f"Successfully compiled to {args.output}")
28
- else:
29
- print(content)
30
- except Exception as e:
31
- print(f"Error: {e}", file=sys.stderr)
32
- sys.exit(1)
33
-
34
- if __name__ == "__main__":
35
- main()
@@ -1,103 +0,0 @@
1
- import re
2
- import json
3
-
4
- def get_directory(path: str) -> list:
5
- # Splits path into: ('./main', '/', 'retro.pamd')
6
- head, sep, tail = path.rpartition('/')
7
- # Recombine the first part and the slash
8
- return [head + sep, tail]
9
-
10
- def read_pamd_cells(file_path: str):
11
- with open(file_path, 'r', encoding='utf-8') as f:
12
- data = json.load(f)
13
-
14
- code_content = ""
15
- markdown_content = ""
16
-
17
- for cell in data.get("cells", []):
18
- cell_type = cell.get("cell_type", "")
19
- source = cell.get("source", "")
20
- if isinstance(source, list):
21
- source = "".join(source)
22
-
23
- if cell_type == "code":
24
- code_content += source + "\n"
25
- elif cell_type == "markdown":
26
- markdown_content += source + "\n"
27
-
28
- return code_content, markdown_content
29
-
30
- def get_imports(file: str, code_text: str):
31
- file_namespace = {"__file__": f"{file}.pamd"}
32
- if not code_text.strip():
33
- raise ValueError(f"The file {file}.pamd needs a 'context()' function but has no code cell.")
34
-
35
- try:
36
- exec(code_text, file_namespace)
37
- except Exception as e:
38
- raise RuntimeError(f"Error executing code in {file}.pamd: {e}")
39
-
40
- if "context" not in file_namespace:
41
- raise ValueError(f"The file {file}.pamd is missing the required 'context()' function.")
42
-
43
- context_func = file_namespace["context"]
44
-
45
- if not callable(context_func):
46
- raise TypeError(f"In {file}.pamd, 'context' was found but it is not a function!")
47
-
48
- result_dict = context_func()
49
-
50
- if not isinstance(result_dict, dict):
51
- raise TypeError(f"The 'context()' function in {file}.pamd must return a dictionary.")
52
-
53
- return result_dict
54
-
55
- def find_imports(text: str):
56
- # <ctx></ctx>
57
- pattern = r"<ctx>(.*?)</ctx>"
58
- return re.findall(pattern, text)
59
-
60
-
61
- def find_templates(text: str):
62
- # <tmp></tmp>
63
- pattern = r"<tmp>(.*?)</tmp>"
64
- return re.findall(pattern, text)
65
-
66
- def map_content(file: str, path: str):
67
- if path and not path.endswith('/'):
68
- full_path = f"{path}/{file}"
69
- elif path.endswith('/'):
70
- full_path = f"{path}{file}"
71
- else:
72
- full_path = file
73
-
74
- code_text, main_file = read_pamd_cells(f"{full_path}.pamd")
75
- templates = find_templates(main_file)
76
- imports = find_imports(main_file)
77
- meta = get_directory(full_path)
78
-
79
- ready_t = []
80
- for i in templates:
81
- ready_t.append(map_content(i, meta[0]))
82
-
83
- return {
84
- "tag_name": file,
85
- "name": meta[1],
86
- "path": meta[0],
87
- "imports": imports,
88
- "templates": ready_t
89
- }
90
-
91
- def process_content(build_tree: dict):
92
- file_path_base = build_tree.get("path", "") + build_tree.get("name", "")
93
- code_text, file_text = read_pamd_cells(file_path_base + ".pamd")
94
-
95
- needed_imports = build_tree.get("imports", [])
96
- if needed_imports:
97
- imports = get_imports(file_path_base, code_text)
98
- for i in needed_imports:
99
- file_text = file_text.replace(f"<ctx>{i}</ctx>", str(imports.get(i, '')))
100
-
101
- for i in build_tree.get("templates", []):
102
- file_text = file_text.replace(f"<tmp>{i.get('tag_name')}</tmp>", process_content(i))
103
- return file_text
File without changes
File without changes