From 8c6c0d318eefee51875f6217443f4bf7cf36bedb Mon Sep 17 00:00:00 2001 From: omicron Date: Tue, 25 Aug 2026 03:05:30 +0200 Subject: [PATCH] Add referenced file handling to Content --- omicron/ssg/output/article.py | 35 +++++++++++++++++++++++++++++++++++ omicron/ssg/output/content.py | 24 ++++++++++++++++++++++++ omicron/ssg/site.py | 2 +- 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/omicron/ssg/output/article.py b/omicron/ssg/output/article.py index 63ae7e9..648181a 100644 --- a/omicron/ssg/output/article.py +++ b/omicron/ssg/output/article.py @@ -1,6 +1,8 @@ import logging +from urllib.parse import urlparse from typing import TYPE_CHECKING, Any from omicron.ssg.output.content import Content +from omicron.ssg.output.file import File from pathlib import Path import omicron.ssg.markdown as md @@ -28,8 +30,41 @@ class Article(Content): ) self.title: str = meta["title"] + def handle_reference(self, path: Path) -> Content | File: + site = self.site + if path in site.by_source: + return site.by_source[path] + return self.copy_referenced_file(path) + + def resolve_references(self, tokens: list[md.Token]) -> None: + # MarkdownIt guarantees links are always in the second level + for token in tokens: + if token.type != "inline" or token.children is None: + continue + for child in token.children: + if child.type == "link_open": + attr = "href" + elif child.type == "image": + attr = "src" + else: + continue + + href = child.attrs.get(attr) + assert type(href) is str, "unexpected link/image href type" + parsed = urlparse(href) + + if parsed.scheme or parsed.netloc: + continue + if not parsed.path or parsed.path.startswith("/"): + continue + target = self.locate_reference(Path(parsed.path)) + + output = self.handle_reference(target) + child.attrs[attr] = parsed._replace(path=output.url).geturl() + def write(self) -> None: tokens = md.parse(self.read_body()) + self.resolve_references(tokens) if self.summary is None: self.summary = md.render_summary(tokens) content = md.render(tokens) diff --git a/omicron/ssg/output/content.py b/omicron/ssg/output/content.py index 470deb5..02a8414 100644 --- a/omicron/ssg/output/content.py +++ b/omicron/ssg/output/content.py @@ -1,6 +1,7 @@ import re from typing import TYPE_CHECKING from omicron.ssg.output.output import Output, OutputError, is_normalized +from omicron.ssg.output.file import File from pathlib import Path from datetime import date @@ -49,6 +50,29 @@ class Content(Output): path = Path(f"{section}/{slug}/index.html") return path + def locate_reference(self, path: Path) -> Path: + if path.is_absolute(): + raise ContentError("Can't resolve absolute references") + base = self.site.site_path / "content" + file = (self.source.parent / path).resolve() + if not file.is_relative_to(base): + raise ContentError("Reference escapes content directory") + return file + + def copy_referenced_file(self, path: Path) -> File: + if not path.is_file(): + raise ContentError(f"Referenced file '{path}' does not exist") + + dest = self.destination.parent / path.name + existing = self.site.by_path.get(dest) + if isinstance(existing, File) and existing.source == path: + return existing + + file = File(self.site, dest, path) + self.site.add_by_path(file) + file.write() + return file + def read_body(self) -> str: with self.source.open("r", encoding="utf-8") as f: f.readline() # opening --- diff --git a/omicron/ssg/site.py b/omicron/ssg/site.py index 3360ba0..89d17d8 100644 --- a/omicron/ssg/site.py +++ b/omicron/ssg/site.py @@ -32,7 +32,7 @@ class ConfigError(RuntimeError): class Site: def __init__(self, site: Path): - self.site_path = site.absolute() + self.site_path = site.resolve() self.content: list[Content] = [] self.directories: list[Directory] = [] self.other_outputs: list[Output] = []