Loading processMDSpec.py +93 −15 Original line number Diff line number Diff line Loading @@ -10,10 +10,13 @@ """ from __future__ import annotations _print = print # save the original print function from typing import Tuple, Generator import argparse from rich import print, markdown import re, sys, yaml from rich import markdown, print import re, sys, yaml, os from contextlib import contextmanager Loading @@ -37,13 +40,79 @@ def includeStack(filename:str) -> Generator [None, None, None]: Generator: A generator that yields nothing. """ if filename in _includeStack: print(f'[red]Circular include detected: {filename}') raise Exception('Circular include detected') raise Exception(f'Circular include detected: {" -> ".join(_includeStack)} -> {filename}') _includeStack.append(filename) yield _includeStack.pop() def expandPaths(lines:list[str], currentPath:str, childPath:str) -> list[str]: """ Expand the paths in the markdown file. This means that all paths in links, images, and include statements are extended so that they would be valid paths from the root document. Args: lines: The lines of the markdown file. currentPath: The current path of the file being processed. childPath: The path of the child file being processed. Returns: list[str]: The lines of the markdown file with expanded paths. """ # Replace all relative paths in the markdown with the new path # add a path to the current path if currentPath[-1] != '/': currentPath += '/' newPath = currentPath + childPath # Remove the leading './' from the path while newPath.startswith('./'): newPath = newPath[2:] inCodeFence = False for index, line in enumerate(lines): # Ignore stuff in code fences if re.match(r'^\s*```.*', line): inCodeFence = not inCodeFence continue if inCodeFence: continue # handle the links in a line (there could be multiple links in a line) links = re.findall(r'\[([^\]]+)\]\(([^\)]+)\)', line) for linkText, linkPath in links: # Skip URLs and absolute paths if linkPath.startswith(('http://', 'https://', '/')): continue # Construct the new path by adding addedPath to the original path newLinkPath = linkPath[2:] if linkPath.startswith('./') else linkPath # Create the updated path updatedPath = f"{newPath}{linkPath}" if newPath.endswith('/') else f"{newPath}/{newLinkPath}" # Replace the original link with the updated one in the markdown line = line.replace(f'[{linkText}]({linkPath})', f'[{linkText}]({updatedPath})') # handle the include statements (there should only be one per line) includes = re.findall(r'^\s*::include{file=([^\}]+)}', line) for includePath in includes: # Construct the new path by adding addedPath to the original path includePath = includePath[2:] if includePath.startswith('./') else includePath # Create the updated path updatedPath = f'{newPath}{includePath}' if newPath.endswith('/') else f'{newPath}/{includePath}' # Replace the original include with the updated one in the markdown line = line.replace(f'::include{{file={includePath}}}', f'::include{{file={updatedPath}}}') lines[index] = line return lines def processFrontMatter(lines:list[str], args:argparse.Namespace) -> Tuple[dict, list[str]]: """ Process the front matter of a markdown file. This includes extracting the front matter information and returning it as a dictionary. Loading Loading @@ -97,7 +166,7 @@ def processFile(args:argparse.Namespace) -> str: The processed markdown content as a string. """ def handleIncludesForFile(filename:str) -> str: def handleIncludesForFile(filename:str, currentPath:str) -> str: """ Read a single markdown file and return its content. Args: Loading @@ -109,6 +178,14 @@ def processFile(args:argparse.Namespace) -> str: Returns: The content of the file. """ # Get the directory path from the filename dirname = os.path.dirname(filename) if dirname and not dirname.endswith('/'): dirname = dirname + '/' dirname = dirname if dirname else '.' currentPath = currentPath if currentPath else '.' filename = os.path.normpath(filename) with includeStack(filename): try: Loading @@ -118,7 +195,10 @@ def processFile(args:argparse.Namespace) -> str: print(f'[red]File not found: {filename}') raise # Expand the paths in the markdown file # extract front matter information lines = expandPaths(lines, currentPath, dirname) fm, lines = processFrontMatter(lines, args) if fm: _frontMatter[filename] = fm Loading @@ -129,7 +209,7 @@ def processFile(args:argparse.Namespace) -> str: inCodeFence = False for line in lines: # Ignore code fences # Ignore stuff code fences if re.match(r'^\s*```.*', line): inCodeFence = not inCodeFence continue Loading @@ -139,15 +219,13 @@ def processFile(args:argparse.Namespace) -> str: # Check for ::include{file=...} pattern using regex at the beginning of a line match = re.search(r'^::include\{\s*file=(.*?)\s*\}', line.strip()) if match: include_filename = match.group(1) includeFilename = match.group(1) # Read the included file and replace the include statement with its content include_content = handleIncludesForFile(include_filename) lines[lines.index(line)] = include_content lines[lines.index(line)] = handleIncludesForFile(includeFilename, os.path.dirname(filename)) return ''.join(lines) return handleIncludesForFile(args.document) return handleIncludesForFile(args.document, os.path.dirname(args.document)) if __name__ == '__main__': Loading @@ -170,7 +248,7 @@ if __name__ == '__main__': try: lines = processFile(args) except Exception as e: print(f'[red]Error processing file: {e}', file=sys.stderr) print(f'[red]Error while processing {args.document}\n{e}', file=sys.stderr) quit(1) if args.outputFrontMatter or args.onlyFrontMatter: Loading @@ -192,7 +270,7 @@ if __name__ == '__main__': print(markdown.Markdown(lines)) else: # Print the raw markdown content print(lines) _print(lines) Loading
processMDSpec.py +93 −15 Original line number Diff line number Diff line Loading @@ -10,10 +10,13 @@ """ from __future__ import annotations _print = print # save the original print function from typing import Tuple, Generator import argparse from rich import print, markdown import re, sys, yaml from rich import markdown, print import re, sys, yaml, os from contextlib import contextmanager Loading @@ -37,13 +40,79 @@ def includeStack(filename:str) -> Generator [None, None, None]: Generator: A generator that yields nothing. """ if filename in _includeStack: print(f'[red]Circular include detected: {filename}') raise Exception('Circular include detected') raise Exception(f'Circular include detected: {" -> ".join(_includeStack)} -> {filename}') _includeStack.append(filename) yield _includeStack.pop() def expandPaths(lines:list[str], currentPath:str, childPath:str) -> list[str]: """ Expand the paths in the markdown file. This means that all paths in links, images, and include statements are extended so that they would be valid paths from the root document. Args: lines: The lines of the markdown file. currentPath: The current path of the file being processed. childPath: The path of the child file being processed. Returns: list[str]: The lines of the markdown file with expanded paths. """ # Replace all relative paths in the markdown with the new path # add a path to the current path if currentPath[-1] != '/': currentPath += '/' newPath = currentPath + childPath # Remove the leading './' from the path while newPath.startswith('./'): newPath = newPath[2:] inCodeFence = False for index, line in enumerate(lines): # Ignore stuff in code fences if re.match(r'^\s*```.*', line): inCodeFence = not inCodeFence continue if inCodeFence: continue # handle the links in a line (there could be multiple links in a line) links = re.findall(r'\[([^\]]+)\]\(([^\)]+)\)', line) for linkText, linkPath in links: # Skip URLs and absolute paths if linkPath.startswith(('http://', 'https://', '/')): continue # Construct the new path by adding addedPath to the original path newLinkPath = linkPath[2:] if linkPath.startswith('./') else linkPath # Create the updated path updatedPath = f"{newPath}{linkPath}" if newPath.endswith('/') else f"{newPath}/{newLinkPath}" # Replace the original link with the updated one in the markdown line = line.replace(f'[{linkText}]({linkPath})', f'[{linkText}]({updatedPath})') # handle the include statements (there should only be one per line) includes = re.findall(r'^\s*::include{file=([^\}]+)}', line) for includePath in includes: # Construct the new path by adding addedPath to the original path includePath = includePath[2:] if includePath.startswith('./') else includePath # Create the updated path updatedPath = f'{newPath}{includePath}' if newPath.endswith('/') else f'{newPath}/{includePath}' # Replace the original include with the updated one in the markdown line = line.replace(f'::include{{file={includePath}}}', f'::include{{file={updatedPath}}}') lines[index] = line return lines def processFrontMatter(lines:list[str], args:argparse.Namespace) -> Tuple[dict, list[str]]: """ Process the front matter of a markdown file. This includes extracting the front matter information and returning it as a dictionary. Loading Loading @@ -97,7 +166,7 @@ def processFile(args:argparse.Namespace) -> str: The processed markdown content as a string. """ def handleIncludesForFile(filename:str) -> str: def handleIncludesForFile(filename:str, currentPath:str) -> str: """ Read a single markdown file and return its content. Args: Loading @@ -109,6 +178,14 @@ def processFile(args:argparse.Namespace) -> str: Returns: The content of the file. """ # Get the directory path from the filename dirname = os.path.dirname(filename) if dirname and not dirname.endswith('/'): dirname = dirname + '/' dirname = dirname if dirname else '.' currentPath = currentPath if currentPath else '.' filename = os.path.normpath(filename) with includeStack(filename): try: Loading @@ -118,7 +195,10 @@ def processFile(args:argparse.Namespace) -> str: print(f'[red]File not found: {filename}') raise # Expand the paths in the markdown file # extract front matter information lines = expandPaths(lines, currentPath, dirname) fm, lines = processFrontMatter(lines, args) if fm: _frontMatter[filename] = fm Loading @@ -129,7 +209,7 @@ def processFile(args:argparse.Namespace) -> str: inCodeFence = False for line in lines: # Ignore code fences # Ignore stuff code fences if re.match(r'^\s*```.*', line): inCodeFence = not inCodeFence continue Loading @@ -139,15 +219,13 @@ def processFile(args:argparse.Namespace) -> str: # Check for ::include{file=...} pattern using regex at the beginning of a line match = re.search(r'^::include\{\s*file=(.*?)\s*\}', line.strip()) if match: include_filename = match.group(1) includeFilename = match.group(1) # Read the included file and replace the include statement with its content include_content = handleIncludesForFile(include_filename) lines[lines.index(line)] = include_content lines[lines.index(line)] = handleIncludesForFile(includeFilename, os.path.dirname(filename)) return ''.join(lines) return handleIncludesForFile(args.document) return handleIncludesForFile(args.document, os.path.dirname(args.document)) if __name__ == '__main__': Loading @@ -170,7 +248,7 @@ if __name__ == '__main__': try: lines = processFile(args) except Exception as e: print(f'[red]Error processing file: {e}', file=sys.stderr) print(f'[red]Error while processing {args.document}\n{e}', file=sys.stderr) quit(1) if args.outputFrontMatter or args.onlyFrontMatter: Loading @@ -192,7 +270,7 @@ if __name__ == '__main__': print(markdown.Markdown(lines)) else: # Print the raw markdown content print(lines) _print(lines)