201 lines
5.6 KiB
Python
201 lines
5.6 KiB
Python
|
|
"""
|
||
|
|
Path utilities for packaging operations.
|
||
|
|
|
||
|
|
Provides utilities for path resolution, rewriting, and
|
||
|
|
normalization within packages.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import re
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Dict, Set, List, Tuple
|
||
|
|
from urllib.parse import urlparse
|
||
|
|
|
||
|
|
from .errors import PackagingError
|
||
|
|
|
||
|
|
|
||
|
|
class PathUtils:
|
||
|
|
"""Utilities for path handling in packages."""
|
||
|
|
|
||
|
|
# Common markdown link patterns
|
||
|
|
IMAGE_PATTERN = re.compile(r'!\[([^\]]*)\]\(([^)]+)\)')
|
||
|
|
LINK_PATTERN = re.compile(r'(?<!!)\[([^\]]*)\]\(([^)]+)\)')
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def rewrite_asset_paths(content: str, asset_map: Dict[str, str]) -> str:
|
||
|
|
"""
|
||
|
|
Rewrite asset paths in markdown content.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
content: Markdown content to process
|
||
|
|
asset_map: Mapping from original paths to new paths
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Content with rewritten asset paths
|
||
|
|
"""
|
||
|
|
def replace_link(match):
|
||
|
|
text = match.group(1)
|
||
|
|
url = match.group(2)
|
||
|
|
|
||
|
|
# Skip external URLs
|
||
|
|
if PathUtils.is_external_url(url):
|
||
|
|
return match.group(0)
|
||
|
|
|
||
|
|
# Check if this path needs rewriting
|
||
|
|
normalized_path = str(Path(url).as_posix())
|
||
|
|
if normalized_path in asset_map:
|
||
|
|
return f''
|
||
|
|
|
||
|
|
return match.group(0)
|
||
|
|
|
||
|
|
def replace_markdown_link(match):
|
||
|
|
text = match.group(1)
|
||
|
|
url = match.group(2)
|
||
|
|
|
||
|
|
# Skip external URLs and anchors
|
||
|
|
if PathUtils.is_external_url(url) or url.startswith('#'):
|
||
|
|
return match.group(0)
|
||
|
|
|
||
|
|
# Check if this path needs rewriting
|
||
|
|
normalized_path = str(Path(url).as_posix())
|
||
|
|
if normalized_path in asset_map:
|
||
|
|
return f'[{text}]({asset_map[normalized_path]})'
|
||
|
|
|
||
|
|
return match.group(0)
|
||
|
|
|
||
|
|
# Process images first
|
||
|
|
content = PathUtils.IMAGE_PATTERN.sub(replace_link, content)
|
||
|
|
|
||
|
|
# Process links
|
||
|
|
content = PathUtils.LINK_PATTERN.sub(replace_markdown_link, content)
|
||
|
|
|
||
|
|
return content
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def is_external_url(url: str) -> bool:
|
||
|
|
"""
|
||
|
|
Check if a URL is external (has a scheme).
|
||
|
|
|
||
|
|
Args:
|
||
|
|
url: URL to check
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
True if external, False if local
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
parsed = urlparse(url)
|
||
|
|
return bool(parsed.scheme)
|
||
|
|
except Exception:
|
||
|
|
return False
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def normalize_path(path: str, base_path: Path = None) -> str:
|
||
|
|
"""
|
||
|
|
Normalize a path for consistent handling.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
path: Path to normalize
|
||
|
|
base_path: Base path for relative resolution
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Normalized path string
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
path_obj = Path(path)
|
||
|
|
|
||
|
|
# Resolve relative to base if provided
|
||
|
|
if base_path and not path_obj.is_absolute():
|
||
|
|
path_obj = base_path / path_obj
|
||
|
|
|
||
|
|
# Normalize and return as POSIX path
|
||
|
|
return str(path_obj.resolve().as_posix())
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
raise PackagingError(f"Failed to normalize path '{path}': {e}")
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def extract_referenced_paths(content: str) -> Set[str]:
|
||
|
|
"""
|
||
|
|
Extract all referenced paths from markdown content.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
content: Markdown content to analyze
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Set of referenced paths
|
||
|
|
"""
|
||
|
|
paths = set()
|
||
|
|
|
||
|
|
# Extract image references
|
||
|
|
for match in PathUtils.IMAGE_PATTERN.finditer(content):
|
||
|
|
url = match.group(2)
|
||
|
|
if not PathUtils.is_external_url(url):
|
||
|
|
paths.add(url)
|
||
|
|
|
||
|
|
# Extract link references
|
||
|
|
for match in PathUtils.LINK_PATTERN.finditer(content):
|
||
|
|
url = match.group(2)
|
||
|
|
if not PathUtils.is_external_url(url) and not url.startswith('#'):
|
||
|
|
paths.add(url)
|
||
|
|
|
||
|
|
return paths
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def resolve_relative_paths(paths: Set[str], base_path: Path) -> Dict[str, Path]:
|
||
|
|
"""
|
||
|
|
Resolve relative paths against a base path.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
paths: Set of paths to resolve
|
||
|
|
base_path: Base path for resolution
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Dictionary mapping original paths to resolved Path objects
|
||
|
|
"""
|
||
|
|
resolved = {}
|
||
|
|
|
||
|
|
for path_str in paths:
|
||
|
|
try:
|
||
|
|
path_obj = Path(path_str)
|
||
|
|
if not path_obj.is_absolute():
|
||
|
|
resolved_path = base_path / path_obj
|
||
|
|
else:
|
||
|
|
resolved_path = path_obj
|
||
|
|
|
||
|
|
resolved[path_str] = resolved_path.resolve()
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
# Skip problematic paths but log the issue
|
||
|
|
continue
|
||
|
|
|
||
|
|
return resolved
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def create_package_path(original_path: Path, package_root: str = "assets") -> str:
|
||
|
|
"""
|
||
|
|
Create a package-internal path for an asset.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
original_path: Original file path
|
||
|
|
package_root: Root directory within package
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Package-internal path
|
||
|
|
"""
|
||
|
|
# Use just the filename to avoid deep nesting
|
||
|
|
filename = original_path.name
|
||
|
|
return f"{package_root}/{filename}"
|
||
|
|
|
||
|
|
|
||
|
|
# Standalone utility functions for convenience
|
||
|
|
def rewrite_asset_paths(content: str, asset_map: Dict[str, str]) -> str:
|
||
|
|
"""
|
||
|
|
Standalone wrapper for PathUtils.rewrite_asset_paths.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
content: Markdown content to process
|
||
|
|
asset_map: Mapping from original paths to new paths
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Content with rewritten asset paths
|
||
|
|
"""
|
||
|
|
return PathUtils.rewrite_asset_paths(content, asset_map)
|