Skip to content
Snippets Groups Projects
Commit a6bd358c authored by Andreas Kraft's avatar Andreas Kraft
Browse files

Support directory-relative imports (change paths of links, images, includes) in sub-folders

parent 98b64bd6
No related branches found
No related tags found
1 merge request!1Restructuring and cleaning scripts for Mkdocs
......@@ -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
......@@ -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.
......@@ -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:
......@@ -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:
......@@ -117,8 +194,11 @@ def processFile(args:argparse.Namespace) -> str:
except FileNotFoundError:
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
......@@ -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
......@@ -139,17 +219,15 @@ 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__':
parser = argparse.ArgumentParser(description='Process markdown specification files.')
......@@ -158,7 +236,7 @@ if __name__ == '__main__':
parser.add_argument('--process-frontmatter', '-fm', dest='outputFrontMatter', action='store_true', help='output front matter only')
parser.add_argument('--frontmatter-only', '-fmo', dest='onlyFrontMatter', action='store_true', help='output only front matter')
parser.add_argument('--verbose', '-v', action='store_true', help='print debug information to stderr.')
parser.add_argument('document', type = str, help = 'a markdown specification document to process')
parser.add_argument('document', type=str, help='a markdown specification document to process')
args = parser.parse_args()
if args.verbose:
......@@ -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:
......@@ -192,7 +270,7 @@ if __name__ == '__main__':
print(markdown.Markdown(lines))
else:
# Print the raw markdown content
print(lines)
_print(lines)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment