diff --git a/pelican/plugins/quarto/adapters.py b/pelican/plugins/quarto/adapters.py new file mode 100644 index 0000000..c31a74c --- /dev/null +++ b/pelican/plugins/quarto/adapters.py @@ -0,0 +1,61 @@ +import logging +from pathlib import Path +import subprocess + +logger = logging.getLogger(__name__) + + +class Quarto: + """Adapter Class for establishing and running Quarto.""" + + def __init__(self, path, output_path): + self.path = Path(path) + self.wdir = self.path.parent + self.output_path = output_path + self._setup_quarto_project() + + def _setup_quarto_project(self): + content_dir = self.path + quarto_config_path = content_dir / "_quarto.yml" + content_dir.mkdir(parents=True, exist_ok=True) + + if quarto_config_path.exists(): + logger.info( + f"_quarto.yml already exists at {quarto_config_path}, skipping setup." + ) + return + + output_dir_abs = self.wdir / self.output_path + + quarto_config = f""" +project: + type: website + output-dir: {output_dir_abs} + +format: + html: + theme: none + """ + with open(quarto_config_path, "w") as config_file: + config_file.write(quarto_config) + logger.info(f"_quarto.yml created at {quarto_config_path}") + + def run_quarto(self, filename): + try: + result = subprocess.run( + ["quarto", "render", filename, "--output", "-"], + cwd=str(Path(self.wdir) / "content"), + capture_output=True, + text=True, + check=False, + ) + if result.returncode == 0: + logger.info("Quarto render completed successfully.") + return result.stdout + logger.error( + f"Error while rendering Quarto Markdown File {filename}: {result.stderr}" + ) + return result.stderr + + except Exception: + logger.error("An exception occured while running Quarto: {e}") diff --git a/pelican/plugins/quarto/parsers.py b/pelican/plugins/quarto/parsers.py new file mode 100644 index 0000000..446f28d --- /dev/null +++ b/pelican/plugins/quarto/parsers.py @@ -0,0 +1,32 @@ +from bs4 import BeautifulSoup + + +class QuartoHTML: + """Facade to Quarto HTML Documents.""" + + def __init__(self, html_string): + self.soup = BeautifulSoup(html_string, "html.parser") + self.header = self._extract_header() + self.body = self._extract_body() + self.header_scripts_links = self._extract_header_scripts_links() + self.header_styles = self._extract_header_styles() + + def _extract_header(self): + header = self.soup.find("head") + return str(header) if header else "" + + def _extract_header_scripts_links(self): + """Extract + + + + + +
+
+

social

+ +
+
+ + + + + \ No newline at end of file diff --git a/pelican/plugins/quarto/tests/test_quarto.py b/pelican/plugins/quarto/tests/test_quarto.py new file mode 100644 index 0000000..1b56efc --- /dev/null +++ b/pelican/plugins/quarto/tests/test_quarto.py @@ -0,0 +1,83 @@ +import os +from pathlib import Path +from tempfile import TemporaryDirectory +from unittest.mock import patch + +from bs4 import BeautifulSoup +import pytest + +from pelican import Pelican +from pelican.settings import read_settings + +TESTFILE_NAME = "testqmd" + + +@pytest.fixture +def temp_path(): + """Create temp path for tests.""" + with TemporaryDirectory() as tempdir: + yield os.path.abspath(tempdir) + + +@pytest.fixture +def create_article(temp_path): + """Create dummy qmd report in content dir.""" + content_dir = Path(temp_path) / "content" + content_dir.mkdir(parents=True, exist_ok=True) + + article_content = """ +--- +title: testqmd +date: 2024-06-02 +category: test +--- +Hi + +""" + article_path = content_dir / f"{TESTFILE_NAME}.qmd" + article_path.write_text(article_content) + return article_path + + +@pytest.fixture +def quarto_run_mock(): + with patch("subprocess.run") as mock_run: + script_dir = Path(__file__).parent + + with open( + script_dir / "test_data" / "quarto_test_output.html", encoding="utf-8" + ) as f: + mock_html_content = f.read() + mock_run.return_value.stdout = mock_html_content + mock_run.return_value.returncode = 0 + yield mock_run + + +def test_plugin_functionality(create_article, temp_path, quarto_run_mock): + """Test basic plugin functionality: Header extraction.""" + path = Path(temp_path) + output_path = path / "output" + content_path = path / "content" + settings = read_settings( + override={ + "PATH": content_path, + "OUTPUT_PATH": output_path, + "PLUGIN_PATHS": ["../"], + "PLUGINS": ["quarto"], + } + ) + pelican = Pelican(settings=settings) + pelican.run() + + articles = os.listdir(output_path) + assert f"{TESTFILE_NAME}.html" in articles, "An article should have been written" + + filepath = output_path / f"{TESTFILE_NAME}.html" + with open(filepath, encoding="utf-8") as f: + html_content = f.read() + + soup = BeautifulSoup(html_content, "html.parser") + + assert soup.find("body") is not None, "The body of the HTML should exist" + quarto_script = soup.find("script", id="quarto-html-after-body") + assert quarto_script is not None, "Quarto-specific script not found in body" diff --git a/pyproject.toml b/pyproject.toml index 3f6a3ec..5d6d847 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,9 @@ classifiers = [ requires-python = ">=3.8.1,<4.0" dependencies = [ "pelican>=4.5", + "pyYAML>=6.0", + "markdown>=3.4", + "beautifulsoup4>=4.0", ] [project.urls] @@ -44,7 +47,6 @@ lint = [ "ruff>=0.6.0,<0.7.0", ] test = [ - "markdown>=3.4", "pytest>=7.0", "pytest-cov>=4.0", "pytest-sugar>=1.0", diff --git a/requirements.txt b/requirements.txt index 449ed95..e0fbaab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,6 @@ pytest==8.3.2 ruff==0.6.2 pelican==4.9.1 +pyYAML==6.0.2 +markdown==3.7 +beautifulsoup4==4.12.3 \ No newline at end of file