Hugoifier

feat: package as hugoifier PyPI distribution - Rename src/ → hugoifier/ to make it a proper importable Python package - Add hugoifier/__init__.py and hugoifier/utils/__init__.py - Convert all imports to relative (from ..config, from .module) - Add pyproject.toml with setuptools config, ruff, pytest settings - Add setup.py shim, MANIFEST.in, CI workflow, PyPI publish workflow - Update Dockerfile to use pip install -e . and CMD ["hugoifier"] - Update all tests to use hugoifier.* import paths - Fix all ruff violations: unused imports, unsorted imports, trailing whitespace, long lines in source files - 87 tests passing, ruff clean

lmata 2026-03-12 22:14 trunk
Commit 91515c0ce303054cffc92874b3905bea579541de753da7fc6386feb0b6a405b2
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,32 @@
1
+name: CI
2
+
3
+on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+jobs:
10
+ test:
11
+ runs-on: ${{ matrix.os }}
12
+ strategy:
13
+ matrix:, macosmatrix:
14
+ os: [ubuntu-latest]
15
+ python-version: ["3.11", "3.12", "3.13"]
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - uses: actions/setup-python@v5
20
+ with:
21
+ python-version: ${{ matrix.python-version }}
22
+
23
+ - name: Install dependencies
24
+ run: |
25
+ python -m pip install --upgrade pip
26
+ pip install -e ".[dev]"
27
+
28
+ - name: Run tests
29
+ run: pytest tests/ -v --tb=short
30
+
31
+ - name: Lint
32
+ run: ru
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,32 @@
1 name: CI
2
3 on:
4 push:
5 branches: [main]
6 pull_request:
7 branches: [main]
8
9 jobs:
10 test:
11 runs-on: ${{ matrix.os }}
12 strategy:
13 matrix:, macosmatrix:
14 os: [ubuntu-latest]
15 python-version: ["3.11", "3.12", "3.13"]
16 steps:
17 - uses: actions/checkout@v4
18
19 - uses: actions/setup-python@v5
20 with:
21 python-version: ${{ matrix.python-version }}
22
23 - name: Install dependencies
24 run: |
25 python -m pip install --upgrade pip
26 pip install -e ".[dev]"
27
28 - name: Run tests
29 run: pytest tests/ -v --tb=short
30
31 - name: Lint
32 run: ru
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,44 @@
1
+name: Publish to PyPI
2
+
3
+on:
4
+ release:
5
+ types: [published]
6
+
7
+permissions:
8
+ id-token: write
9
+
10
+jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: "3.12"
19
+
20
+ - name: Install build tools
21
+ run: pip install build
22
+
23
+ - name: Build package
24
+ run: python -m build
25
+
26
+ - name: Upload artifacts
27
+ uses: actions/upload-artifact@v4
28
+ with:
29
+ name: dist
30
+ path: dist/
31
+
32
+ publish-pypi:
33
+ needs: build
34
+ runs-on: ubuntu-latest
35
+ environment: pypi
36
+ steps:
37
+ - name: Download artifacts
38
+ uses: actions/download-artifact@v4
39
+ with:
40
+ name: dist
41
+ path: dist/
42
+
43
+ - name: Publish to PyPI
44
+ uses: pypa/gh-action-pypi-publish@release/v1
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,44 @@
1 name: Publish to PyPI
2
3 on:
4 release:
5 types: [published]
6
7 permissions:
8 id-token: write
9
10 jobs:
11 build:
12 runs-on: ubuntu-latest
13 steps:
14 - uses: actions/checkout@v4
15
16 - uses: actions/setup-python@v5
17 with:
18 python-version: "3.12"
19
20 - name: Install build tools
21 run: pip install build
22
23 - name: Build package
24 run: python -m build
25
26 - name: Upload artifacts
27 uses: actions/upload-artifact@v4
28 with:
29 name: dist
30 path: dist/
31
32 publish-pypi:
33 needs: build
34 runs-on: ubuntu-latest
35 environment: pypi
36 steps:
37 - name: Download artifacts
38 uses: actions/download-artifact@v4
39 with:
40 name: dist
41 path: dist/
42
43 - name: Publish to PyPI
44 uses: pypa/gh-action-pypi-publish@release/v1
+4 -10
--- Dockerfile
+++ Dockerfile
@@ -5,19 +5,13 @@
55
WORKDIR /app
66
77
# Copy the requirements file into the container
88
COPY requirements.txt ./
99
10
-# Install any needed packages specified in requirements.txt
11
-RUN pip install --no-cache-dir -r requirements.txt
10
+# Install package and dependencies
11
+COPY pyproject.toml ./
12
+RUN pip install --no-cache-dir -e .
1213
1314
# Copy the rest of the application code into the container
1415
COPY . .
1516
16
-# Expose any necessary ports (if your application uses them)
17
-# EXPOSE 8000
18
-
19
-# Set environment variable for OpenAI API key (you may also pass this at runtime)
20
-# ENV OPENAI_API_KEY=your_openai_api_key
21
-
22
-# Define the command to run your application
23
-CMD ["python3", "src/cli.py"]
17
+CMD ["hugoifier"]
2418
2519
ADDED MANIFEST.in
2620
ADDED hugoifier/__init__.py
2721
ADDED hugoifier/cli.py
2822
ADDED hugoifier/config.py
2923
ADDED hugoifier/utils/__init__.py
3024
ADDED hugoifier/utils/analyze.py
3125
ADDED hugoifier/utils/cloudflare.py
3226
ADDED hugoifier/utils/complete.py
3327
ADDED hugoifier/utils/decapify.py
3428
ADDED hugoifier/utils/deploy.py
3529
ADDED hugoifier/utils/generate_decap_config.py
3630
ADDED hugoifier/utils/hugoify.py
3731
ADDED hugoifier/utils/parser.py
3832
ADDED hugoifier/utils/theme_finder.py
3933
ADDED hugoifier/utils/theme_patcher.py
4034
ADDED hugoifier/utils/translate.py
4135
ADDED pyproject.toml
4236
ADDED setup.py
4337
DELETED src/cli.py
4438
DELETED src/config.py
4539
DELETED src/utils/analyze.py
4640
DELETED src/utils/cloudflare.py
4741
DELETED src/utils/complete.py
4842
DELETED src/utils/decapify.py
4943
DELETED src/utils/deploy.py
5044
DELETED src/utils/generate_decap_config.py
5145
DELETED src/utils/hugoify.py
5246
DELETED src/utils/parser.py
5347
DELETED src/utils/theme_finder.py
5448
DELETED src/utils/theme_patcher.py
5549
DELETED src/utils/translate.py
--- Dockerfile
+++ Dockerfile
@@ -5,19 +5,13 @@
5 WORKDIR /app
6
7 # Copy the requirements file into the container
8 COPY requirements.txt ./
9
10 # Install any needed packages specified in requirements.txt
11 RUN pip install --no-cache-dir -r requirements.txt
 
12
13 # Copy the rest of the application code into the container
14 COPY . .
15
16 # Expose any necessary ports (if your application uses them)
17 # EXPOSE 8000
18
19 # Set environment variable for OpenAI API key (you may also pass this at runtime)
20 # ENV OPENAI_API_KEY=your_openai_api_key
21
22 # Define the command to run your application
23 CMD ["python3", "src/cli.py"]
24
25 DDED MANIFEST.in
26 DDED hugoifier/__init__.py
27 DDED hugoifier/cli.py
28 DDED hugoifier/config.py
29 DDED hugoifier/utils/__init__.py
30 DDED hugoifier/utils/analyze.py
31 DDED hugoifier/utils/cloudflare.py
32 DDED hugoifier/utils/complete.py
33 DDED hugoifier/utils/decapify.py
34 DDED hugoifier/utils/deploy.py
35 DDED hugoifier/utils/generate_decap_config.py
36 DDED hugoifier/utils/hugoify.py
37 DDED hugoifier/utils/parser.py
38 DDED hugoifier/utils/theme_finder.py
39 DDED hugoifier/utils/theme_patcher.py
40 DDED hugoifier/utils/translate.py
41 DDED pyproject.toml
42 DDED setup.py
43 ELETED src/cli.py
44 ELETED src/config.py
45 ELETED src/utils/analyze.py
46 ELETED src/utils/cloudflare.py
47 ELETED src/utils/complete.py
48 ELETED src/utils/decapify.py
49 ELETED src/utils/deploy.py
50 ELETED src/utils/generate_decap_config.py
51 ELETED src/utils/hugoify.py
52 ELETED src/utils/parser.py
53 ELETED src/utils/theme_finder.py
54 ELETED src/utils/theme_patcher.py
55 ELETED src/utils/translate.py
--- Dockerfile
+++ Dockerfile
@@ -5,19 +5,13 @@
5 WORKDIR /app
6
7 # Copy the requirements file into the container
8 COPY requirements.txt ./
9
10 # Install package and dependencies
11 COPY pyproject.toml ./
12 RUN pip install --no-cache-dir -e .
13
14 # Copy the rest of the application code into the container
15 COPY . .
16
17 CMD ["hugoifier"]
 
 
 
 
 
 
 
18
19 DDED MANIFEST.in
20 DDED hugoifier/__init__.py
21 DDED hugoifier/cli.py
22 DDED hugoifier/config.py
23 DDED hugoifier/utils/__init__.py
24 DDED hugoifier/utils/analyze.py
25 DDED hugoifier/utils/cloudflare.py
26 DDED hugoifier/utils/complete.py
27 DDED hugoifier/utils/decapify.py
28 DDED hugoifier/utils/deploy.py
29 DDED hugoifier/utils/generate_decap_config.py
30 DDED hugoifier/utils/hugoify.py
31 DDED hugoifier/utils/parser.py
32 DDED hugoifier/utils/theme_finder.py
33 DDED hugoifier/utils/theme_patcher.py
34 DDED hugoifier/utils/translate.py
35 DDED pyproject.toml
36 DDED setup.py
37 ELETED src/cli.py
38 ELETED src/config.py
39 ELETED src/utils/analyze.py
40 ELETED src/utils/cloudflare.py
41 ELETED src/utils/complete.py
42 ELETED src/utils/decapify.py
43 ELETED src/utils/deploy.py
44 ELETED src/utils/generate_decap_config.py
45 ELETED src/utils/hugoify.py
46 ELETED src/utils/parser.py
47 ELETED src/utils/theme_finder.py
48 ELETED src/utils/theme_patcher.py
49 ELETED src/utils/translate.py
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -0,0 +1,4 @@
1
+include LICENSE
2
+include README.md
3
+include requirements.txt
4
+recursive-include hugoifier *.py
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -0,0 +1,4 @@
 
 
 
 
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -0,0 +1,4 @@
1 include LICENSE
2 include README.md
3 include requirements.txt
4 recursive-include hugoifier *.py
--- a/hugoifier/__init__.py
+++ b/hugoifier/__init__.py
@@ -0,0 +1,3 @@
1
+"""Hugoifier — AI-powered Hugo theme converter with Decap CMS integration."""
2
+
3
+__version__ = "0.1.0"
--- a/hugoifier/__init__.py
+++ b/hugoifier/__init__.py
@@ -0,0 +1,3 @@
 
 
 
--- a/hugoifier/__init__.py
+++ b/hugoifier/__init__.py
@@ -0,0 +1,3 @@
1 """Hugoifier — AI-powered Hugo theme converter with Decap CMS integration."""
2
3 __version__ = "0.1.0"

No diff available

--- a/hugoifier/config.py
+++ b/hugoifier/config.py
@@ -0,0 +1,38 @@
1
+"""
2
+Multi-backend AI configuration.
3
+
4
+Set HUGOIFIER_BACKEND env var to switch backends:
5
+ anthropic (default) — claude-sonnet-4-6
6
+ openai — gpt-4-turbo
7
+ google — gemini-1.5-pro
8
+
9
+Model can be overridden per-backend:
10
+ ANTHROPIC_MODEL, OPENAI_MODEL, GOOGLE_MODEL
11
+"""
12
+
13
+import os
14
+
15
+BACKEND = os.getenv('HUGOIFIER_BACKEND', 'anthropic').lower()
16
+
17
+# Anthropic settings
18
+ANTHROPIC_API_KEY = os.getenv('ANTHROPIC_API_KEY')
19
+ANTHROPIC_MODEL = os.getenv('ANTHROPIC_MODEL', 'claude-sonnet-4-6')
20
+
21
+# OpenAI settings
22
+OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')
23
+OPENAI_MODEL = os.getenv('OPENAI_MODEL', 'gpt-4-turbo')
24
+
25
+# Google settings
26
+GOOGLE_API_KEY = os.getenv('GOOGLE_API_KEY')
27
+GOOGLE_MODEL = os.getenv('GOOGLE_MODEL', 'gemini-1.5-pro')
28
+
29
+MAX_TOKENS = int(os.getenv('HUGOIFIER_MAX_TOKENS', '4096'))
30
+
31
+
32
+def call_ai(prompt: str, system: str = "You are a helpful Hugo theme conversion assistant."x_tokens or MAX_TOKENS
33
+ if BACKEND == 'anthropic':
34
+ return _ca)
35
+ elif BACKEND == 'openai':
36
+ return _call_openai(prompt, system)
37
+ elif BACKEND == 'google':
38
+ retu
--- a/hugoifier/config.py
+++ b/hugoifier/config.py
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/hugoifier/config.py
+++ b/hugoifier/config.py
@@ -0,0 +1,38 @@
1 """
2 Multi-backend AI configuration.
3
4 Set HUGOIFIER_BACKEND env var to switch backends:
5 anthropic (default) — claude-sonnet-4-6
6 openai — gpt-4-turbo
7 google — gemini-1.5-pro
8
9 Model can be overridden per-backend:
10 ANTHROPIC_MODEL, OPENAI_MODEL, GOOGLE_MODEL
11 """
12
13 import os
14
15 BACKEND = os.getenv('HUGOIFIER_BACKEND', 'anthropic').lower()
16
17 # Anthropic settings
18 ANTHROPIC_API_KEY = os.getenv('ANTHROPIC_API_KEY')
19 ANTHROPIC_MODEL = os.getenv('ANTHROPIC_MODEL', 'claude-sonnet-4-6')
20
21 # OpenAI settings
22 OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')
23 OPENAI_MODEL = os.getenv('OPENAI_MODEL', 'gpt-4-turbo')
24
25 # Google settings
26 GOOGLE_API_KEY = os.getenv('GOOGLE_API_KEY')
27 GOOGLE_MODEL = os.getenv('GOOGLE_MODEL', 'gemini-1.5-pro')
28
29 MAX_TOKENS = int(os.getenv('HUGOIFIER_MAX_TOKENS', '4096'))
30
31
32 def call_ai(prompt: str, system: str = "You are a helpful Hugo theme conversion assistant."x_tokens or MAX_TOKENS
33 if BACKEND == 'anthropic':
34 return _ca)
35 elif BACKEND == 'openai':
36 return _call_openai(prompt, system)
37 elif BACKEND == 'google':
38 retu

No diff available

--- a/hugoifier/utils/analyze.py
+++ b/hugoifier/utils/analyze.py
@@ -0,0 +1,87 @@
1
+"""
2
+Analyzes a Hugo theme or raw HTML theme and reports structure + recommendations.
3
+"""
4
+
5
+import logging
6
+import os
7
+
8
+from ..config import call_ai
9
+from .theme_finder import find_hugo_theme, find_raw_html_files
10
+
11
+SYSTEM = "You are an expert Hugo theme developer analyzing themes for conversion."
12
+
13
+
14
+def analyze(path: str) -> str:
15
+ logging.info(f"Analyzing {path} ...")
16
+
17
+ info = find_hugo_theme(path)
18
+
19
+ if info:
20
+ return _analyze_hugo_theme(info)
21
+ else:
22
+ return _analyze_raw_html(path)
23
+
24
+
25
+def _analyze_hugo_theme(info: dict) -> str:
26
+ theme_dir = info['theme_dir']
27
+ theme_name = info['theme_name']
28
+ example_site = info['example_site']
29
+
30
+ # Collect layout files
31
+ layouts = []
32
+ for root, dirs, files in os.walk(os.path.join(theme_dir, 'layouts')):
33
+ for f in files:
34
+ if f.endswith('.html'):
35
+ rel = os.path.relpath(os.path.join(root, f), theme_dir)
36
+ layouts.append(rel)
37
+
38
+ # Collect content types from exampleSite
39
+ content_types = []
40
+ if example_site:
41
+ content_dir = os.path.join(example_site, 'content')
42
+ if os.path.isdir(content_dir):
43
+ content_types = [
44
+ d for d in os.listdir(content_dir)
45
+ if os.path.isdir(os.path.join(content_dir, d))
46
+ ]
47
+
48
+ report = [
49
+ f"Theme: {theme_name}",
50
+ f"Theme dir: {theme_dir}",
51
+ f"Layouts ({len(layouts)}):",
52
+ *[f" {layout}" for layout in sorted(layouts)],
53
+ f"Content types: {content_types}",
54
+ f"ExampleSite: {example_site or 'none'}",
55
+ "",
56
+ "Status: Already a Hugo theme. Use 'complete' to assemble a working site.",
57
+ ]
58
+ return "\n".join(report)
59
+
60
+
61
+def _analyze_raw_html(path: str) -> str:
62
+ html_files = find_raw_html_files(path)
63
+ if not html_files:
64
+ return f"No HTML files found at {path}"
65
+
66
+ # Read main HTML file for AI analysis
67
+ main = next(
68
+ (f for f in html_files if os.path.basename(f).lower() == 'index.html'),
69
+ html_files[0],
70
+ )
71
+ with open(main, 'r', errors='replace') as f:
72
+ html = f.read()[:20000]
73
+
74
+ prompt = f"""Analyze this HTML theme file and provide:
75
+1. Identified reusable components (header, footer, nav, sidebar, etc.)
76
+2. Recommended Hugo template tags for dynamic content
77
+3. Suggested partial splits
78
+4. Content sections that should be data/ YAML files for Decap CMS
79
+
80
+HTML:
81
+{html}"""
82
+
83
+ try:
84
+ return call_ai(prompt, SYSTEM)
85
+ except Exception as e:
86
+ logging.error(f"AI analysis failed: {e}")
87
+ return f"HTML theme with {len(html_files)} files. AI analysis failed: {e}"
--- a/hugoifier/utils/analyze.py
+++ b/hugoifier/utils/analyze.py
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/hugoifier/utils/analyze.py
+++ b/hugoifier/utils/analyze.py
@@ -0,0 +1,87 @@
1 """
2 Analyzes a Hugo theme or raw HTML theme and reports structure + recommendations.
3 """
4
5 import logging
6 import os
7
8 from ..config import call_ai
9 from .theme_finder import find_hugo_theme, find_raw_html_files
10
11 SYSTEM = "You are an expert Hugo theme developer analyzing themes for conversion."
12
13
14 def analyze(path: str) -> str:
15 logging.info(f"Analyzing {path} ...")
16
17 info = find_hugo_theme(path)
18
19 if info:
20 return _analyze_hugo_theme(info)
21 else:
22 return _analyze_raw_html(path)
23
24
25 def _analyze_hugo_theme(info: dict) -> str:
26 theme_dir = info['theme_dir']
27 theme_name = info['theme_name']
28 example_site = info['example_site']
29
30 # Collect layout files
31 layouts = []
32 for root, dirs, files in os.walk(os.path.join(theme_dir, 'layouts')):
33 for f in files:
34 if f.endswith('.html'):
35 rel = os.path.relpath(os.path.join(root, f), theme_dir)
36 layouts.append(rel)
37
38 # Collect content types from exampleSite
39 content_types = []
40 if example_site:
41 content_dir = os.path.join(example_site, 'content')
42 if os.path.isdir(content_dir):
43 content_types = [
44 d for d in os.listdir(content_dir)
45 if os.path.isdir(os.path.join(content_dir, d))
46 ]
47
48 report = [
49 f"Theme: {theme_name}",
50 f"Theme dir: {theme_dir}",
51 f"Layouts ({len(layouts)}):",
52 *[f" {layout}" for layout in sorted(layouts)],
53 f"Content types: {content_types}",
54 f"ExampleSite: {example_site or 'none'}",
55 "",
56 "Status: Already a Hugo theme. Use 'complete' to assemble a working site.",
57 ]
58 return "\n".join(report)
59
60
61 def _analyze_raw_html(path: str) -> str:
62 html_files = find_raw_html_files(path)
63 if not html_files:
64 return f"No HTML files found at {path}"
65
66 # Read main HTML file for AI analysis
67 main = next(
68 (f for f in html_files if os.path.basename(f).lower() == 'index.html'),
69 html_files[0],
70 )
71 with open(main, 'r', errors='replace') as f:
72 html = f.read()[:20000]
73
74 prompt = f"""Analyze this HTML theme file and provide:
75 1. Identified reusable components (header, footer, nav, sidebar, etc.)
76 2. Recommended Hugo template tags for dynamic content
77 3. Suggested partial splits
78 4. Content sections that should be data/ YAML files for Decap CMS
79
80 HTML:
81 {html}"""
82
83 try:
84 return call_ai(prompt, SYSTEM)
85 except Exception as e:
86 logging.error(f"AI analysis failed: {e}")
87 return f"HTML theme with {len(html_files)} files. AI analysis failed: {e}"
--- a/hugoifier/utils/cloudflare.py
+++ b/hugoifier/utils/cloudflare.py
@@ -0,0 +1,31 @@
1
+"""
2
+Configures and deploys a Hugo site to Cloudflare, handling page creation,
3
+DNS settings, and deployment via the Cloudflare API.
4
+"""
5
+
6
+import logging
7
+
8
+
9
+# Function to configure and deploy to Cloudflare
10
+def configure_cloudflare(path, zone):
11
+ logging.info(f"Starting Cloudflare configuration for {path} in zone {zone}...")
12
+ try:
13
+ # Placeholder logic for Cloudflare configuration
14
+ # This could involve API calls to Cloudflare to create pages, set DNS, etc.
15
+ logging.info("Creating Cloudflare page...")
16
+ # Example API call to create a page
17
+ # cloudflare_api.create_page(path, zone)
18
+
19
+ logging.info("Deploying site...")
20
+ # Example API call to deploy the site
21
+ # cloudflare_api.deploy_site(path, zone)
22
+
23
+ logging.info("Configuring DNS settings...")
24
+ # Example API call to configure DNS
25
+ # cloudflare_api.configure_dns(path, zone)
26
+
27
+ logging.info("Cloudflare configuration complete.")
28
+ return "Cloudflare configuration complete"
29
+ except Exception as e:
30
+ logging.error(f"Error during Cloudflare configuration: {e}")
31
+ return "Cloudflare configuration failed"
--- a/hugoifier/utils/cloudflare.py
+++ b/hugoifier/utils/cloudflare.py
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/hugoifier/utils/cloudflare.py
+++ b/hugoifier/utils/cloudflare.py
@@ -0,0 +1,31 @@
1 """
2 Configures and deploys a Hugo site to Cloudflare, handling page creation,
3 DNS settings, and deployment via the Cloudflare API.
4 """
5
6 import logging
7
8
9 # Function to configure and deploy to Cloudflare
10 def configure_cloudflare(path, zone):
11 logging.info(f"Starting Cloudflare configuration for {path} in zone {zone}...")
12 try:
13 # Placeholder logic for Cloudflare configuration
14 # This could involve API calls to Cloudflare to create pages, set DNS, etc.
15 logging.info("Creating Cloudflare page...")
16 # Example API call to create a page
17 # cloudflare_api.create_page(path, zone)
18
19 logging.info("Deploying site...")
20 # Example API call to deploy the site
21 # cloudflare_api.deploy_site(path, zone)
22
23 logging.info("Configuring DNS settings...")
24 # Example API call to configure DNS
25 # cloudflare_api.configure_dns(path, zone)
26
27 logging.info("Cloudflare configuration complete.")
28 return "Cloudflare configuration complete"
29 except Exception as e:
30 logging.error(f"Error during Cloudflare configuration: {e}")
31 return "Cloudflare configuration failed"
--- a/hugoifier/utils/complete.py
+++ b/hugoifier/utils/complete.py
@@ -0,0 +1,250 @@
1
+"""
2
+Full end-to-end pipeline: detect → copy → configure → decap.
3
+
4
+For already-Hugo themes: assembles a clean, standalone site.
5
+For raw HTML themes: calls hugoify first, then assembles.
6
+"""
7
+
8
+import logging
9
+import os
10
+import shutil
11
+from pathlib import Path
12
+
13
+from .decapify import decapify
14
+from .hugoify import hugoify_html
15
+from .theme_finder imraw_html_files
16
+from .theme_patcher import patch_config, patch_theme
17
+
18
+
19
+def complete(
20
+ input_path: str,
21
+ output_dir: str = None,
22
+ cms_name: str = None,
23
+ cms_logo: str = None,
24
+ cms_color: str = None,
25
+) -> str:
26
+ """
27
+ Run the full pipeline for a theme.
28
+
29
+ Args:
30
+ input_path: Path to a theme directory (from themes/) or raw HTML dir.
31
+ output_dir: Where to write the output site. Defaults to output/{theme-name}.
32
+ cms_name: Whitelabel CMS name for Decap admin UI.
33
+ cms_logo: Whitelabel logo URL for Decap admin UI.
34
+ cms_color: Whitelabel top-bar color for Decap admin UI.
35
+
36
+ Returns:
37
+ Path to the generated site, or error message.
38
+ """
39
+ logging.info(f"Starting pipeline for {input_path} ...")
40
+
41
+ branding = {'cms_name': cms_name, 'cms_logo': cms_logo, 'cms_color': cms_color}
42
+ info = find_hugo_theme(input_path)
43
+
44
+ if info:
45
+ return _assemble_hugo_site(info, output_dir, branding)
46
+t_raw_html(input_p# Raw HTML path
47
+ html_files = find_raw_ if not html_files:
48
+ or HTML files fh)
49
+ if nextjs_infraw_html(input_path, html_files, output_dir, branding)
50
+
51
+
52
+# ---------------------------------------------------------------------------
53
+# Hugo theme path
54
+# ---------------------------------------------------------------------------
55
+
56
+def _assemble_hugo_site(info: dict, output_dir: str = None, branding: dict = None) -> str:
57
+ theme_dir = info['theme_dir']
58
+ example_site = info['example_site']
59
+ theme_name = info['theme_name']
60
+
61
+ if output_dir is None:
62
+ output_dir = str(Path(__file__).parents[2] / 'output' / theme_name)
63
+
64
+ logging.info(f"Building site at {output_dir} ...")
65
+ os.makedirs(output_dir, exist_ok=True)
66
+
67
+ # 1. Copy theme files → themes/{theme_name}/
68
+ dest_theme = os.path.join(output_dir, 'themes', theme_name)
69
+ _copy_dir(theme_dir, dest_theme, exclude={'exampleSite', '__MACOSX', '.DS_Store'})
70
+ logging.info(f"Copied theme to {dest_theme}")
71
+ patch_theme(dest_themy-Hugo themes: assembles a clean, standalone site.
72
+For raw HTML themes: calls hugoify first, then assembles.
73
+"""
74
+
75
+import logging
76
+import os
77
+import shutil
78
+from pathlib import Path
79
+
80
+from .decapify import decapify
81
+from .hugoify import hugoify_html, hugoify_nextjs
82
+from .theme_finder import find_hugo_theme, find_nextjs_app, find_raw_html_files
83
+from .theme_patcher import patch_config, patch_theme
84
+
85
+
86
+def complete(
87
+ input_path: str,
88
+ output_dir: str = None,
89
+ cms_name: str = None,
90
+ cms_logo: str = None,
91
+ cms_color: str = None,
92
+) -> str:
93
+ """
94
+ Run the full pipeline for a theme.
95
+
96
+ Args:
97
+ input_path: Path to a theme directory (from themes/) or raw HTML dir.
98
+ output_dir: Where to write the output site. Defaults to output/{theme-name}.
99
+ cms_name: Whitelabel CMS name for Decap admin UI.
100
+ cms_logo: Whitelabel logo URL for Decap admin UI.
101
+ cms_color: Whitelabel top-bar color for Decap admin UI.
102
+
103
+ Returns:
104
+ Path to the generated site, or error message.
105
+ """
106
+ logging.info(f"Starting pipeline for {input_path} ...")
107
+
108
+ branding = {'cms_name': cms_name, 'cms_logo': cms_logo, 'cms_color': cms_color}
109
+ info = find_hugo_theme(input_path)
110
+
111
+ if info:
112
+ return _assemble_hugo_site(info, output_dir, branding)
113
+
114
+ # Next.js path (check before raw HTML since Next.js projects may contain .html files)
115
+ nextjs_info = find_nextjs_app(input_path)
116
+ if nextjs_info:
117
+ return _convert_nextjs(input_path, nextjs_info, output_dir, branding)
118
+
119
+ # Raw HTML path
120
+ html_files = find_raw_html_files(input_path)
121
+ if not html_files:
122
+ raise ValueError(f"No Hugo theme, Next.js app, or HTML files found in {input_path}")
123
+ return _convert_raw_html(input_path, html_files, output_dir, branding)
124
+
125
+
126
+# ---------------------------------------------------------------------------
127
+# Hugo theme path
128
+# ---------------------------------------------------------------------------
129
+
130
+def _assemble_hugo_site(info: dict, output_dir: str = None, branding: dict = None) -> str:
131
+ theme_dir = info['theme_dir']
132
+ example_site = info['example_site']
133
+ theme_name = info['theme_name']
134
+
135
+ if output_dir is None:
136
+ output_dir = str(Path(__file__).parents[2] / 'output' / theme_name)
137
+
138
+ logging.info(f"Building site at {output_dir} ...")
139
+ os.makedirs(output_dir, exist_ok=True)
140
+
141
+ # 1. Copy theme files → themes/{theme_name}/
142
+ dest_theme = os.path.join(output_dir, 'themes', theme_name)
143
+ _copy_dir(theme_dir, dest_theme, exclude={'exampleSite', '__MACOSX', '.DS_Store'})
144
+ logging.info(f"Copied theme to {dest_theme}")
145
+ patch_theme(dest_theme)
146
+
147
+ # 2. Copy exampleSite content/static/data → site root
148
+ if example_site:
149
+ for subdir in ('content', 'data', 'i18n'):
150
+ src = os.path.join(example_site, subdir)
151
+ if os.path.isdir(src):
152
+ _copy_dir(src, os.path.join(output_dir, subdir))
153
+ logging.info(f"Copied {subdir}/ from exampleSite")
154
+
155
+ # Static: merge exampleSite/static into output/static
156
+ src_static = os.path.join(example_site, 'static')
157
+ if os.path.isdir(src_static):
158
+ _copy_dir(src_static, os.path.join(output_dir, 'static'))
159
+ logging.info("Copied static/ from exampleSite")
160
+
161
+ # Write hugo.toml from exampleSite config
162
+ config_toml = _find_config(example_site)
163
+ if config_toml:
164
+ _write_hugo_toml(config_toml, output_dir, theme_name)
165
+ else:
166
+ _write_minimal_hugo_toml(output_dir, theme_name)
167
+ else:
168
+ _write_minimal_hugo_toml(output_dir, theme_name)
169
+ # Create minimal content/_index.md
170
+ content_dir = os.path.join(output_dir, 'content')
171
+ os.makedirs(content_dir, exist_ok=True)
172
+ index_md = os.path.join(content_dir, '_index.md')
173
+ if not os.path.exists(index_md):
174
+ with open(index_md, 'w') as f:
175
+ f.write('---\ntitle: Home\n---\n')
176
+
177
+ # 3. Generate Decap CMS config
178
+ b = branding or {}
179
+ decapify(
180
+ output_dir,
181
+ cms_name=b.get('cms_name'), cms_logo=b.get('cms_logo'), cms_color=b.get('cms_color'),
182
+ )
183
+
184
+ logging.info(f"Done. Site ready at: {output_dir}")
185
+ logging.info(f"Run: cd {output_dir} && hugo serve")
186
+ return output_dir
187
+
188
+
189
+# ---------------------------------------------------------------------------
190
+# Next.js path
191
+# ---------------------------------------------------------------------------
192
+
193
+def _convert_nextjs(
194
+ input_path: str, nextjs_info: dict, output_dir: str = None, branding: dict = None
195
+) -> st"""
196
+Full end-to-end pipeline: detect → copy → configure → decap.
197
+
198
+For already-Hugo themes: assembles a clean, standalone site.
199
+For raw HTML themes: calls hugoify first, then assembles.
200
+"""
201
+
202
+import logging
203
+import os
204
+import shutil
205
+from pathlib import Path
206
+
207
+from .decapify import decapify
208
+from .hugoify import hugoify_html, hugoify_nextjs
209
+from .theme_finder import find_hugo_theme, find_nextjs_app, find_raw_html_files
210
+from .theme_patcher import patch_config, patch_theme
211
+
212
+
213
+def complete(
214
+ input_path: str,
215
+ output_dir: str = None,
216
+ cms_name: str = None,
217
+ cms_logo: str = None,
218
+ cms_color: str = None,
219
+) -> str:
220
+ """
221
+ Run the full pipeline for a theme.
222
+
223
+ Args:
224
+ input_path: Path to a theme directory (from themes/) or raw HTML dir.
225
+ output_dir: Where to write the output site. Defaults to output/{theme-name}.
226
+ cms_name: Whitelabel CMS name for Decap admin UI.
227
+ cms_logo: Whitelabel logo URL for Decap admin UI.
228
+ cms_color: Whitelabel top-bar color for Decap admin UI.
229
+
230
+ Returns:
231
+ Path to the generated site, or error message.
232
+ """
233
+ logging.info(f"Starting pipeline for {input_path} ...")
234
+
235
+ branding = {'cms_name': cms_name, 'cms_logo': cms_logo, 'cms_color': cms_color}
236
+ info = find_hugo_theme(input_path)
237
+
238
+ if info:
239
+ return _assemble_hugo_site(info, output_dir, branding)
240
+
241
+ # Next.js path (check before raw HTML since Next.js projects may contain .html files)
242
+ nextjs_info = find_nextjs_app(input_path)
243
+ if nextjs_info:
244
+ return _convert_nextjs(input_path, nextjs_info, output_dir, branding)
245
+
246
+ # Raw HTML path
247
+ html_files = find_raw_html_files(input_path)
248
+ if not html_files:
249
+ raise ValueError(f"No Hugo theme, Next.js app, or HTML files found in {input_path}")
250
+ return _convert_raw_html(input_path, html_files,
--- a/hugoifier/utils/complete.py
+++ b/hugoifier/utils/complete.py
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/hugoifier/utils/complete.py
+++ b/hugoifier/utils/complete.py
@@ -0,0 +1,250 @@
1 """
2 Full end-to-end pipeline: detect → copy → configure → decap.
3
4 For already-Hugo themes: assembles a clean, standalone site.
5 For raw HTML themes: calls hugoify first, then assembles.
6 """
7
8 import logging
9 import os
10 import shutil
11 from pathlib import Path
12
13 from .decapify import decapify
14 from .hugoify import hugoify_html
15 from .theme_finder imraw_html_files
16 from .theme_patcher import patch_config, patch_theme
17
18
19 def complete(
20 input_path: str,
21 output_dir: str = None,
22 cms_name: str = None,
23 cms_logo: str = None,
24 cms_color: str = None,
25 ) -> str:
26 """
27 Run the full pipeline for a theme.
28
29 Args:
30 input_path: Path to a theme directory (from themes/) or raw HTML dir.
31 output_dir: Where to write the output site. Defaults to output/{theme-name}.
32 cms_name: Whitelabel CMS name for Decap admin UI.
33 cms_logo: Whitelabel logo URL for Decap admin UI.
34 cms_color: Whitelabel top-bar color for Decap admin UI.
35
36 Returns:
37 Path to the generated site, or error message.
38 """
39 logging.info(f"Starting pipeline for {input_path} ...")
40
41 branding = {'cms_name': cms_name, 'cms_logo': cms_logo, 'cms_color': cms_color}
42 info = find_hugo_theme(input_path)
43
44 if info:
45 return _assemble_hugo_site(info, output_dir, branding)
46 t_raw_html(input_p# Raw HTML path
47 html_files = find_raw_ if not html_files:
48 or HTML files fh)
49 if nextjs_infraw_html(input_path, html_files, output_dir, branding)
50
51
52 # ---------------------------------------------------------------------------
53 # Hugo theme path
54 # ---------------------------------------------------------------------------
55
56 def _assemble_hugo_site(info: dict, output_dir: str = None, branding: dict = None) -> str:
57 theme_dir = info['theme_dir']
58 example_site = info['example_site']
59 theme_name = info['theme_name']
60
61 if output_dir is None:
62 output_dir = str(Path(__file__).parents[2] / 'output' / theme_name)
63
64 logging.info(f"Building site at {output_dir} ...")
65 os.makedirs(output_dir, exist_ok=True)
66
67 # 1. Copy theme files → themes/{theme_name}/
68 dest_theme = os.path.join(output_dir, 'themes', theme_name)
69 _copy_dir(theme_dir, dest_theme, exclude={'exampleSite', '__MACOSX', '.DS_Store'})
70 logging.info(f"Copied theme to {dest_theme}")
71 patch_theme(dest_themy-Hugo themes: assembles a clean, standalone site.
72 For raw HTML themes: calls hugoify first, then assembles.
73 """
74
75 import logging
76 import os
77 import shutil
78 from pathlib import Path
79
80 from .decapify import decapify
81 from .hugoify import hugoify_html, hugoify_nextjs
82 from .theme_finder import find_hugo_theme, find_nextjs_app, find_raw_html_files
83 from .theme_patcher import patch_config, patch_theme
84
85
86 def complete(
87 input_path: str,
88 output_dir: str = None,
89 cms_name: str = None,
90 cms_logo: str = None,
91 cms_color: str = None,
92 ) -> str:
93 """
94 Run the full pipeline for a theme.
95
96 Args:
97 input_path: Path to a theme directory (from themes/) or raw HTML dir.
98 output_dir: Where to write the output site. Defaults to output/{theme-name}.
99 cms_name: Whitelabel CMS name for Decap admin UI.
100 cms_logo: Whitelabel logo URL for Decap admin UI.
101 cms_color: Whitelabel top-bar color for Decap admin UI.
102
103 Returns:
104 Path to the generated site, or error message.
105 """
106 logging.info(f"Starting pipeline for {input_path} ...")
107
108 branding = {'cms_name': cms_name, 'cms_logo': cms_logo, 'cms_color': cms_color}
109 info = find_hugo_theme(input_path)
110
111 if info:
112 return _assemble_hugo_site(info, output_dir, branding)
113
114 # Next.js path (check before raw HTML since Next.js projects may contain .html files)
115 nextjs_info = find_nextjs_app(input_path)
116 if nextjs_info:
117 return _convert_nextjs(input_path, nextjs_info, output_dir, branding)
118
119 # Raw HTML path
120 html_files = find_raw_html_files(input_path)
121 if not html_files:
122 raise ValueError(f"No Hugo theme, Next.js app, or HTML files found in {input_path}")
123 return _convert_raw_html(input_path, html_files, output_dir, branding)
124
125
126 # ---------------------------------------------------------------------------
127 # Hugo theme path
128 # ---------------------------------------------------------------------------
129
130 def _assemble_hugo_site(info: dict, output_dir: str = None, branding: dict = None) -> str:
131 theme_dir = info['theme_dir']
132 example_site = info['example_site']
133 theme_name = info['theme_name']
134
135 if output_dir is None:
136 output_dir = str(Path(__file__).parents[2] / 'output' / theme_name)
137
138 logging.info(f"Building site at {output_dir} ...")
139 os.makedirs(output_dir, exist_ok=True)
140
141 # 1. Copy theme files → themes/{theme_name}/
142 dest_theme = os.path.join(output_dir, 'themes', theme_name)
143 _copy_dir(theme_dir, dest_theme, exclude={'exampleSite', '__MACOSX', '.DS_Store'})
144 logging.info(f"Copied theme to {dest_theme}")
145 patch_theme(dest_theme)
146
147 # 2. Copy exampleSite content/static/data → site root
148 if example_site:
149 for subdir in ('content', 'data', 'i18n'):
150 src = os.path.join(example_site, subdir)
151 if os.path.isdir(src):
152 _copy_dir(src, os.path.join(output_dir, subdir))
153 logging.info(f"Copied {subdir}/ from exampleSite")
154
155 # Static: merge exampleSite/static into output/static
156 src_static = os.path.join(example_site, 'static')
157 if os.path.isdir(src_static):
158 _copy_dir(src_static, os.path.join(output_dir, 'static'))
159 logging.info("Copied static/ from exampleSite")
160
161 # Write hugo.toml from exampleSite config
162 config_toml = _find_config(example_site)
163 if config_toml:
164 _write_hugo_toml(config_toml, output_dir, theme_name)
165 else:
166 _write_minimal_hugo_toml(output_dir, theme_name)
167 else:
168 _write_minimal_hugo_toml(output_dir, theme_name)
169 # Create minimal content/_index.md
170 content_dir = os.path.join(output_dir, 'content')
171 os.makedirs(content_dir, exist_ok=True)
172 index_md = os.path.join(content_dir, '_index.md')
173 if not os.path.exists(index_md):
174 with open(index_md, 'w') as f:
175 f.write('---\ntitle: Home\n---\n')
176
177 # 3. Generate Decap CMS config
178 b = branding or {}
179 decapify(
180 output_dir,
181 cms_name=b.get('cms_name'), cms_logo=b.get('cms_logo'), cms_color=b.get('cms_color'),
182 )
183
184 logging.info(f"Done. Site ready at: {output_dir}")
185 logging.info(f"Run: cd {output_dir} && hugo serve")
186 return output_dir
187
188
189 # ---------------------------------------------------------------------------
190 # Next.js path
191 # ---------------------------------------------------------------------------
192
193 def _convert_nextjs(
194 input_path: str, nextjs_info: dict, output_dir: str = None, branding: dict = None
195 ) -> st"""
196 Full end-to-end pipeline: detect → copy → configure → decap.
197
198 For already-Hugo themes: assembles a clean, standalone site.
199 For raw HTML themes: calls hugoify first, then assembles.
200 """
201
202 import logging
203 import os
204 import shutil
205 from pathlib import Path
206
207 from .decapify import decapify
208 from .hugoify import hugoify_html, hugoify_nextjs
209 from .theme_finder import find_hugo_theme, find_nextjs_app, find_raw_html_files
210 from .theme_patcher import patch_config, patch_theme
211
212
213 def complete(
214 input_path: str,
215 output_dir: str = None,
216 cms_name: str = None,
217 cms_logo: str = None,
218 cms_color: str = None,
219 ) -> str:
220 """
221 Run the full pipeline for a theme.
222
223 Args:
224 input_path: Path to a theme directory (from themes/) or raw HTML dir.
225 output_dir: Where to write the output site. Defaults to output/{theme-name}.
226 cms_name: Whitelabel CMS name for Decap admin UI.
227 cms_logo: Whitelabel logo URL for Decap admin UI.
228 cms_color: Whitelabel top-bar color for Decap admin UI.
229
230 Returns:
231 Path to the generated site, or error message.
232 """
233 logging.info(f"Starting pipeline for {input_path} ...")
234
235 branding = {'cms_name': cms_name, 'cms_logo': cms_logo, 'cms_color': cms_color}
236 info = find_hugo_theme(input_path)
237
238 if info:
239 return _assemble_hugo_site(info, output_dir, branding)
240
241 # Next.js path (check before raw HTML since Next.js projects may contain .html files)
242 nextjs_info = find_nextjs_app(input_path)
243 if nextjs_info:
244 return _convert_nextjs(input_path, nextjs_info, output_dir, branding)
245
246 # Raw HTML path
247 html_files = find_raw_html_files(input_path)
248 if not html_files:
249 raise ValueError(f"No Hugo theme, Next.js app, or HTML files found in {input_path}")
250 return _convert_raw_html(input_path, html_files,
--- a/hugoifier/utils/decapify.py
+++ b/hugoifier/utils/decapify.py
@@ -0,0 +1,271 @@
1
+"""
2
+Generates Decap CMS integration for a Hugo site.
3
+
4
+Writes:
5
+ static/admin/index.html — Decap CMS admin panel
6
+ static/admin/config.yml — CMS config mapped to actual content structure
7
+"""
8
+
9
+import logging
10
+import os
11
+import re
12
+
13
+import yaml
14
+
15
+DECAP_CDN = "https://unpkg.com/decap-cms@^3.0.0/dist/decap-cms.js"
16
+
17
+# Whitelabel defaults — override via decapify() kwargs or env vars
18
+DEFAULT_CMS_NAME = os.getenv('CMS_NAME', 'Content Manager')
19
+DEFAULT_CMS_LOGO = os.getenv('CMS_LOGO_URL', '') # URL or empty
20
+DEFAULT_CMS_COLOR = os.getenv('CMS_COLOR', '#2e3748') # top-bar background
21
+
22
+
23
+def decapify(
24
+ site_dir: str,
25
+ cms_name: str = None,
26
+ cms_logo: str = None,
27
+) -> str:
28
+ """r = None,
29
+) -> str:
30
+ """
31
+ Add Decap CMS to a Hugo site directory.
32
+
33
+ Args:
34
+ site_dir: Root of the assembled Hugo site (has hugo.toml, content/, thWhitelabel name shown in the admin UI (default: 'Content Manager').
35
+ cms_logo: er').
36
+ cms_logo: URL to a logo image for the admin UI (optnal).
37
+ cms_color:
38
+ Returns:
39
+ Status message.
40
+ """
41
+ logging.info(f"Adding Decap CMS to {site_dir} ...")
42
+
43
+ admin_dir = os.path.join(site_dir, 'static', 'admin')
44
+ os.makedirs(admin_dir, exist_ok=True)
45
+
46
+ branding = {
47
+ 'name': cms_name or DEFAULT_CMS_NAME,
48
+ 'logo': cms_logo or DEFAULT_CMS_LOGO,
49
+ 'color': cms_color or DEFAULT_CMS_COLOR,
50
+ }
51
+
52
+ _write_admin_index(admin_dir, branding)
53
+ _write_decap_config(site_dir, admin_dir _create_media_dir(site_dir)
54
+
55
+ logging.info("Decap CMS integration complete.")
56
+ return "Decap CMS integration complete"
57
+
58
+
59
+# ---------------------------------------------------------------------------
60
+# Admin index.html
61
+# ---------------------------------------------------------------------------
62
+
63
+def _sanitize_color(color: str) -> str:
64
+ """Allow only valid CSS hex colors (#rgb or #rrggbb) to prevent style injection."""
65
+ if re.fullmatch(r'#[0-9a-fA-F]{3}(?:[0-9a-fA-F]{3})?', color):
66
+ return color
67
+ return '#2e3748' # fall back to default
68
+
69
+
70
+def _write_admin_index(admin_dir: str, branding: dict):
71
+ import html as html_mod
72
+ name = html_mod.escape(branding['name'])
73
+ logo_html = ''
74
+ if branding['logo']:
75
+ logo_url = html_mod.escape(branding['logo'])
76
+ logo_html = f'\n <img src="{logo_url}" alt="{name}" style="max-height:40px;margin:8px 0;">'
77
+
78
+ color_css = ''
79
+ if branding['color']:
80
+ safe_color = _sanitize_color(branding['color'])
81
+ color_css = f"""
82
+ <style>
83
+ [class^="AppHeader"] {{ background-color: {safe_color} !important; }}
84
+ </style>"""
85
+
86
+ html = f"""<!doctype html>
87
+<html>
88
+<head>
89
+ <meta charset="utf-8" />
90
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
91
+ <meta name="robots" content="noindex" />
92
+ <title>{name}</title>{color_css}
93
+</head>
94
+<body>{logo_html}
95
+ <script src="{DECAP_CDN}"></script>
96
+</body>
97
+</html>
98
+"""
99
+ with open(os.path.join(admin_dir, 'index.html'), 'w') as f:
100
+ f.write(html)
101
+
102
+
103
+# ---------------------------------------------------------------------------
104
+# config.yml
105
+# ---------------------------------------------------------------------------
106
+edia_dir(site_dir)
107
+
108
+ logging.info("Decap CMS integration complete.")
109
+ return gbb) to prevent style injectio{
110
+ ---------------------gateway',
111
+ 'branch': 'main',
112
+('client_id', clienullmatch(r'#[0-9a-fA-F]{3}(?:[0-9a-fA-F]{3})?', color):
113
+ return color
114
+ return '#2e3748' # fall back to default
115
+
116
+
117
+def _write_admin_index(admin_dir: str, branding: dict):
118
+ import html as html_mod
119
+ name = html_mod.escape(branding['name'])
120
+ logo_html = ''
121
+ if branding['logo']:
122
+ logo_url = html_mod.escape(branding['logo'])
123
+ logo_html = f'\n <img src="{logo_url}" alt="{name}" style="max-height:40px;margin:8px 0;">'
124
+
125
+ color_css = ''
126
+ if branding['color']:
127
+ safe_color = _sanitize_color(branding['color'])
128
+ color_css = f"""
129
+ <style>
130
+ [class^="AppHeader"] {{ background-color: {safe_color} !important; }}
131
+ </style>"""
132
+
133
+ html = f"""<!doctype html>
134
+<html>
135
+<head>
136
+ <meta charset="utf-8" />
137
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
138
+ <meta name="robots" content="noindex" />
139
+ <title>{name}</title>{color_css}
140
+</head>
141
+<body>{logo_html}
142
+ <script src="{DECAP_CDN}"></script>
143
+</body>
144
+</html>
145
+"""
146
+ with open(os.path.join(admin_dir, 'index.html'), 'w') as f:
147
+ f.write(html)
148
+
149
+
150
+# ---------------------------------------------------------------------------
151
+# config.yml
152
+# ---------------------------------------------------------------------------
153
+
154
+def _write_decap_config(site_dir: str, admin_dir: str, github_repo: str = None):
155
+ content_dir = os.path.join(site_dir, 'content')
156
+ collections = _build_collections(content_dir)
157
+
158
+ backend = {
159
+ 'name': 'github',
160
+ 'branch': 'main',
161
+ }
162
+ if github_repo:
163
+ backend['repo'] = github_repo
164
+ backend['base_url'] = '' # placeholder — set to deployed site URL
165
+ backend['auth_endpoint'] = '/api/auth'
166
+
167
+ config = {
168
+ 'backend': backend,
169
+ 'media_folder': 'static/images/uploads',
170
+ 'public_folder': '/images/uploads',
171
+ 'collections': collections,
172
+ }
173
+
174
+ config_path = os.path.join(admin_dir, 'config.yml')
175
+ with open(config_path, 'w') as f:
176
+ yaml.dump(config, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
177
+
178
+ logging.info(f"Wrote Decap CMS config to {config_path}")
179
+
180
+
181
+def _collect_md_files(dirpath: str) -> list:
182
+ """Recursively collect all .md files under dirpath (excluding _index.md)."""
183
+ found = []
184
+ for root, dirs, files in os.walk(dirpath):
185
+ for f in files:
186
+ if f.endswith('.md') and f != '_index.md':
187
+ found.append(os.path.join(root, f))
188
+ return found
189
+
190
+
191
+def _build_collections(content_dir: str) -> list:
192
+ """
193
+ Inspect content/ to build Decap CMS collections.
194
+ - Subdirs with any .md files (at any depth) → folder collection (e.g. blog)
195
+ - Subdirs with only a top-level _index.md → file collection (e.g. about, contact)
196
+ """
197
+ if not os.path.isdir(content_dir):
198
+ return [_default_pages_collection()]
199
+
200
+ collections = []
201
+
202
+ entries = sorted(os.listdir(content_dir))
203
+ for entry in entries:
204
+ subdir = os.path.join(content_dir, entry)
205
+ if not os.path.isdir(subdir):
206
+ continue
207
+
208
+ # Collect all .md files at any depth (excluding _index.md)
209
+ non_index = _collect_md_files(subdir)
210
+ has_index = os.path.exists(os.path.join(subdir, '_index.md'))
211
+
212
+ if non_index:
213
+ # Folder collection (blog, posts, etc.) — use shallowest sample for field inference
214
+ rel_files = [os.path.relpath(f, subdir) for f in non_index]
215
+ fields = _infer_fields_for_folder(subdir, rel_files)
216
+ collections.append({
217
+ 'name': entry,
218
+ 'label': entry.replace('-', ' ').title(),
219
+ 'folder': f'content/{entry}',
220
+ 'create': True,
221
+ 'slug': '{{slug}}',
222
+ 'fields': fields,
223
+ })
224
+ elif has_index:
225
+ # File collection (single page)
226
+ fields = _infer_fields_for_file(os.path.join(subdir, '_index.md'))
227
+ collections.append({
228
+ 'name': entry,
229
+ 'label': entry.replace('-', ' ').title(),
230
+ 'files': [{
231
+ 'name': entry,
232
+ 'label': entry.replace('-', ' ').title(),
233
+ 'file': f'content/{entry}/_index.md',
234
+ 'fields': fields,
235
+ }],
236
+ })
237
+
238
+ if not collections:
239
+ collections.append(_default_pages_collection())
240
+
241
+ return collections
242
+
243
+
244
+def _infer_fields_for_folder(subdir: str, md_files: list) -> list:
245
+ """Read a sample .md file and extract frontmatter keys as fields."""
246
+ # md_files may be relative paths (from _collect_md_files); resolve to absolute
247
+ first = md_files[0]
248
+ sample = first if os.path.isabs(first) else os.path.join(subdir, first)
249
+ frontmatter = _parse_frontmatter(sample)
250
+
251
+ fields = []
252
+ field_map = {
253
+ 'title': {'label': 'Title', 'name': 'title', 'widget': 'string'},
254
+ 'date': {'label': 'Date', 'name': 'date', 'widget': 'datetime'},
255
+ 'description': {'label': 'Description', 'name': 'description', 'widget': 'text'},
256
+ 'image': {'label': 'Image', 'name': 'image', 'widget': 'image', 'required': False},
257
+ 'categories': {'label': 'Categories', 'name': 'categories', 'widget': 'list',
258
+ 'required': False},
259
+ 'tags': {'label': 'Tags', 'name': 'tags', 'widget': 'list', 'required': False},
260
+ 'draft': {'label': 'Draft', 'name': 'draft', 'widget': 'boolean', 'default': False},
261
+ 'author': {'label': 'Author', 'name': 'author', 'widget': 'string', 'required': False},
262
+ }
263
+
264
+ # Add known fields in a logical order
265
+ for key in ['title', 'date', 'description', 'image', 'categories', 'tags', 'author', 'draft']:
266
+ if key in frontmatter:
267
+ fields.append(field_map[key])
268
+
269
+ # Add any remaining frontmatter keys not in our map
270
+ for key, value in frontmatter.items():
271
+ if key not in field_map and key not in ('type
--- a/hugoifier/utils/decapify.py
+++ b/hugoifier/utils/decapify.py
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/hugoifier/utils/decapify.py
+++ b/hugoifier/utils/decapify.py
@@ -0,0 +1,271 @@
1 """
2 Generates Decap CMS integration for a Hugo site.
3
4 Writes:
5 static/admin/index.html — Decap CMS admin panel
6 static/admin/config.yml — CMS config mapped to actual content structure
7 """
8
9 import logging
10 import os
11 import re
12
13 import yaml
14
15 DECAP_CDN = "https://unpkg.com/decap-cms@^3.0.0/dist/decap-cms.js"
16
17 # Whitelabel defaults — override via decapify() kwargs or env vars
18 DEFAULT_CMS_NAME = os.getenv('CMS_NAME', 'Content Manager')
19 DEFAULT_CMS_LOGO = os.getenv('CMS_LOGO_URL', '') # URL or empty
20 DEFAULT_CMS_COLOR = os.getenv('CMS_COLOR', '#2e3748') # top-bar background
21
22
23 def decapify(
24 site_dir: str,
25 cms_name: str = None,
26 cms_logo: str = None,
27 ) -> str:
28 """r = None,
29 ) -> str:
30 """
31 Add Decap CMS to a Hugo site directory.
32
33 Args:
34 site_dir: Root of the assembled Hugo site (has hugo.toml, content/, thWhitelabel name shown in the admin UI (default: 'Content Manager').
35 cms_logo: er').
36 cms_logo: URL to a logo image for the admin UI (optnal).
37 cms_color:
38 Returns:
39 Status message.
40 """
41 logging.info(f"Adding Decap CMS to {site_dir} ...")
42
43 admin_dir = os.path.join(site_dir, 'static', 'admin')
44 os.makedirs(admin_dir, exist_ok=True)
45
46 branding = {
47 'name': cms_name or DEFAULT_CMS_NAME,
48 'logo': cms_logo or DEFAULT_CMS_LOGO,
49 'color': cms_color or DEFAULT_CMS_COLOR,
50 }
51
52 _write_admin_index(admin_dir, branding)
53 _write_decap_config(site_dir, admin_dir _create_media_dir(site_dir)
54
55 logging.info("Decap CMS integration complete.")
56 return "Decap CMS integration complete"
57
58
59 # ---------------------------------------------------------------------------
60 # Admin index.html
61 # ---------------------------------------------------------------------------
62
63 def _sanitize_color(color: str) -> str:
64 """Allow only valid CSS hex colors (#rgb or #rrggbb) to prevent style injection."""
65 if re.fullmatch(r'#[0-9a-fA-F]{3}(?:[0-9a-fA-F]{3})?', color):
66 return color
67 return '#2e3748' # fall back to default
68
69
70 def _write_admin_index(admin_dir: str, branding: dict):
71 import html as html_mod
72 name = html_mod.escape(branding['name'])
73 logo_html = ''
74 if branding['logo']:
75 logo_url = html_mod.escape(branding['logo'])
76 logo_html = f'\n <img src="{logo_url}" alt="{name}" style="max-height:40px;margin:8px 0;">'
77
78 color_css = ''
79 if branding['color']:
80 safe_color = _sanitize_color(branding['color'])
81 color_css = f"""
82 <style>
83 [class^="AppHeader"] {{ background-color: {safe_color} !important; }}
84 </style>"""
85
86 html = f"""<!doctype html>
87 <html>
88 <head>
89 <meta charset="utf-8" />
90 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
91 <meta name="robots" content="noindex" />
92 <title>{name}</title>{color_css}
93 </head>
94 <body>{logo_html}
95 <script src="{DECAP_CDN}"></script>
96 </body>
97 </html>
98 """
99 with open(os.path.join(admin_dir, 'index.html'), 'w') as f:
100 f.write(html)
101
102
103 # ---------------------------------------------------------------------------
104 # config.yml
105 # ---------------------------------------------------------------------------
106 edia_dir(site_dir)
107
108 logging.info("Decap CMS integration complete.")
109 return gbb) to prevent style injectio{
110 ---------------------gateway',
111 'branch': 'main',
112 ('client_id', clienullmatch(r'#[0-9a-fA-F]{3}(?:[0-9a-fA-F]{3})?', color):
113 return color
114 return '#2e3748' # fall back to default
115
116
117 def _write_admin_index(admin_dir: str, branding: dict):
118 import html as html_mod
119 name = html_mod.escape(branding['name'])
120 logo_html = ''
121 if branding['logo']:
122 logo_url = html_mod.escape(branding['logo'])
123 logo_html = f'\n <img src="{logo_url}" alt="{name}" style="max-height:40px;margin:8px 0;">'
124
125 color_css = ''
126 if branding['color']:
127 safe_color = _sanitize_color(branding['color'])
128 color_css = f"""
129 <style>
130 [class^="AppHeader"] {{ background-color: {safe_color} !important; }}
131 </style>"""
132
133 html = f"""<!doctype html>
134 <html>
135 <head>
136 <meta charset="utf-8" />
137 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
138 <meta name="robots" content="noindex" />
139 <title>{name}</title>{color_css}
140 </head>
141 <body>{logo_html}
142 <script src="{DECAP_CDN}"></script>
143 </body>
144 </html>
145 """
146 with open(os.path.join(admin_dir, 'index.html'), 'w') as f:
147 f.write(html)
148
149
150 # ---------------------------------------------------------------------------
151 # config.yml
152 # ---------------------------------------------------------------------------
153
154 def _write_decap_config(site_dir: str, admin_dir: str, github_repo: str = None):
155 content_dir = os.path.join(site_dir, 'content')
156 collections = _build_collections(content_dir)
157
158 backend = {
159 'name': 'github',
160 'branch': 'main',
161 }
162 if github_repo:
163 backend['repo'] = github_repo
164 backend['base_url'] = '' # placeholder — set to deployed site URL
165 backend['auth_endpoint'] = '/api/auth'
166
167 config = {
168 'backend': backend,
169 'media_folder': 'static/images/uploads',
170 'public_folder': '/images/uploads',
171 'collections': collections,
172 }
173
174 config_path = os.path.join(admin_dir, 'config.yml')
175 with open(config_path, 'w') as f:
176 yaml.dump(config, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
177
178 logging.info(f"Wrote Decap CMS config to {config_path}")
179
180
181 def _collect_md_files(dirpath: str) -> list:
182 """Recursively collect all .md files under dirpath (excluding _index.md)."""
183 found = []
184 for root, dirs, files in os.walk(dirpath):
185 for f in files:
186 if f.endswith('.md') and f != '_index.md':
187 found.append(os.path.join(root, f))
188 return found
189
190
191 def _build_collections(content_dir: str) -> list:
192 """
193 Inspect content/ to build Decap CMS collections.
194 - Subdirs with any .md files (at any depth) → folder collection (e.g. blog)
195 - Subdirs with only a top-level _index.md → file collection (e.g. about, contact)
196 """
197 if not os.path.isdir(content_dir):
198 return [_default_pages_collection()]
199
200 collections = []
201
202 entries = sorted(os.listdir(content_dir))
203 for entry in entries:
204 subdir = os.path.join(content_dir, entry)
205 if not os.path.isdir(subdir):
206 continue
207
208 # Collect all .md files at any depth (excluding _index.md)
209 non_index = _collect_md_files(subdir)
210 has_index = os.path.exists(os.path.join(subdir, '_index.md'))
211
212 if non_index:
213 # Folder collection (blog, posts, etc.) — use shallowest sample for field inference
214 rel_files = [os.path.relpath(f, subdir) for f in non_index]
215 fields = _infer_fields_for_folder(subdir, rel_files)
216 collections.append({
217 'name': entry,
218 'label': entry.replace('-', ' ').title(),
219 'folder': f'content/{entry}',
220 'create': True,
221 'slug': '{{slug}}',
222 'fields': fields,
223 })
224 elif has_index:
225 # File collection (single page)
226 fields = _infer_fields_for_file(os.path.join(subdir, '_index.md'))
227 collections.append({
228 'name': entry,
229 'label': entry.replace('-', ' ').title(),
230 'files': [{
231 'name': entry,
232 'label': entry.replace('-', ' ').title(),
233 'file': f'content/{entry}/_index.md',
234 'fields': fields,
235 }],
236 })
237
238 if not collections:
239 collections.append(_default_pages_collection())
240
241 return collections
242
243
244 def _infer_fields_for_folder(subdir: str, md_files: list) -> list:
245 """Read a sample .md file and extract frontmatter keys as fields."""
246 # md_files may be relative paths (from _collect_md_files); resolve to absolute
247 first = md_files[0]
248 sample = first if os.path.isabs(first) else os.path.join(subdir, first)
249 frontmatter = _parse_frontmatter(sample)
250
251 fields = []
252 field_map = {
253 'title': {'label': 'Title', 'name': 'title', 'widget': 'string'},
254 'date': {'label': 'Date', 'name': 'date', 'widget': 'datetime'},
255 'description': {'label': 'Description', 'name': 'description', 'widget': 'text'},
256 'image': {'label': 'Image', 'name': 'image', 'widget': 'image', 'required': False},
257 'categories': {'label': 'Categories', 'name': 'categories', 'widget': 'list',
258 'required': False},
259 'tags': {'label': 'Tags', 'name': 'tags', 'widget': 'list', 'required': False},
260 'draft': {'label': 'Draft', 'name': 'draft', 'widget': 'boolean', 'default': False},
261 'author': {'label': 'Author', 'name': 'author', 'widget': 'string', 'required': False},
262 }
263
264 # Add known fields in a logical order
265 for key in ['title', 'date', 'description', 'image', 'categories', 'tags', 'author', 'draft']:
266 if key in frontmatter:
267 fields.append(field_map[key])
268
269 # Add any remaining frontmatter keys not in our map
270 for key, value in frontmatter.items():
271 if key not in field_map and key not in ('type
--- a/hugoifier/utils/deploy.py
+++ b/hugoifier/utils/deploy.py
@@ -0,0 +1,28 @@
1
+"""
2
+Handles deployment of a Hugo site, ensuring prerequisites are met
3
+and executing the deployment process (optionally via Cloudflare).
4
+"""
5
+
6
+import logging
7
+
8
+
9
+# Function to handle deployment tasks
10
+def deploy(path, zone):
11
+ logging.info(f"Starting deployment for {path} to zone {zone}...")
12
+ try:
13
+ # Check prerequisites
14
+ logging.info("Checking prerequisites...")
15
+ # Example check for necessary files or configurations
16
+ # if not os.path.exists(os.path.join(path, 'config.toml')):
17
+ # raise FileNotFoundError("Missing config.toml")
18
+
19
+ # Deploy site using Cloudflare functions
20
+ logging.info("Deploying site...")
21
+ # Example API call to deploy the site
22
+ # cloudflare_api.deploy_site(path, zone)
23
+
24
+ logging.info("Deployment complete.")
25
+ return "Deployment complete"
26
+ except Exception as e:
27
+ logging.error(f"Error during deployment: {e}")
28
+ return "Deployment failed"
--- a/hugoifier/utils/deploy.py
+++ b/hugoifier/utils/deploy.py
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/hugoifier/utils/deploy.py
+++ b/hugoifier/utils/deploy.py
@@ -0,0 +1,28 @@
1 """
2 Handles deployment of a Hugo site, ensuring prerequisites are met
3 and executing the deployment process (optionally via Cloudflare).
4 """
5
6 import logging
7
8
9 # Function to handle deployment tasks
10 def deploy(path, zone):
11 logging.info(f"Starting deployment for {path} to zone {zone}...")
12 try:
13 # Check prerequisites
14 logging.info("Checking prerequisites...")
15 # Example check for necessary files or configurations
16 # if not os.path.exists(os.path.join(path, 'config.toml')):
17 # raise FileNotFoundError("Missing config.toml")
18
19 # Deploy site using Cloudflare functions
20 logging.info("Deploying site...")
21 # Example API call to deploy the site
22 # cloudflare_api.deploy_site(path, zone)
23
24 logging.info("Deployment complete.")
25 return "Deployment complete"
26 except Exception as e:
27 logging.error(f"Error during deployment: {e}")
28 return "Deployment failed"
--- a/hugoifier/utils/generate_decap_config.py
+++ b/hugoifier/utils/generate_decap_config.py
@@ -0,0 +1,10 @@
1
+"""
2
+generate_decap_config — thin wrapper kept for backwards compatibility.
3
+The real implementation lives in decapify.py.
4
+"""
5
+
6
+from .decapify import decapify
7
+
8
+
9
+def generate_decap_config(theme_path: str) -> str:
10
+ return decapify(theme_path)
--- a/hugoifier/utils/generate_decap_config.py
+++ b/hugoifier/utils/generate_decap_config.py
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
--- a/hugoifier/utils/generate_decap_config.py
+++ b/hugoifier/utils/generate_decap_config.py
@@ -0,0 +1,10 @@
1 """
2 generate_decap_config — thin wrapper kept for backwards compatibility.
3 The real implementation lives in decapify.py.
4 """
5
6 from .decapify import decapify
7
8
9 def generate_decap_config(theme_path: str) -> str:
10 return decapify(theme_path)
--- a/hugoifier/utils/hugoify.py
+++ b/hugoifier/utils/hugoify.py
@@ -0,0 +1,105 @@
1
+Returns dict mapping relative la, e.g.:
2
+ if rel_path.endswith('.css'):
3
+ "<!DOCTYPE html>..."t_nextjs_sources...import logging
4
+im"""tml_path, 'r)
5
+
6
+ # Extract <body> content
7
+ body_match = re.search(r'<body[^>]*>(.*?)</body>', html, re.DOTALL)
8
+ body_content = body_match.group(1).strip() if body_match else html
9
+
10
+ # Extract body attributes (class, style, etc.)
11
+ body_attrs_match = re.search(r'<body([^>]*)>', html)
12
+
13
+ r'<title>[^<]*</title>',
14
+ '<title>{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} | {{ .Site.Title }}{{ end }}</title>',
15
+ head_content
16
+ )
17
+ baseof = f'''<!DOCTYPE html>
18
+<html lang="{{{{
19
+HTML to convert:
20
+{html}eCode }}}}{{{{ . }}}}{{{{ else{{{{ end }}}}'
21
+
22
+ layouts = {
23
+ "_default/baseof.html": baseof,
24
+ racted {len(layouts)} layout files directly from HTML (no AI)")
25
+ return same format as hugoify_html()
26
+ """
27
+ Convert a Next.js app to a set of Hugo layout files.
28
+
29
+ If dev_u# Build the source context for tsource_block += f"\n{'='*60}\n// FILE: {rel_path}\n{'='*60}\n{content}\n"rn layouts
30
+
31
+
32
+dNext.js React applicationPD,1:)4OR@pK,37zDWi;the HTML shell, <head>, and blocksise falls back to igationAI-powered TSX source conversion.
33
+
34
+ Args:
35
+ info: dict from find_nextjs_app() w}} ... {{{{ end }}}}
36
+- Additional "partials/{{name}}.html" for each major section component
37
+
38
+Conversion rules:
39
+- JSX `className` → the original <head> struct → Hugo partials via `{{{{ partial "name.html" . }}}}`
40
+- `app/layout.tsx` → `eturns dict mapping apping relative la, e.g.:
41
+ if rel_path.endswith('.css'):
42
+ "<!DOCTYPE html>..."t."t_nextjs_sources...imReturns - Animation wrappers (FadeIn, motion.div) → plain `<div>` elements preserving clasace hardcoded <title> with Hugo template
43
+ head_content = re.sub(
44
+ r'<title>[^<]*</title>',
45
+ '<title>{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} | {{ .Site.Title }}{{ end }}</title>',
46
+ head_content
47
+ )
48
+ baseof = f'''<!DOCTYPE html>
49
+<html lang="{{{{ with .Site.LanguageCode }}}}{{{{ . }}}}{{{{ else }}}}en{{{{ end }}}}">
50
+<head>
51
+{head_content}
52
+</head>
53
+<body{" " + body_attrs if body_attrs else ""}>
54
+ {{{{- block "main" . }}}}{{{{- end }}}}
55
+</body>
56
+</html>'''
57
+ else:
58
+ baseof = _fallback_baseof()
59
+
60
+ index_html = f'{{{{ dturns dict mapping relative la, e.g.:
61
+ if rel_path.endswith('.css'):
62
+ "<!DOCTYrelative la, e.g.:
63
+ if rel_path.endswith('.css'):
64
+ "<!DOCTYPE html>..."t_nextjs_sources...import logging
65
+im"""tml_path, 'r', errors='replace') as f:
66
+ html = f.read()
67
+
68
+ logging.info(f"Read {len(html)} chars from {html_path}")
69
+
70
+ # Extract <body> content
71
+ body_match = re.search(r'<body[^>]*>(.*?)</body>', html, re.DOTALL)
72
+ body_content = body_match.group(1).strip() if body_match else html
73
+
74
+ # Extract body attributes (class, style, etc.)
75
+ body_attrs_match = re.search(r'<body([^>]*)>', html)
76
+ body_attrs = body_attrs_match.group(1).strip() if body_attrs_match else ''
77
+
78
+ # Build baseof.html preserving the original <head> structure
79
+ head_match = re.search(r'<head[^>]*>(.*?)</head>', html, re.DOTALL)
80
+ if head_match:
81
+ head_content = head_match.gr."""responseturnng relative la, e.g.:
82
+ )
83
+
84
+ logging.info(f"Read {len(html)} chars from {html_path}")
85
+
86
+ # Extract <body> content
87
+ Returns dict mapping relative la, e.g.:
88
+ if rel_path.endswith('.css'):
89
+ "<!DOCTYPE html>..."t_nextjs_sources...import logging
90
+im"""tml_path, 'r', errors='replace') as f:
91
+ html = f.read()
92
+
93
+ logging.info(f"Read {len(html)} chars from {html_path}")
94
+
95
+ # Extract <body> content
96
+ body_match = re.search(r'<body[^>]*>(.*?)</body>', html, re.DOTALL)
97
+ body_content = body_match.group(1).strip() if body_match else html
98
+
99
+ # Extract body attributes (class, style, etc.)
100
+ body_attrs_match = re.search(r'<body([^>]*)>', html)
101
+ body_attrs = body_attrs_match.group(1).strip() if body_attrs_match else ''
102
+
103
+ # Build baseof.html preserving the original <head> structure
104
+ head_match = re.search(r'<head[^>]*>(.*?)</head>', html, re.DOTALL)
105
+ if head_mat
--- a/hugoifier/utils/hugoify.py
+++ b/hugoifier/utils/hugoify.py
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/hugoifier/utils/hugoify.py
+++ b/hugoifier/utils/hugoify.py
@@ -0,0 +1,105 @@
1 Returns dict mapping relative la, e.g.:
2 if rel_path.endswith('.css'):
3 "<!DOCTYPE html>..."t_nextjs_sources...import logging
4 im"""tml_path, 'r)
5
6 # Extract <body> content
7 body_match = re.search(r'<body[^>]*>(.*?)</body>', html, re.DOTALL)
8 body_content = body_match.group(1).strip() if body_match else html
9
10 # Extract body attributes (class, style, etc.)
11 body_attrs_match = re.search(r'<body([^>]*)>', html)
12
13 r'<title>[^<]*</title>',
14 '<title>{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} | {{ .Site.Title }}{{ end }}</title>',
15 head_content
16 )
17 baseof = f'''<!DOCTYPE html>
18 <html lang="{{{{
19 HTML to convert:
20 {html}eCode }}}}{{{{ . }}}}{{{{ else{{{{ end }}}}'
21
22 layouts = {
23 "_default/baseof.html": baseof,
24 racted {len(layouts)} layout files directly from HTML (no AI)")
25 return same format as hugoify_html()
26 """
27 Convert a Next.js app to a set of Hugo layout files.
28
29 If dev_u# Build the source context for tsource_block += f"\n{'='*60}\n// FILE: {rel_path}\n{'='*60}\n{content}\n"rn layouts
30
31
32 dNext.js React applicationPD,1:)4OR@pK,37zDWi;the HTML shell, <head>, and blocksise falls back to igationAI-powered TSX source conversion.
33
34 Args:
35 info: dict from find_nextjs_app() w}} ... {{{{ end }}}}
36 - Additional "partials/{{name}}.html" for each major section component
37
38 Conversion rules:
39 - JSX `className` → the original <head> struct → Hugo partials via `{{{{ partial "name.html" . }}}}`
40 - `app/layout.tsx` → `eturns dict mapping apping relative la, e.g.:
41 if rel_path.endswith('.css'):
42 "<!DOCTYPE html>..."t."t_nextjs_sources...imReturns - Animation wrappers (FadeIn, motion.div) → plain `<div>` elements preserving clasace hardcoded <title> with Hugo template
43 head_content = re.sub(
44 r'<title>[^<]*</title>',
45 '<title>{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} | {{ .Site.Title }}{{ end }}</title>',
46 head_content
47 )
48 baseof = f'''<!DOCTYPE html>
49 <html lang="{{{{ with .Site.LanguageCode }}}}{{{{ . }}}}{{{{ else }}}}en{{{{ end }}}}">
50 <head>
51 {head_content}
52 </head>
53 <body{" " + body_attrs if body_attrs else ""}>
54 {{{{- block "main" . }}}}{{{{- end }}}}
55 </body>
56 </html>'''
57 else:
58 baseof = _fallback_baseof()
59
60 index_html = f'{{{{ dturns dict mapping relative la, e.g.:
61 if rel_path.endswith('.css'):
62 "<!DOCTYrelative la, e.g.:
63 if rel_path.endswith('.css'):
64 "<!DOCTYPE html>..."t_nextjs_sources...import logging
65 im"""tml_path, 'r', errors='replace') as f:
66 html = f.read()
67
68 logging.info(f"Read {len(html)} chars from {html_path}")
69
70 # Extract <body> content
71 body_match = re.search(r'<body[^>]*>(.*?)</body>', html, re.DOTALL)
72 body_content = body_match.group(1).strip() if body_match else html
73
74 # Extract body attributes (class, style, etc.)
75 body_attrs_match = re.search(r'<body([^>]*)>', html)
76 body_attrs = body_attrs_match.group(1).strip() if body_attrs_match else ''
77
78 # Build baseof.html preserving the original <head> structure
79 head_match = re.search(r'<head[^>]*>(.*?)</head>', html, re.DOTALL)
80 if head_match:
81 head_content = head_match.gr."""responseturnng relative la, e.g.:
82 )
83
84 logging.info(f"Read {len(html)} chars from {html_path}")
85
86 # Extract <body> content
87 Returns dict mapping relative la, e.g.:
88 if rel_path.endswith('.css'):
89 "<!DOCTYPE html>..."t_nextjs_sources...import logging
90 im"""tml_path, 'r', errors='replace') as f:
91 html = f.read()
92
93 logging.info(f"Read {len(html)} chars from {html_path}")
94
95 # Extract <body> content
96 body_match = re.search(r'<body[^>]*>(.*?)</body>', html, re.DOTALL)
97 body_content = body_match.group(1).strip() if body_match else html
98
99 # Extract body attributes (class, style, etc.)
100 body_attrs_match = re.search(r'<body([^>]*)>', html)
101 body_attrs = body_attrs_match.group(1).strip() if body_attrs_match else ''
102
103 # Build baseof.html preserving the original <head> structure
104 head_match = re.search(r'<head[^>]*>(.*?)</head>', html, re.DOTALL)
105 if head_mat
--- a/hugoifier/utils/parser.py
+++ b/hugoifier/utils/parser.py
@@ -0,0 +1,32 @@
1
+"""
2
+Performs parsing, linting, and validation of web content to ensure it adheres
3
+to best practices and standards. Checks for syntax errors and structural issues.
4
+"""
5
+
6
+import logging
7
+
8
+
9
+# Function to perform parsing, linting, and validation
10
+def parse(path):
11
+ logging.info(f"Starting parsing and linting for {path}...")
12
+ try:
13
+ # Parse input
14
+ logging.info("Parsing input...")
15
+ # Example parsing logic
16
+ # parse_html_structure(path)
17
+
18
+ # Lint code
19
+ logging.info("Linting code...")
20
+ # Example linting logic
21
+ # lint_html_css_js(path)
22
+
23
+ # Validate structure
24
+ logging.info("Validating structure...")
25
+ # Example validation logic
26
+ # validate_html_structure(path)
27
+
28
+ logging.info("Parsing complete.")
29
+ return "Parsing complete"
30
+ except Exception as e:
31
+ logging.error(f"Error during parsing: {e}")
32
+ return "Parsing failed"
--- a/hugoifier/utils/parser.py
+++ b/hugoifier/utils/parser.py
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/hugoifier/utils/parser.py
+++ b/hugoifier/utils/parser.py
@@ -0,0 +1,32 @@
1 """
2 Performs parsing, linting, and validation of web content to ensure it adheres
3 to best practices and standards. Checks for syntax errors and structural issues.
4 """
5
6 import logging
7
8
9 # Function to perform parsing, linting, and validation
10 def parse(path):
11 logging.info(f"Starting parsing and linting for {path}...")
12 try:
13 # Parse input
14 logging.info("Parsing input...")
15 # Example parsing logic
16 # parse_html_structure(path)
17
18 # Lint code
19 logging.info("Linting code...")
20 # Example linting logic
21 # lint_html_css_js(path)
22
23 # Validate structure
24 logging.info("Validating structure...")
25 # Example validation logic
26 # validate_html_structure(path)
27
28 logging.info("Parsing complete.")
29 return "Parsing complete"
30 except Exception as e:
31 logging.error(f"Error during parsing: {e}")
32 return "Parsing failed"
--- a/hugoifier/utils/theme_finder.py
+++ b/hugoifier/utils/theme_finder.py
@@ -0,0 +1,74 @@
1
+"""
2
+Locates the actual Hugo theme and exampleSite within the messy zip-extracted structure.
3
+Themes in themes/ are structured as: {name}/{name}/themes/{theme-name}/
4
+"""
5
+
6
+import }/
7
+"""
8
+
9
+import json
10
+import logging
11
+import os
12
+
13
+
14
+def find_hugo_theme(input_path):
15
+ """
16
+ Given a path like themes/revolve-hugo or the inner extracted dir,
17
+ find the Hugo theme directory (has layouts/), the exampleSite, and the theme name.
18
+
19
+ Returns dict with:
20
+ theme_dir: path to the Hugo theme (has layouts/, archetypes/, etc.)
21
+ example_site: path to exampleSite dir (may be None)
22
+ theme_name: name of the theme (used in hugo.toml)
23
+ is_hugo_theme: True if input is already a Hugo theme
24
+ """
25
+ input_path = os.path.abspath(input_path)
26
+
27
+ # Walk up to find the theme dir containing layouts/
28
+ candidates = []
29
+ for root, dirs, files in os.walk(input_path):
30
+ # Skip __MACOSX junk
31
+ if '__MACOSX' in root:
32
+ continue
33
+ if 'layouts' in dirs and '_default' in os.listdir(os.path.join(root, 'layouts')):
34
+ candidates.append(root)
35
+
36
+ if not candidates:
37
+ return None
38
+
39
+ # Pick the deepest match (most likely the actual theme dir)
40
+ theme_dir = max(candidates, key=lambda p: p.count(os.sep))
41
+ if len(candidates) > 1:
42
+ logging.warning(
43
+ f"Multiple Hugo theme candidates found; using {theme_dir!r}. "
44
+ f"Others: {[c for c in candidates if c != theme_dir]}"
45
+ )
46
+
47
+ # Detect exampleSite
48
+ example_site = None
49
+ for candidate in [
50
+ os.path.join(theme_dir, 'exampleSite'),
51
+ os.path.join(os.path.dirname(theme_dir), 'exampleSite'),
52
+ ]:
53
+ if os.path.isdir(candidate):
54
+ example_site = candidate
55
+ break
56
+
57
+ theme_name = os.path.basename(theme_dir)
58
+
59
+ return {
60
+ 'theme_dir': theme_dir,
61
+ 'example_site': example_site,
62
+ 'theme_name': theme_name,
63
+ 'is_hugo_them return None
64
+
65
+
66
+def find_raw_html_files(input_path):
67
+ """Find HTML files in a raw HTML theme (not a Hugo theme)."""
68
+ html_files = []
69
+ for root, dirs, files in os.walk(input_path):
70
+ if '__MACOSX' in root:
71
+ continue
72
+ for f in files:
73
+ if f.endswith('.html') and 'exampleSite' not in root:
74
+ html_files.append(os.path.join(root,
--- a/hugoifier/utils/theme_finder.py
+++ b/hugoifier/utils/theme_finder.py
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/hugoifier/utils/theme_finder.py
+++ b/hugoifier/utils/theme_finder.py
@@ -0,0 +1,74 @@
1 """
2 Locates the actual Hugo theme and exampleSite within the messy zip-extracted structure.
3 Themes in themes/ are structured as: {name}/{name}/themes/{theme-name}/
4 """
5
6 import }/
7 """
8
9 import json
10 import logging
11 import os
12
13
14 def find_hugo_theme(input_path):
15 """
16 Given a path like themes/revolve-hugo or the inner extracted dir,
17 find the Hugo theme directory (has layouts/), the exampleSite, and the theme name.
18
19 Returns dict with:
20 theme_dir: path to the Hugo theme (has layouts/, archetypes/, etc.)
21 example_site: path to exampleSite dir (may be None)
22 theme_name: name of the theme (used in hugo.toml)
23 is_hugo_theme: True if input is already a Hugo theme
24 """
25 input_path = os.path.abspath(input_path)
26
27 # Walk up to find the theme dir containing layouts/
28 candidates = []
29 for root, dirs, files in os.walk(input_path):
30 # Skip __MACOSX junk
31 if '__MACOSX' in root:
32 continue
33 if 'layouts' in dirs and '_default' in os.listdir(os.path.join(root, 'layouts')):
34 candidates.append(root)
35
36 if not candidates:
37 return None
38
39 # Pick the deepest match (most likely the actual theme dir)
40 theme_dir = max(candidates, key=lambda p: p.count(os.sep))
41 if len(candidates) > 1:
42 logging.warning(
43 f"Multiple Hugo theme candidates found; using {theme_dir!r}. "
44 f"Others: {[c for c in candidates if c != theme_dir]}"
45 )
46
47 # Detect exampleSite
48 example_site = None
49 for candidate in [
50 os.path.join(theme_dir, 'exampleSite'),
51 os.path.join(os.path.dirname(theme_dir), 'exampleSite'),
52 ]:
53 if os.path.isdir(candidate):
54 example_site = candidate
55 break
56
57 theme_name = os.path.basename(theme_dir)
58
59 return {
60 'theme_dir': theme_dir,
61 'example_site': example_site,
62 'theme_name': theme_name,
63 'is_hugo_them return None
64
65
66 def find_raw_html_files(input_path):
67 """Find HTML files in a raw HTML theme (not a Hugo theme)."""
68 html_files = []
69 for root, dirs, files in os.walk(input_path):
70 if '__MACOSX' in root:
71 continue
72 for f in files:
73 if f.endswith('.html') and 'exampleSite' not in root:
74 html_files.append(os.path.join(root,
--- a/hugoifier/utils/theme_patcher.py
+++ b/hugoifier/utils/theme_patcher.py
@@ -0,0 +1,72 @@
1
+"""
2
+Patches common Hugo deprecations in theme layout files so they work with Hugo >= v0.128.
3
+
4
+Call patch_theme(theme_dir) after copying theme files to the output directory.
5
+"""
6
+
7
+import logging
8
+import os
9
+import re
10
+
11
+# Map of (pattern, replacement) for deprecated Hugo template variables/functions
12
+TEMPLATE_PATCHES = [
13
+ # .Site.DisqusShortname → .Site.Config.Services.Disqus.Shortname
14
+ (r'\.Site\.DisqusShortname', '.Site.Config.Services.Disqus.Shortname'),
15
+ # .Site.GoogleAnalytics → .Site.Config.Services.GoogleAnalytics.ID
16
+ (r'\.Site\.GoogleAnalytics\b', '.Site.Config.Services.GoogleAnalytics.ID'),
17
+ # absLangURL → absLangURL still works but absURL is preferred for simple cases
18
+ # safeHTMLAttr is fine, no change needed
19
+]
20
+
21
+# Config key patches: (old_pattern, replacement)
22
+CONFIG_PATCHES = [
23
+ # paginate → [pagination] pagerSize
24
+ (r'^paginate\s*=\s*(\d+)$', r'[pagination]\n pagerSize = \1'),
25
+ # googleAnalytics = "UA-xxx" → [services.googleAnalytics] id = "UA-xxx"
26
+ (r'^googleAnalytics\s*=\s*"([^"]+)"', r'[services.googleAnalytics]\n id = "\1"'),
27
+ # disqusShortname = "xxx" → [services.disqus] shortname = "xxx"
28
+ (r'^disqusShortname\s*=\s*"([^"]+)"', r'[services.disqus]\n shortname = "\1"'),
29
+]
30
+
31
+
32
+def patch_theme(theme_dir: str):
33
+ """Patch deprecated Hugo APIs in all layout files under theme_dir/layouts/."""
34
+ layouts_dir = os.path.join(theme_dir, 'layouts')
35
+ if not os.path.isdir(layouts_dir):
36
+ return
37
+
38
+ patched = 0
39
+ for root, dirs, files in os.walk(layouts_dir):
40
+ for fname in files:
41
+ if not fname.endswith('.html'):
42
+ continue
43
+ path = os.path.join(root, fname)
44
+ with open(path, 'r', errors='replace') as f:
45
+ content = f.read()
46
+
47
+ new_content = content
48
+ for pattern, replacement in TEMPLATE_PATCHES:
49
+ new_content = re.sub(pattern, replacement, new_content)
50
+
51
+ if new_content != content:
52
+ with open(path, 'w') as f:
53
+ f.write(new_content)
54
+ patched += 1
55
+
56
+ if patched:
57
+ logging.info(f"Patched {patched} template file(s) in {theme_dir}")
58
+
59
+
60
+def patch_config(config_path: str):
61
+ """Patch deprecated keys in a hugo.toml / config.toml file."""
62
+ with open(config_path, 'r') as f:
63
+ content = f.read()
64
+
65
+ new_content = content
66
+ for pattern, replacement in CONFIG_PATCHES:
67
+ new_content = re.sub(pattern, replacement, new_content, flags=re.MULTILINE)
68
+
69
+ if new_content != content:
70
+ with open(config_path, 'w') as f:
71
+ f.write(new_content)
72
+ logging.info(f"Patched deprecated config keys in {config_path}")
--- a/hugoifier/utils/theme_patcher.py
+++ b/hugoifier/utils/theme_patcher.py
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/hugoifier/utils/theme_patcher.py
+++ b/hugoifier/utils/theme_patcher.py
@@ -0,0 +1,72 @@
1 """
2 Patches common Hugo deprecations in theme layout files so they work with Hugo >= v0.128.
3
4 Call patch_theme(theme_dir) after copying theme files to the output directory.
5 """
6
7 import logging
8 import os
9 import re
10
11 # Map of (pattern, replacement) for deprecated Hugo template variables/functions
12 TEMPLATE_PATCHES = [
13 # .Site.DisqusShortname → .Site.Config.Services.Disqus.Shortname
14 (r'\.Site\.DisqusShortname', '.Site.Config.Services.Disqus.Shortname'),
15 # .Site.GoogleAnalytics → .Site.Config.Services.GoogleAnalytics.ID
16 (r'\.Site\.GoogleAnalytics\b', '.Site.Config.Services.GoogleAnalytics.ID'),
17 # absLangURL → absLangURL still works but absURL is preferred for simple cases
18 # safeHTMLAttr is fine, no change needed
19 ]
20
21 # Config key patches: (old_pattern, replacement)
22 CONFIG_PATCHES = [
23 # paginate → [pagination] pagerSize
24 (r'^paginate\s*=\s*(\d+)$', r'[pagination]\n pagerSize = \1'),
25 # googleAnalytics = "UA-xxx" → [services.googleAnalytics] id = "UA-xxx"
26 (r'^googleAnalytics\s*=\s*"([^"]+)"', r'[services.googleAnalytics]\n id = "\1"'),
27 # disqusShortname = "xxx" → [services.disqus] shortname = "xxx"
28 (r'^disqusShortname\s*=\s*"([^"]+)"', r'[services.disqus]\n shortname = "\1"'),
29 ]
30
31
32 def patch_theme(theme_dir: str):
33 """Patch deprecated Hugo APIs in all layout files under theme_dir/layouts/."""
34 layouts_dir = os.path.join(theme_dir, 'layouts')
35 if not os.path.isdir(layouts_dir):
36 return
37
38 patched = 0
39 for root, dirs, files in os.walk(layouts_dir):
40 for fname in files:
41 if not fname.endswith('.html'):
42 continue
43 path = os.path.join(root, fname)
44 with open(path, 'r', errors='replace') as f:
45 content = f.read()
46
47 new_content = content
48 for pattern, replacement in TEMPLATE_PATCHES:
49 new_content = re.sub(pattern, replacement, new_content)
50
51 if new_content != content:
52 with open(path, 'w') as f:
53 f.write(new_content)
54 patched += 1
55
56 if patched:
57 logging.info(f"Patched {patched} template file(s) in {theme_dir}")
58
59
60 def patch_config(config_path: str):
61 """Patch deprecated keys in a hugo.toml / config.toml file."""
62 with open(config_path, 'r') as f:
63 content = f.read()
64
65 new_content = content
66 for pattern, replacement in CONFIG_PATCHES:
67 new_content = re.sub(pattern, replacement, new_content, flags=re.MULTILINE)
68
69 if new_content != content:
70 with open(config_path, 'w') as f:
71 f.write(new_content)
72 logging.info(f"Patched deprecated config keys in {config_path}")
--- a/hugoifier/utils/translate.py
+++ b/hugoifier/utils/translate.py
@@ -0,0 +1,25 @@
1
+"""
2
+Translates web content using the configured AI backend.
3
+"""
4
+
5
+import logging
6
+
7
+from ..config import call_ai
8
+
9
+
10
+def translate(path: str, target_language: str = "Spanish") -> str:
11
+ logging.info(f"Translating content in {path} ...")
12
+ try:
13
+ with open(path, 'r', errors='replace') as f:
14
+ content = f.read()
15
+
16
+ prompt = f"""Translate the following web content to {target_language}.
17
+Preserve all HTML tags and formatting. Only translate visible text.
18
+
19
+Content:
20
+{content[:20000]}"""
21
+
22
+ return call_ai(prompt, "You are a professional translator.")
23
+ except Exception as e:
24
+ logging.error(f"Translation failed: {e}")
25
+ return f"Translation failed: {e}"
--- a/hugoifier/utils/translate.py
+++ b/hugoifier/utils/translate.py
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/hugoifier/utils/translate.py
+++ b/hugoifier/utils/translate.py
@@ -0,0 +1,25 @@
1 """
2 Translates web content using the configured AI backend.
3 """
4
5 import logging
6
7 from ..config import call_ai
8
9
10 def translate(path: str, target_language: str = "Spanish") -> str:
11 logging.info(f"Translating content in {path} ...")
12 try:
13 with open(path, 'r', errors='replace') as f:
14 content = f.read()
15
16 prompt = f"""Translate the following web content to {target_language}.
17 Preserve all HTML tags and formatting. Only translate visible text.
18
19 Content:
20 {content[:20000]}"""
21
22 return call_ai(prompt, "You are a professional translator.")
23 except Exception as e:
24 logging.error(f"Translation failed: {e}")
25 return f"Translation failed: {e}"
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -0,0 +1,73 @@
1
+[build-system]
2
+requires = ["setuptools>=69.0", "wheel"]
3
+build-backend = "setuptools.build_meta"
4
+
5
+[project]
6
+name = "hugoifier"
7
+version = "0.1.0"
8
+description = "AI-powered Hugo theme converter with Decap CMS integration"
9
+readme = "README.md"
10
+license = "MIT"
11
+requires-python = ">=3.11"
12
+authors = [
13
+ { name = "CONFLICT LLC" },
14
+]
15
+keywords = ["hugo", "cms", "decap", "static-site", "theme", "ai", "converter"]
16
+classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Internet :: WWW/HTTP :: Site Management",
25
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
26
+]
27
+
28
+dependencies = [
29
+ "anthropic>=0.5.0",
30
+ "openai>=1.0.0",
31
+ "google-generativeai>=0.3.0",
32
+ "pyyaml>=6.0",
33
+]
34
+
35
+[project.optional-dependencies]
36
+dev = [
37
+ "pytest>=7.3.0",
38
+ "pytest-cov>=4.1.0",
39
+ "ruff>=0.1.0",
40
+ "build>=1.0.0",
41
+ "twine>=4.0.0",
42
+]
43
+
44
+[project.urls]
45
+Homepage = "https://hugoifier.dev"
46
+Documentation = "https://hugoifier.dev"
47
+Repository = "https://github.com/ConflictHQ/hugoifier"
48
+Issues = "https://github.com/ConflictHQ/hugoifier/issues"
49
+
50
+[project.scripts]
51
+hugoifier = "hugoifier.cli:main"
52
+
53
+[tool.setuptools.packages.find]
54
+include = ["hugoifier*"]
55
+
56
+[tool.ruff]
57
+line-length = 100
58
+target-version = "py311"
59
+
60
+[tool.ruff.lint]
61
+select = ["E", "F", "W", "I"]
62
+
63
+[tool.ruff.lint.per-file-ignores]
64
+"hugoifier/cli.py" = ["E501"]
65
+"hugoifier/utitests/*" = ["E501"]
66
+
67
+[tool.pytest.ini_options]
68
+testpaths = ["tests"]
69
+python_files = "test_*.py"
70
+
71
+[tool.mypy]
72
+python_version = "3.11"
73
+warn_return_any = true
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -0,0 +1,73 @@
1 [build-system]
2 requires = ["setuptools>=69.0", "wheel"]
3 build-backend = "setuptools.build_meta"
4
5 [project]
6 name = "hugoifier"
7 version = "0.1.0"
8 description = "AI-powered Hugo theme converter with Decap CMS integration"
9 readme = "README.md"
10 license = "MIT"
11 requires-python = ">=3.11"
12 authors = [
13 { name = "CONFLICT LLC" },
14 ]
15 keywords = ["hugo", "cms", "decap", "static-site", "theme", "ai", "converter"]
16 classifiers = [
17 "Development Status :: 3 - Alpha",
18 "Intended Audience :: Developers",
19 "Operating System :: OS Independent",
20 "Programming Language :: Python :: 3",
21 "Programming Language :: Python :: 3.11",
22 "Programming Language :: Python :: 3.12",
23 "Programming Language :: Python :: 3.13",
24 "Topic :: Internet :: WWW/HTTP :: Site Management",
25 "Topic :: Scientific/Engineering :: Artificial Intelligence",
26 ]
27
28 dependencies = [
29 "anthropic>=0.5.0",
30 "openai>=1.0.0",
31 "google-generativeai>=0.3.0",
32 "pyyaml>=6.0",
33 ]
34
35 [project.optional-dependencies]
36 dev = [
37 "pytest>=7.3.0",
38 "pytest-cov>=4.1.0",
39 "ruff>=0.1.0",
40 "build>=1.0.0",
41 "twine>=4.0.0",
42 ]
43
44 [project.urls]
45 Homepage = "https://hugoifier.dev"
46 Documentation = "https://hugoifier.dev"
47 Repository = "https://github.com/ConflictHQ/hugoifier"
48 Issues = "https://github.com/ConflictHQ/hugoifier/issues"
49
50 [project.scripts]
51 hugoifier = "hugoifier.cli:main"
52
53 [tool.setuptools.packages.find]
54 include = ["hugoifier*"]
55
56 [tool.ruff]
57 line-length = 100
58 target-version = "py311"
59
60 [tool.ruff.lint]
61 select = ["E", "F", "W", "I"]
62
63 [tool.ruff.lint.per-file-ignores]
64 "hugoifier/cli.py" = ["E501"]
65 "hugoifier/utitests/*" = ["E501"]
66
67 [tool.pytest.ini_options]
68 testpaths = ["tests"]
69 python_files = "test_*.py"
70
71 [tool.mypy]
72 python_version = "3.11"
73 warn_return_any = true
+5
--- a/setup.py
+++ b/setup.py
@@ -0,0 +1,5 @@
1
+"""Backwards-compatible setup.py — all config lives in pyproject.toml."""
2
+
3
+from setuptools import setup
4
+
5
+setup()
--- a/setup.py
+++ b/setup.py
@@ -0,0 +1,5 @@
 
 
 
 
 
--- a/setup.py
+++ b/setup.py
@@ -0,0 +1,5 @@
1 """Backwards-compatible setup.py — all config lives in pyproject.toml."""
2
3 from setuptools import setup
4
5 setup()
D src/cli.py
-131
--- a/src/cli.py
+++ b/src/cli.py
@@ -1,131 +0,0 @@
1
-"""
2
-Hugo-ifier CLI Tool
3
-
4
-Usage examples:
5
- python cli.py complete themes/revolve-hugo
6
- python cli.py complete themes/revolve-hugo --output /tmp/my-site
7
- HUGOIFIER_BACKEND=openai python cli.py complete themes/revolve-hugo
8
- python cli.py analyze themes/revolve-hugo
9
- python cli.py hugoify themes/revolve-hugo
10
- python cli.py decapify output/revolve-hugo
11
-"""
12
-
13
-import argparse
14
-import logging
15
-import sys
16
-import os
17
-
18
-# Ensure src/ is on the path when called directly
19
-sys.path.insert(0, os.path.dirname(__file__))
20
-
21
-from utils.analyze import analyze
22
-from utils.complete import complete
23
-from utils.cloudflare import configure_cloudflare
24
-from utils.deploy import deploy
25
-from utils.hugoify import hugoify
26
-from utils.decapify import decapify
27
-from utils.translate import translate
28
-from utils.parser import parse
29
-
30
-
31
-def main():
32
- parser = argparse.ArgumentParser(
33
- description="Hugo-ifier — AI-powered Hugo theme converter",
34
- formatter_class=argparse.RawDescriptionHelpFormatter,
35
- epilog=__doc__,
36
- )
37
- parser.add_argument(
38
- '--backend', choices=['anthropic', 'openai', 'google'],
39
- help='AI backend to use (overrides HUGOIFIER_BACKEND env var)',
40
- )
41
- subparsers = parser.add_subparsers(dest="command")
42
-
43
- # complete — full pipeline
44
- complete_parser = subparsers.add_parser("complete", help="Run the full pipeline (analyze → hugoify → decap)")
45
- complete_parser.add_argument("path", help="Path to the theme directory")
46
- complete_parser.add_argument("--output", "-o", help="Output directory (default: output/{theme-name})")
47
- complete_parser.add_argument("--cms-name", default=None, help="Whitelabel CMS name")
48
- complete_parser.add_argument("--cms-logo", default=None, help="Whitelabel logo URL")
49
- complete_parser.add_argument("--cms-color", default=None, help="Whitelabel top-bar hex color")
50
-
51
- # analyze
52
- analyze_parser = subparsers.add_parser("analyze", help="Analyze a theme and report structure")
53
- analyze_parser.add_argument("path", help="Path to the theme")
54
-
55
- # hugoify
56
- hugoify_parser = subparsers.add_parser("hugoify", help="Convert HTML to Hugo theme (or validate existing Hugo theme)")
57
- hugoify_parser.add_argument("path", help="Path to HTML file or theme directory")
58
-
59
- # decapify
60
- decapify_parser = subparsers.add_parser("decapify", help="Add Decap CMS to an assembled Hugo site")
61
- decapify_parser.add_argument("path", help="Path to the Hugo site directory")
62
- decapify_parser.add_argument("--cms-name", default=None, help="Whitelabel CMS name (default: 'Content Manager')")
63
- decapify_parser.add_argument("--cms-logo", default=None, help="Whitelabel logo URL")
64
- decapify_parser.add_argument("--cms-color", default=None, help="Whitelabel top-bar hex color")
65
-
66
- # translate
67
- translate_parser = subparsers.add_parser("translate", help="Translate content to another language")
68
- translate_parser.add_argument("path", help="Path to the content file")
69
- translate_parser.add_argument("--target-language", default="Spanish", help="Target language (default: Spanish)")
70
-
71
- # parse / lint
72
- parser_parser = subparsers.add_parser("parser", help="Parse and lint (stub)")
73
- parser_parser.add_argument("path", help="Path to the theme")
74
-
75
- # deploy (stub)
76
- deploy_parser = subparsers.add_parser("deploy", help="Deploy to Cloudflare (stub)")
77
- deploy_parser.add_argument("path", help="Path to the site")
78
- deploy_parser.add_argument("zone", help="Cloudflare zone")
79
-
80
- # cloudflare (stub)
81
- cloudflare_parser = subparsers.add_parser("cloudflare", help="Configure Cloudflare (stub)")
82
- cloudflare_parser.add_argument("path", help="Path to the site")
83
- cloudflare_parser.add_argument("zone", help="Cloudflare zone")
84
-
85
- args = parser.parse_args()
86
-
87
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
88
-
89
- # Override backend if specified on command line
90
- if args.backend:
91
- import config as cfg
92
- cfg.BACKEND = args.backend
93
-
94
- try:
95
- if args.command == "complete":
96
- result = complete(
97
- args.path,
98
- output_dir=args.output,
99
- cms_name=args.cms_name,
100
- cms_logo=args.cms_logo,
101
- cms_color=args.cms_color,
102
- )
103
- print(result)
104
- elif args.command == "analyze":
105
- print(analyze(args.path))
106
- elif args.command == "hugoify":
107
- print(hugoify(args.path))
108
- elif args.command == "decapify":
109
- print(decapify(
110
- args.path,
111
- cms_name=args.cms_name,
112
- cms_logo=args.cms_logo,
113
- cms_color=args.cms_color,
114
- ))
115
- elif args.command == "translate":
116
- print(translate(args.path, target_language=args.target_language))
117
- elif args.command == "parser":
118
- print(parse(args.path))
119
- elif args.command == "deploy":
120
- print(deploy(args.path, args.zone))
121
- elif args.command == "cloudflare":
122
- print(configure_cloudflare(args.path, args.zone))
123
- else:
124
- parser.print_help()
125
- except (ValueError, EnvironmentError) as e:
126
- print(f"Error: {e}", file=sys.stderr)
127
- sys.exit(1)
128
-
129
-
130
-if __name__ == "__main__":
131
- main()
--- a/src/cli.py
+++ b/src/cli.py
@@ -1,131 +0,0 @@
1 """
2 Hugo-ifier CLI Tool
3
4 Usage examples:
5 python cli.py complete themes/revolve-hugo
6 python cli.py complete themes/revolve-hugo --output /tmp/my-site
7 HUGOIFIER_BACKEND=openai python cli.py complete themes/revolve-hugo
8 python cli.py analyze themes/revolve-hugo
9 python cli.py hugoify themes/revolve-hugo
10 python cli.py decapify output/revolve-hugo
11 """
12
13 import argparse
14 import logging
15 import sys
16 import os
17
18 # Ensure src/ is on the path when called directly
19 sys.path.insert(0, os.path.dirname(__file__))
20
21 from utils.analyze import analyze
22 from utils.complete import complete
23 from utils.cloudflare import configure_cloudflare
24 from utils.deploy import deploy
25 from utils.hugoify import hugoify
26 from utils.decapify import decapify
27 from utils.translate import translate
28 from utils.parser import parse
29
30
31 def main():
32 parser = argparse.ArgumentParser(
33 description="Hugo-ifier — AI-powered Hugo theme converter",
34 formatter_class=argparse.RawDescriptionHelpFormatter,
35 epilog=__doc__,
36 )
37 parser.add_argument(
38 '--backend', choices=['anthropic', 'openai', 'google'],
39 help='AI backend to use (overrides HUGOIFIER_BACKEND env var)',
40 )
41 subparsers = parser.add_subparsers(dest="command")
42
43 # complete — full pipeline
44 complete_parser = subparsers.add_parser("complete", help="Run the full pipeline (analyze → hugoify → decap)")
45 complete_parser.add_argument("path", help="Path to the theme directory")
46 complete_parser.add_argument("--output", "-o", help="Output directory (default: output/{theme-name})")
47 complete_parser.add_argument("--cms-name", default=None, help="Whitelabel CMS name")
48 complete_parser.add_argument("--cms-logo", default=None, help="Whitelabel logo URL")
49 complete_parser.add_argument("--cms-color", default=None, help="Whitelabel top-bar hex color")
50
51 # analyze
52 analyze_parser = subparsers.add_parser("analyze", help="Analyze a theme and report structure")
53 analyze_parser.add_argument("path", help="Path to the theme")
54
55 # hugoify
56 hugoify_parser = subparsers.add_parser("hugoify", help="Convert HTML to Hugo theme (or validate existing Hugo theme)")
57 hugoify_parser.add_argument("path", help="Path to HTML file or theme directory")
58
59 # decapify
60 decapify_parser = subparsers.add_parser("decapify", help="Add Decap CMS to an assembled Hugo site")
61 decapify_parser.add_argument("path", help="Path to the Hugo site directory")
62 decapify_parser.add_argument("--cms-name", default=None, help="Whitelabel CMS name (default: 'Content Manager')")
63 decapify_parser.add_argument("--cms-logo", default=None, help="Whitelabel logo URL")
64 decapify_parser.add_argument("--cms-color", default=None, help="Whitelabel top-bar hex color")
65
66 # translate
67 translate_parser = subparsers.add_parser("translate", help="Translate content to another language")
68 translate_parser.add_argument("path", help="Path to the content file")
69 translate_parser.add_argument("--target-language", default="Spanish", help="Target language (default: Spanish)")
70
71 # parse / lint
72 parser_parser = subparsers.add_parser("parser", help="Parse and lint (stub)")
73 parser_parser.add_argument("path", help="Path to the theme")
74
75 # deploy (stub)
76 deploy_parser = subparsers.add_parser("deploy", help="Deploy to Cloudflare (stub)")
77 deploy_parser.add_argument("path", help="Path to the site")
78 deploy_parser.add_argument("zone", help="Cloudflare zone")
79
80 # cloudflare (stub)
81 cloudflare_parser = subparsers.add_parser("cloudflare", help="Configure Cloudflare (stub)")
82 cloudflare_parser.add_argument("path", help="Path to the site")
83 cloudflare_parser.add_argument("zone", help="Cloudflare zone")
84
85 args = parser.parse_args()
86
87 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
88
89 # Override backend if specified on command line
90 if args.backend:
91 import config as cfg
92 cfg.BACKEND = args.backend
93
94 try:
95 if args.command == "complete":
96 result = complete(
97 args.path,
98 output_dir=args.output,
99 cms_name=args.cms_name,
100 cms_logo=args.cms_logo,
101 cms_color=args.cms_color,
102 )
103 print(result)
104 elif args.command == "analyze":
105 print(analyze(args.path))
106 elif args.command == "hugoify":
107 print(hugoify(args.path))
108 elif args.command == "decapify":
109 print(decapify(
110 args.path,
111 cms_name=args.cms_name,
112 cms_logo=args.cms_logo,
113 cms_color=args.cms_color,
114 ))
115 elif args.command == "translate":
116 print(translate(args.path, target_language=args.target_language))
117 elif args.command == "parser":
118 print(parse(args.path))
119 elif args.command == "deploy":
120 print(deploy(args.path, args.zone))
121 elif args.command == "cloudflare":
122 print(configure_cloudflare(args.path, args.zone))
123 else:
124 parser.print_help()
125 except (ValueError, EnvironmentError) as e:
126 print(f"Error: {e}", file=sys.stderr)
127 sys.exit(1)
128
129
130 if __name__ == "__main__":
131 main()
--- a/src/cli.py
+++ b/src/cli.py
@@ -1,131 +0,0 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
D src/config.py
-38
--- a/src/config.py
+++ b/src/config.py
@@ -1,38 +0,0 @@
1
-"""
2
-Multi-backend AI configuration.
3
-
4
-Set HUGOIFIER_BACKEND env var to switch backends:
5
- anthropic (default) — claude-sonnet-4-6
6
- openai — gpt-4-turbo
7
- google — gemini-1.5-pro
8
-
9
-Model can be overridden per-backend:
10
- ANTHROPIC_MODEL, OPENAI_MODEL, GOOGLE_MODEL
11
-"""
12
-
13
-import os
14
-
15
-BACKEND = os.getenv('HUGOIFIER_BACKEND', 'anthropic').lower()
16
-
17
-# Anthropic settings
18
-ANTHROPIC_API_KEY = os.getenv('ANTHROPIC_API_KEY')
19
-ANTHROPIC_MODEL = os.getenv('ANTHROPIC_MODEL', 'claude-sonnet-4-6')
20
-
21
-# OpenAI settings
22
-OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')
23
-OPENAI_MODEL = os.getenv('OPENAI_MODEL', 'gpt-4-turbo')
24
-
25
-# Google settings
26
-GOOGLE_API_KEY = os.getenv('GOOGLE_API_KEY')
27
-GOOGLE_MODEL = os.getenv('GOOGLE_MODEL', 'gemini-1.5-pro')
28
-
29
-MAX_TOKENS = int(os.getenv('HUGOIFIER_MAX_TOKENS', '4096'))
30
-
31
-
32
-def call_ai(prompt: str, system: str = "You are a helpful Hugo theme conversion assistant."x_tokens or MAX_TOKENS
33
- if BACKEND == 'anthropic':
34
- return _ca)
35
- elif BACKEND == 'openai':
36
- return _call_openai(prompt, system)
37
- elif BACKEND == 'google':
38
- retu
--- a/src/config.py
+++ b/src/config.py
@@ -1,38 +0,0 @@
1 """
2 Multi-backend AI configuration.
3
4 Set HUGOIFIER_BACKEND env var to switch backends:
5 anthropic (default) — claude-sonnet-4-6
6 openai — gpt-4-turbo
7 google — gemini-1.5-pro
8
9 Model can be overridden per-backend:
10 ANTHROPIC_MODEL, OPENAI_MODEL, GOOGLE_MODEL
11 """
12
13 import os
14
15 BACKEND = os.getenv('HUGOIFIER_BACKEND', 'anthropic').lower()
16
17 # Anthropic settings
18 ANTHROPIC_API_KEY = os.getenv('ANTHROPIC_API_KEY')
19 ANTHROPIC_MODEL = os.getenv('ANTHROPIC_MODEL', 'claude-sonnet-4-6')
20
21 # OpenAI settings
22 OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')
23 OPENAI_MODEL = os.getenv('OPENAI_MODEL', 'gpt-4-turbo')
24
25 # Google settings
26 GOOGLE_API_KEY = os.getenv('GOOGLE_API_KEY')
27 GOOGLE_MODEL = os.getenv('GOOGLE_MODEL', 'gemini-1.5-pro')
28
29 MAX_TOKENS = int(os.getenv('HUGOIFIER_MAX_TOKENS', '4096'))
30
31
32 def call_ai(prompt: str, system: str = "You are a helpful Hugo theme conversion assistant."x_tokens or MAX_TOKENS
33 if BACKEND == 'anthropic':
34 return _ca)
35 elif BACKEND == 'openai':
36 return _call_openai(prompt, system)
37 elif BACKEND == 'google':
38 retu
--- a/src/config.py
+++ b/src/config.py
@@ -1,38 +0,0 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
D src/utils/analyze.py
-81
--- a/src/utils/analyze.py
+++ b/src/utils/analyze.py
@@ -1,81 +0,0 @@
1
-"""
2
-Analyzes a Hugo theme or raw HTML theme and reports structure + recommendations.
3
-"""
4
-
5
-import logging
6
-import os
7
-
8
-from config import call_ai
9
-from utils.theme_finder import find_hugo_theme, find_raw_html_files
10
-
11
-SYSTEM = "You are an expert Hugo theme developer analyzing themes for conversion."
12
-
13
-
14
-def analyze(path: str) -> str:
15
- logging.info(f"Analyzing {path} ...")
16
-
17
- info = find_hugo_theme(path)
18
-
19
- if info:
20
- return _analyze_hugo_theme(info)
21
- else:
22
- return _analyze_raw_html(path)
23
-
24
-
25
-def _analyze_hugo_theme(info: dict) -> str:
26
- theme_dir = info['theme_dir']
27
- theme_name = info['theme_name']
28
- example_site = info['example_site']
29
-
30
- # Collect layout files
31
- layouts = []
32
- for root, dirs, files in os.walk(os.path.join(theme_dir, 'layouts')):
33
- for f in files:
34
- if f.endswith('.html'):
35
- rel = os.path.relpath(os.path.join(root, f), theme_dir)
36
- layouts.append(rel)
37
-
38
- # Collect content types from exampleSite
39
- content_types = []
40
- if example_site:
41
- content_dir = os.path.join(example_site, 'content')
42
- if os.path.isdir(content_dir):
43
- content_types = [d for d in os.listdir(content_dir) if os.path.isdir(os.path.join(content_dir, d))]
44
-
45
- report = [
46
- f"Theme: {theme_name}",
47
- f"Theme dir: {theme_dir}",
48
- f"Layouts ({len(layouts)}):",
49
- *[f" {layout}" for layout in sorted(layouts)],
50
- f"Content types: {content_types}",
51
- f"ExampleSite: {example_site or 'none'}",
52
- "",
53
- "Status: Already a Hugo theme. Use 'complete' to assemble a working site.",
54
- ]
55
- return "\n".join(report)
56
-
57
-
58
-def _analyze_raw_html(path: str) -> str:
59
- html_files = find_raw_html_files(path)
60
- if not html_files:
61
- return f"No HTML files found at {path}"
62
-
63
- # Read main HTML file for AI analysis
64
- main = next((f for f in html_files if os.path.basename(f).lower() == 'index.html'), html_files[0])
65
- with open(main, 'r', errors='replace') as f:
66
- html = f.read()[:20000]
67
-
68
- prompt = f"""Analyze this HTML theme file and provide:
69
-1. Identified reusable components (header, footer, nav, sidebar, etc.)
70
-2. Recommended Hugo template tags for dynamic content
71
-3. Suggested partial splits
72
-4. Content sections that should be data/ YAML files for Decap CMS
73
-
74
-HTML:
75
-{html}"""
76
-
77
- try:
78
- return call_ai(prompt, SYSTEM)
79
- except Exception as e:
80
- logging.error(f"AI analysis failed: {e}")
81
- return f"HTML theme with {len(html_files)} files. AI analysis failed: {e}"
--- a/src/utils/analyze.py
+++ b/src/utils/analyze.py
@@ -1,81 +0,0 @@
1 """
2 Analyzes a Hugo theme or raw HTML theme and reports structure + recommendations.
3 """
4
5 import logging
6 import os
7
8 from config import call_ai
9 from utils.theme_finder import find_hugo_theme, find_raw_html_files
10
11 SYSTEM = "You are an expert Hugo theme developer analyzing themes for conversion."
12
13
14 def analyze(path: str) -> str:
15 logging.info(f"Analyzing {path} ...")
16
17 info = find_hugo_theme(path)
18
19 if info:
20 return _analyze_hugo_theme(info)
21 else:
22 return _analyze_raw_html(path)
23
24
25 def _analyze_hugo_theme(info: dict) -> str:
26 theme_dir = info['theme_dir']
27 theme_name = info['theme_name']
28 example_site = info['example_site']
29
30 # Collect layout files
31 layouts = []
32 for root, dirs, files in os.walk(os.path.join(theme_dir, 'layouts')):
33 for f in files:
34 if f.endswith('.html'):
35 rel = os.path.relpath(os.path.join(root, f), theme_dir)
36 layouts.append(rel)
37
38 # Collect content types from exampleSite
39 content_types = []
40 if example_site:
41 content_dir = os.path.join(example_site, 'content')
42 if os.path.isdir(content_dir):
43 content_types = [d for d in os.listdir(content_dir) if os.path.isdir(os.path.join(content_dir, d))]
44
45 report = [
46 f"Theme: {theme_name}",
47 f"Theme dir: {theme_dir}",
48 f"Layouts ({len(layouts)}):",
49 *[f" {layout}" for layout in sorted(layouts)],
50 f"Content types: {content_types}",
51 f"ExampleSite: {example_site or 'none'}",
52 "",
53 "Status: Already a Hugo theme. Use 'complete' to assemble a working site.",
54 ]
55 return "\n".join(report)
56
57
58 def _analyze_raw_html(path: str) -> str:
59 html_files = find_raw_html_files(path)
60 if not html_files:
61 return f"No HTML files found at {path}"
62
63 # Read main HTML file for AI analysis
64 main = next((f for f in html_files if os.path.basename(f).lower() == 'index.html'), html_files[0])
65 with open(main, 'r', errors='replace') as f:
66 html = f.read()[:20000]
67
68 prompt = f"""Analyze this HTML theme file and provide:
69 1. Identified reusable components (header, footer, nav, sidebar, etc.)
70 2. Recommended Hugo template tags for dynamic content
71 3. Suggested partial splits
72 4. Content sections that should be data/ YAML files for Decap CMS
73
74 HTML:
75 {html}"""
76
77 try:
78 return call_ai(prompt, SYSTEM)
79 except Exception as e:
80 logging.error(f"AI analysis failed: {e}")
81 return f"HTML theme with {len(html_files)} files. AI analysis failed: {e}"
--- a/src/utils/analyze.py
+++ b/src/utils/analyze.py
@@ -1,81 +0,0 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
D src/utils/cloudflare.py
-30
--- a/src/utils/cloudflare.py
+++ b/src/utils/cloudflare.py
@@ -1,30 +0,0 @@
1
-"""
2
-This script configures and deploys a Hugo site to Cloudflare, handling page creation, DNS settings, and deployment.
3
-It uses Cloudflare's API to automate these tasks.
4
-"""
5
-
6
-import logging
7
-
8
-# Function to configure and deploy to Cloudflare
9
-def configure_cloudflare(path, zone):
10
- logging.info(f"Starting Cloudflare configuration for {path} in zone {zone}...")
11
- try:
12
- # Placeholder logic for Cloudflare configuration
13
- # This could involve API calls to Cloudflare to create pages, set DNS, etc.
14
- logging.info("Creating Cloudflare page...")
15
- # Example API call to create a page
16
- # cloudflare_api.create_page(path, zone)
17
-
18
- logging.info("Deploying site...")
19
- # Example API call to deploy the site
20
- # cloudflare_api.deploy_site(path, zone)
21
-
22
- logging.info("Configuring DNS settings...")
23
- # Example API call to configure DNS
24
- # cloudflare_api.configure_dns(path, zone)
25
-
26
- logging.info("Cloudflare configuration complete.")
27
- return "Cloudflare configuration complete"
28
- except Exception as e:
29
- logging.error(f"Error during Cloudflare configuration: {e}")
30
- return "Cloudflare configuration failed"
--- a/src/utils/cloudflare.py
+++ b/src/utils/cloudflare.py
@@ -1,30 +0,0 @@
1 """
2 This script configures and deploys a Hugo site to Cloudflare, handling page creation, DNS settings, and deployment.
3 It uses Cloudflare's API to automate these tasks.
4 """
5
6 import logging
7
8 # Function to configure and deploy to Cloudflare
9 def configure_cloudflare(path, zone):
10 logging.info(f"Starting Cloudflare configuration for {path} in zone {zone}...")
11 try:
12 # Placeholder logic for Cloudflare configuration
13 # This could involve API calls to Cloudflare to create pages, set DNS, etc.
14 logging.info("Creating Cloudflare page...")
15 # Example API call to create a page
16 # cloudflare_api.create_page(path, zone)
17
18 logging.info("Deploying site...")
19 # Example API call to deploy the site
20 # cloudflare_api.deploy_site(path, zone)
21
22 logging.info("Configuring DNS settings...")
23 # Example API call to configure DNS
24 # cloudflare_api.configure_dns(path, zone)
25
26 logging.info("Cloudflare configuration complete.")
27 return "Cloudflare configuration complete"
28 except Exception as e:
29 logging.error(f"Error during Cloudflare configuration: {e}")
30 return "Cloudflare configuration failed"
--- a/src/utils/cloudflare.py
+++ b/src/utils/cloudflare.py
@@ -1,30 +0,0 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
D src/utils/complete.py
-239
--- a/src/utils/complete.py
+++ b/src/utils/complete.py
@@ -1,239 +0,0 @@
1
-"""
2
-Full end-to-end pipeline: detect → copy → configure → decap.
3
-
4
-For already-Hugo themes: assembles a clean, standalone site.
5
-For raw HTML themes: calls hugoify first, then assembles.
6
-"""
7
-
8
-import logging
9
-import os
10
-import shutil
11
-from pathlib import Path
12
-
13
-from utils.theme_finder import find_hugo_theme, find_raw_html_files
14
-from utils.hugoify import hugoify_html
15
-from utils.decapify import decapify
16
-from utils.theme_patcher import patch_theme, patch_config
17
-
18
-def complete(
19
- input_path: str,
20
- output_dir: str = None,
21
- cms_name: str = None,
22
- cms_logo: str = None,
23
- cms_color: str = None,
24
-) -> str:
25
- """
26
- Run the full pipeline for a theme.
27
-
28
- Args:
29
- input_path: Path to a theme directory (from themes/) or raw HTML dir.
30
- output_dir: Where to write the output site. Defaults to output/{theme-name}.
31
- cms_name: Whitelabel CMS name for Decap admin UI.
32
- cms_logo: Whitelabel logo URL for Decap admin UI.
33
- cms_color: Whitelabel top-bar color for Decap admin UI.
34
-
35
- Returns:
36
- Path to the generated site, or error message.
37
- """
38
- logging.info(f"Starting pipeline for {input_path} ...")
39
-
40
- branding = {'cms_name': cms_name, 'cms_logo': cms_logo, 'cms_color': cms_color}
41
- info = find_hugo_theme(input_path)
42
-
43
- if info:
44
- return _assemble_hugo_site(info, output_dir, branding)
45
- else:
46
- # Raw HTML path
47
- html_files = find_raw_html_files(input_path)
48
- if not html_files:
49
- raise ValueError(f"No Hugo theme or HTML files found in {input_path}")
50
- return _convert_raw_html(input_path, html_files, output_dir, branding)
51
-
52
-
53
-# ---------------------------------------------------------------------------
54
-# Hugo theme path
55
-# ---------------------------------------------------------------------------
56
-
57
-def _assemble_hugo_site(info: dict, output_dir: str = None, branding: dict = None) -> str:
58
- theme_dir = info['theme_dir']
59
- example_site = info['example_site']
60
- theme_name = info['theme_name']
61
-
62
- if output_dir is None:
63
- output_dir = str(Path(__file__).parents[2] / 'output' / theme_name)
64
-
65
- logging.info(f"Building site at {output_dir} ...")
66
- os.makedirs(output_dir, exist_ok=True)
67
-
68
- # 1. Copy theme files → themes/{theme_name}/
69
- dest_theme = os.path.join(output_dir, 'themes', theme_name)
70
- _copy_dir(theme_dir, dest_theme, exclude={'exampleSite', '__MACOSX', '.DS_Store'})
71
- logging.info(f"Copied theme to {dest_theme}")
72
- patch_theme(dest_theme)
73
-
74
- # 2. Copy exampleSite content/static/data → site root
75
- if example_site:
76
- for subdir in ('content', 'data', 'i18n'):
77
- src = os.path.join(example_site, subdir)
78
- if os.path.isdir(src):
79
- _copy_dir(src, os.path.join(output_dir, subdir))
80
- logging.info(f"Copied {subdir}/ from exampleSite")
81
-
82
- # Static: merge exampleSite/static into output/static
83
- src_static = os.path.join(example_site, 'static')
84
- if os.path.isdir(src_static):
85
- _copy_dir(src_static, os.path.join(output_dir, 'static'))
86
- logging.info("Copied static/ from exampleSite")
87
-
88
- # Write hugo.toml from exampleSite config
89
- config_toml = _find_config(example_site)
90
- if config_toml:
91
- _write_hugo_toml(config_toml, output_dir, theme_name)
92
- else:
93
- _write_minimal_hugo_toml(output_dir, theme_name)
94
- else:
95
- _write_minimal_hugo_toml(output_dir, theme_name)
96
- # Create minimal content/_index.md
97
- content_dir = os.path.join(output_dir, 'content')
98
- os.makedirs(content_dir, exist_ok=True)
99
- index_md = os.path.join(content_dir, '_index.md')
100
- if not os.path.exists(index_md):
101
- with open(index_md, 'w') as f:
102
- f.write('---\ntitle: Home\n---\n')
103
-
104
- # 3. Generate Decap CMS config
105
- b = branding or {}
106
- decapify(output_dir, cms_name=b.get('cms_name'), cms_logo=b.get('cms_logo'), cms_color=b.get('cms_color'))
107
-
108
- logging.info(f"Done. Site ready at: {output_dir}")
109
- logging.info(f"Run: cd {output_dir} && hugo serve")
110
- return output_dir
111
-
112
-
113
-# ---------------------------------------------------------------------------
114
-# Raw HTML path
115
-# ---------------------------------------------------------------------------
116
-
117
-def _convert_raw_html(input_path: str, html_files: list, output_dir: str = None, branding: dict = None) -> str:
118
- theme_name = os.path.basename(os.path.abspath(input_path))
119
-
120
- if output_dir is None:
121
- output_dir = str(Path(__file__).parents[2] / 'output' / theme_name)
122
-
123
- logging.info(f"Converting raw HTML theme: {theme_name}")
124
-
125
- # Use AI to convert the main HTML file to Hugo layouts
126
- main_html = _pick_main_html(html_files)
127
- logging.info(f"Converting {main_html} ...")
128
- hugo_layouts = hugoify_html(main_html)
129
-
130
- os.makedirs(output_dir, exist_ok=True)
131
-
132
- # Write converted layouts
133
- theme_layouts_dir = os.path.join(output_dir, 'themes', theme_name, 'layouts')
134
- os.makedirs(os.path.join(theme_layouts_dir, '_default'), exist_ok=True)
135
- os.makedirs(os.path.join(theme_layouts_dir, 'partials'), exist_ok=True)
136
-
137
- for filename, content in hugo_layouts.items():
138
- dest = os.path.join(theme_layouts_dir, filename)
139
- os.makedirs(os.path.dirname(dest), exist_ok=True)
140
- with open(dest, 'w') as f:
141
- f.write(content)
142
-
143
- # Copy CSS/JS/images
144
- for ext_dir in ('css', 'js', 'images', 'img', 'assets', 'fonts'):
145
- src = os.path.join(input_path, ext_dir)
146
- if os.path.isdir(src):
147
- _copy_dir(src, os.path.join(output_dir, 'themes', theme_name, 'static', ext_dir))
148
-
149
- _write_minimal_hugo_toml(output_dir, theme_name)
150
-
151
- # Create minimal content
152
- content_dir = os.path.join(output_dir, 'content')
153
- os.makedirs(content_dir, exist_ok=True)
154
- with open(os.path.join(content_dir, '_index.md'), 'w') as f:
155
- f.write('---\ntitle: Home\n---\n')
156
-
157
- b = branding or {}
158
- decapify(output_dir, cms_name=b.get('cms_name'), cms_logo=b.get('cms_logo'), cms_color=b.get('cms_color'))
159
-
160
- logging.info(f"Done. Site ready at: {output_dir}")
161
- return output_dir
162
-
163
-
164
-# ---------------------------------------------------------------------------
165
-# Helpers
166
-# ---------------------------------------------------------------------------
167
-
168
-def _copy_dir(src: str, dest: str, exclude: set = None):
169
- """Copy src → dest, skipping excluded names."""
170
- exclude = exclude or set()
171
- if not os.path.isdir(src):
172
- return
173
- os.makedirs(dest, exist_ok=True)
174
- for item in os.listdir(src):
175
- if item in exclude or item.startswith('._'):
176
- continue
177
- s = os.path.join(src, item)
178
- d = os.path.join(dest, item)
179
- if os.path.isdir(s):
180
- _copy_dir(s, d, exclude)
181
- else:
182
- shutil.copy2(s, d)
183
-
184
-
185
-def _find_config(example_site: str) -> str | None:
186
- """Find config.toml or hugo.toml in exampleSite."""
187
- for name in ('hugo.toml', 'config.toml'):
188
- p = os.path.join(example_site, name)
189
- if os.path.exists(p):
190
- return p
191
- # config/_default/config.toml pattern
192
- p = os.path.join(example_site, 'config', '_default', 'config.toml')
193
- if os.path.exists(p):
194
- return p
195
- return None
196
-
197
-
198
-def _write_hugo_toml(source_config: str, output_dir: str, theme_name: str):
199
- """Copy source config to hugo.toml, ensuring theme = theme_name and modern key names."""
200
- import re
201
- with open(source_config, 'r') as f:
202
- content = f.read()
203
-
204
- # Suppress noisy but harmless warnings from example content
205
- if 'ignoreLogs' not in content:
206
- content += "\nignorelogs = ['warning-goldmark-raw-html']\n"
207
-
208
- # Ensure theme is set correctly
209
- if re.search(r'^theme\s*=', content, re.MULTILINE):
210
- content = re.sub(r'^theme\s*=.*$', f'theme = "{theme_name}"', content, flags=re.MULTILINE)
211
- else:
212
- content = f'theme = "{theme_name}"\n' + content
213
-
214
- dest = os.path.join(output_dir, 'hugo.toml')
215
- with open(dest, 'w') as f:
216
- f.write(content)
217
- patch_config(dest)
218
- logging.info("Wrote hugo.toml")
219
-
220
-
221
-def _write_minimal_hugo_toml(output_dir: str, theme_name: str):
222
- dest = os.path.join(output_dir, 'hugo.toml')
223
- safe_name = theme_name.replace('"', '')
224
- title = safe_name.replace('-', ' ').title()
225
- with open(dest, 'w') as f:
226
- f.write(f'''baseURL = "http://localhost:1313/"
227
-languageCode = "en-us"
228
-title = "{title}"
229
-theme = "{safe_name}"
230
-''')
231
- logging.info("Wrote minimal hugo.toml")
232
-
233
-
234
-def _pick_main_html(html_files: list) -> str:
235
- """Pick the most likely 'main' HTML file (index.html or first one)."""
236
- for f in html_files:
237
- if os.path.basename(f).lower() in ('index.html', 'home.html', 'main.html'):
238
- return f
239
- return html_files[0]
--- a/src/utils/complete.py
+++ b/src/utils/complete.py
@@ -1,239 +0,0 @@
1 """
2 Full end-to-end pipeline: detect → copy → configure → decap.
3
4 For already-Hugo themes: assembles a clean, standalone site.
5 For raw HTML themes: calls hugoify first, then assembles.
6 """
7
8 import logging
9 import os
10 import shutil
11 from pathlib import Path
12
13 from utils.theme_finder import find_hugo_theme, find_raw_html_files
14 from utils.hugoify import hugoify_html
15 from utils.decapify import decapify
16 from utils.theme_patcher import patch_theme, patch_config
17
18 def complete(
19 input_path: str,
20 output_dir: str = None,
21 cms_name: str = None,
22 cms_logo: str = None,
23 cms_color: str = None,
24 ) -> str:
25 """
26 Run the full pipeline for a theme.
27
28 Args:
29 input_path: Path to a theme directory (from themes/) or raw HTML dir.
30 output_dir: Where to write the output site. Defaults to output/{theme-name}.
31 cms_name: Whitelabel CMS name for Decap admin UI.
32 cms_logo: Whitelabel logo URL for Decap admin UI.
33 cms_color: Whitelabel top-bar color for Decap admin UI.
34
35 Returns:
36 Path to the generated site, or error message.
37 """
38 logging.info(f"Starting pipeline for {input_path} ...")
39
40 branding = {'cms_name': cms_name, 'cms_logo': cms_logo, 'cms_color': cms_color}
41 info = find_hugo_theme(input_path)
42
43 if info:
44 return _assemble_hugo_site(info, output_dir, branding)
45 else:
46 # Raw HTML path
47 html_files = find_raw_html_files(input_path)
48 if not html_files:
49 raise ValueError(f"No Hugo theme or HTML files found in {input_path}")
50 return _convert_raw_html(input_path, html_files, output_dir, branding)
51
52
53 # ---------------------------------------------------------------------------
54 # Hugo theme path
55 # ---------------------------------------------------------------------------
56
57 def _assemble_hugo_site(info: dict, output_dir: str = None, branding: dict = None) -> str:
58 theme_dir = info['theme_dir']
59 example_site = info['example_site']
60 theme_name = info['theme_name']
61
62 if output_dir is None:
63 output_dir = str(Path(__file__).parents[2] / 'output' / theme_name)
64
65 logging.info(f"Building site at {output_dir} ...")
66 os.makedirs(output_dir, exist_ok=True)
67
68 # 1. Copy theme files → themes/{theme_name}/
69 dest_theme = os.path.join(output_dir, 'themes', theme_name)
70 _copy_dir(theme_dir, dest_theme, exclude={'exampleSite', '__MACOSX', '.DS_Store'})
71 logging.info(f"Copied theme to {dest_theme}")
72 patch_theme(dest_theme)
73
74 # 2. Copy exampleSite content/static/data → site root
75 if example_site:
76 for subdir in ('content', 'data', 'i18n'):
77 src = os.path.join(example_site, subdir)
78 if os.path.isdir(src):
79 _copy_dir(src, os.path.join(output_dir, subdir))
80 logging.info(f"Copied {subdir}/ from exampleSite")
81
82 # Static: merge exampleSite/static into output/static
83 src_static = os.path.join(example_site, 'static')
84 if os.path.isdir(src_static):
85 _copy_dir(src_static, os.path.join(output_dir, 'static'))
86 logging.info("Copied static/ from exampleSite")
87
88 # Write hugo.toml from exampleSite config
89 config_toml = _find_config(example_site)
90 if config_toml:
91 _write_hugo_toml(config_toml, output_dir, theme_name)
92 else:
93 _write_minimal_hugo_toml(output_dir, theme_name)
94 else:
95 _write_minimal_hugo_toml(output_dir, theme_name)
96 # Create minimal content/_index.md
97 content_dir = os.path.join(output_dir, 'content')
98 os.makedirs(content_dir, exist_ok=True)
99 index_md = os.path.join(content_dir, '_index.md')
100 if not os.path.exists(index_md):
101 with open(index_md, 'w') as f:
102 f.write('---\ntitle: Home\n---\n')
103
104 # 3. Generate Decap CMS config
105 b = branding or {}
106 decapify(output_dir, cms_name=b.get('cms_name'), cms_logo=b.get('cms_logo'), cms_color=b.get('cms_color'))
107
108 logging.info(f"Done. Site ready at: {output_dir}")
109 logging.info(f"Run: cd {output_dir} && hugo serve")
110 return output_dir
111
112
113 # ---------------------------------------------------------------------------
114 # Raw HTML path
115 # ---------------------------------------------------------------------------
116
117 def _convert_raw_html(input_path: str, html_files: list, output_dir: str = None, branding: dict = None) -> str:
118 theme_name = os.path.basename(os.path.abspath(input_path))
119
120 if output_dir is None:
121 output_dir = str(Path(__file__).parents[2] / 'output' / theme_name)
122
123 logging.info(f"Converting raw HTML theme: {theme_name}")
124
125 # Use AI to convert the main HTML file to Hugo layouts
126 main_html = _pick_main_html(html_files)
127 logging.info(f"Converting {main_html} ...")
128 hugo_layouts = hugoify_html(main_html)
129
130 os.makedirs(output_dir, exist_ok=True)
131
132 # Write converted layouts
133 theme_layouts_dir = os.path.join(output_dir, 'themes', theme_name, 'layouts')
134 os.makedirs(os.path.join(theme_layouts_dir, '_default'), exist_ok=True)
135 os.makedirs(os.path.join(theme_layouts_dir, 'partials'), exist_ok=True)
136
137 for filename, content in hugo_layouts.items():
138 dest = os.path.join(theme_layouts_dir, filename)
139 os.makedirs(os.path.dirname(dest), exist_ok=True)
140 with open(dest, 'w') as f:
141 f.write(content)
142
143 # Copy CSS/JS/images
144 for ext_dir in ('css', 'js', 'images', 'img', 'assets', 'fonts'):
145 src = os.path.join(input_path, ext_dir)
146 if os.path.isdir(src):
147 _copy_dir(src, os.path.join(output_dir, 'themes', theme_name, 'static', ext_dir))
148
149 _write_minimal_hugo_toml(output_dir, theme_name)
150
151 # Create minimal content
152 content_dir = os.path.join(output_dir, 'content')
153 os.makedirs(content_dir, exist_ok=True)
154 with open(os.path.join(content_dir, '_index.md'), 'w') as f:
155 f.write('---\ntitle: Home\n---\n')
156
157 b = branding or {}
158 decapify(output_dir, cms_name=b.get('cms_name'), cms_logo=b.get('cms_logo'), cms_color=b.get('cms_color'))
159
160 logging.info(f"Done. Site ready at: {output_dir}")
161 return output_dir
162
163
164 # ---------------------------------------------------------------------------
165 # Helpers
166 # ---------------------------------------------------------------------------
167
168 def _copy_dir(src: str, dest: str, exclude: set = None):
169 """Copy src → dest, skipping excluded names."""
170 exclude = exclude or set()
171 if not os.path.isdir(src):
172 return
173 os.makedirs(dest, exist_ok=True)
174 for item in os.listdir(src):
175 if item in exclude or item.startswith('._'):
176 continue
177 s = os.path.join(src, item)
178 d = os.path.join(dest, item)
179 if os.path.isdir(s):
180 _copy_dir(s, d, exclude)
181 else:
182 shutil.copy2(s, d)
183
184
185 def _find_config(example_site: str) -> str | None:
186 """Find config.toml or hugo.toml in exampleSite."""
187 for name in ('hugo.toml', 'config.toml'):
188 p = os.path.join(example_site, name)
189 if os.path.exists(p):
190 return p
191 # config/_default/config.toml pattern
192 p = os.path.join(example_site, 'config', '_default', 'config.toml')
193 if os.path.exists(p):
194 return p
195 return None
196
197
198 def _write_hugo_toml(source_config: str, output_dir: str, theme_name: str):
199 """Copy source config to hugo.toml, ensuring theme = theme_name and modern key names."""
200 import re
201 with open(source_config, 'r') as f:
202 content = f.read()
203
204 # Suppress noisy but harmless warnings from example content
205 if 'ignoreLogs' not in content:
206 content += "\nignorelogs = ['warning-goldmark-raw-html']\n"
207
208 # Ensure theme is set correctly
209 if re.search(r'^theme\s*=', content, re.MULTILINE):
210 content = re.sub(r'^theme\s*=.*$', f'theme = "{theme_name}"', content, flags=re.MULTILINE)
211 else:
212 content = f'theme = "{theme_name}"\n' + content
213
214 dest = os.path.join(output_dir, 'hugo.toml')
215 with open(dest, 'w') as f:
216 f.write(content)
217 patch_config(dest)
218 logging.info("Wrote hugo.toml")
219
220
221 def _write_minimal_hugo_toml(output_dir: str, theme_name: str):
222 dest = os.path.join(output_dir, 'hugo.toml')
223 safe_name = theme_name.replace('"', '')
224 title = safe_name.replace('-', ' ').title()
225 with open(dest, 'w') as f:
226 f.write(f'''baseURL = "http://localhost:1313/"
227 languageCode = "en-us"
228 title = "{title}"
229 theme = "{safe_name}"
230 ''')
231 logging.info("Wrote minimal hugo.toml")
232
233
234 def _pick_main_html(html_files: list) -> str:
235 """Pick the most likely 'main' HTML file (index.html or first one)."""
236 for f in html_files:
237 if os.path.basename(f).lower() in ('index.html', 'home.html', 'main.html'):
238 return f
239 return html_files[0]
--- a/src/utils/complete.py
+++ b/src/utils/complete.py
@@ -1,239 +0,0 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
D src/utils/decapify.py
-270
--- a/src/utils/decapify.py
+++ b/src/utils/decapify.py
@@ -1,270 +0,0 @@
1
-"""
2
-Generates Decap CMS integration for a Hugo site.
3
-
4
-Writes:
5
- static/admin/index.html — Decap CMS admin panel
6
- static/admin/config.yml — CMS config mapped to actual content structure
7
-"""
8
-
9
-import logging
10
-import os
11
-import re
12
-import yaml
13
-
14
-DECAP_CDN = "https://unpkg.com/decap-cms@^3.0.0/dist/decap-cms.js"
15
-
16
-# Whitelabel defaults — override via decapify() kwargs or env vars
17
-DEFAULT_CMS_NAME = os.getenv('CMS_NAME', 'Content Manager')
18
-DEFAULT_CMS_LOGO = os.getenv('CMS_LOGO_URL', '') # URL or empty
19
-DEFAULT_CMS_COLOR = os.getenv('CMS_COLOR', '#2e3748') # top-bar background
20
-
21
-
22
-def decapify(
23
- site_dir: str,
24
- cms_name: str = None,
25
- cms_logo: str = None,
26
- cms_color: str = None,
27
-) -> str:
28
- """
29
- Add Decap CMS to a Hugo site directory.
30
-
31
- Args:
32
- site_dir: Root of the assembled Hugo site (has hugo.toml, content/, themes/).
33
- cms_name: Whitelabel name shown in the admin UI (default: 'Content Manager').
34
- cms_logo: URL to a logo image for the admin UI (optional).
35
- cms_color: Hex color for the admin top bar (default: '#2e3748').
36
-
37
- Returns:
38
- Status message.
39
- """
40
- logging.info(f"Adding Decap CMS to {site_dir} ...")
41
-
42
- admin_dir = os.path.join(site_dir, 'static', 'admin')
43
- os.makedirs(admin_dir, exist_ok=True)
44
-
45
- branding = {
46
- 'name': cms_name or DEFAULT_CMS_NAME,
47
- 'logo': cms_logo or DEFAULT_CMS_LOGO,
48
- 'color': cms_color or DEFAULT_CMS_COLOR,
49
- }
50
-
51
- _write_admin_index(admin_dir, branding)
52
- _write_decap_config(site_dir, admin_dir)
53
-
54
- logging.info("Decap CMS integration complete.")
55
- return "Decap CMS integration complete"
56
-
57
-
58
-# ---------------------------------------------------------------------------
59
-# Admin index.html
60
-# ---------------------------------------------------------------------------
61
-
62
-def _sanitize_color(color: str) -> str:
63
- """Allow only valid CSS hex colors (#rgb or #rrggbb) to prevent style injection."""
64
- if re.fullmatch(r'#[0-9a-fA-F]{3}(?:[0-9a-fA-F]{3})?', color):
65
- return color
66
- return '#2e3748' # fall back to default
67
-
68
-
69
-def _write_admin_index(admin_dir: str, branding: dict):
70
- import html as html_mod
71
- name = html_mod.escape(branding['name'])
72
- logo_html = ''
73
- if branding['logo']:
74
- logo_url = html_mod.escape(branding['logo'])
75
- logo_html = f'\n <img src="{logo_url}" alt="{name}" style="max-height:40px;margin:8px 0;">'
76
-
77
- color_css = ''
78
- if branding['color']:
79
- safe_color = _sanitize_color(branding['color'])
80
- color_css = f"""
81
- <style>
82
- [class^="AppHeader"] {{ background-color: {safe_color} !important; }}
83
- </style>"""
84
-
85
- html = f"""<!doctype html>
86
-<html>
87
-<head>
88
- <meta charset="utf-8" />
89
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
90
- <meta name="robots" content="noindex" />
91
- <title>{name}</title>{color_css}
92
-</head>
93
-<body>{logo_html}
94
- <script src="{DECAP_CDN}"></script>
95
-</body>
96
-</html>
97
-"""
98
- with open(os.path.join(admin_dir, 'index.html'), 'w') as f:
99
- f.write(html)
100
-
101
-
102
-# ---------------------------------------------------------------------------
103
-# config.yml
104
-# ---------------------------------------------------------------------------
105
-
106
-def _write_decap_config(site_dir: str, admin_dir: str):
107
- content_dir = os.path.join(site_dir, 'content')
108
- collections = _build_collections(content_dir)
109
-
110
- config = {
111
- 'backend': {
112
- 'name': 'git-gateway',
113
- 'branch': 'main',
114
- },
115
- 'media_folder': 'static/images/uploads',
116
- 'public_folder': '/images/uploads',
117
- 'collections': collections,
118
- }
119
-
120
- config_path = os.path.join(admin_dir, 'config.yml')
121
- with open(config_path, 'w') as f:
122
- yaml.dump(config, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
123
-
124
- logging.info(f"Wrote Decap CMS config to {config_path}")
125
-
126
-
127
-def _collect_md_files(dirpath: str) -> list:
128
- """Recursively collect all .md files under dirpath (excluding _index.md)."""
129
- found = []
130
- for root, dirs, files in os.walk(dirpath):
131
- for f in files:
132
- if f.endswith('.md') and f != '_index.md':
133
- found.append(os.path.join(root, f))
134
- return found
135
-
136
-
137
-def _build_collections(content_dir: str) -> list:
138
- """
139
- Inspect content/ to build Decap CMS collections.
140
- - Subdirs with any .md files (at any depth) → folder collection (e.g. blog)
141
- - Subdirs with only a top-level _index.md → file collection (e.g. about, contact)
142
- """
143
- if not os.path.isdir(content_dir):
144
- return [_default_pages_collection()]
145
-
146
- collections = []
147
-
148
- entries = sorted(os.listdir(content_dir))
149
- for entry in entries:
150
- subdir = os.path.join(content_dir, entry)
151
- if not os.path.isdir(subdir):
152
- continue
153
-
154
- # Collect all .md files at any depth (excluding _index.md)
155
- non_index = _collect_md_files(subdir)
156
- has_index = os.path.exists(os.path.join(subdir, '_index.md'))
157
-
158
- if non_index:
159
- # Folder collection (blog, posts, etc.) — use shallowest sample for field inference
160
- fields = _infer_fields_for_folder(subdir, [os.path.relpath(f, subdir) for f in non_index])
161
- collections.append({
162
- 'name': entry,
163
- 'label': entry.replace('-', ' ').title(),
164
- 'folder': f'content/{entry}',
165
- 'create': True,
166
- 'slug': '{{slug}}',
167
- 'fields': fields,
168
- })
169
- elif has_index:
170
- # File collection (single page)
171
- fields = _infer_fields_for_file(os.path.join(subdir, '_index.md'))
172
- collections.append({
173
- 'name': entry,
174
- 'label': entry.replace('-', ' ').title(),
175
- 'files': [{
176
- 'name': entry,
177
- 'label': entry.replace('-', ' ').title(),
178
- 'file': f'content/{entry}/_index.md',
179
- 'fields': fields,
180
- }],
181
- })
182
-
183
- if not collections:
184
- collections.append(_default_pages_collection())
185
-
186
- return collections
187
-
188
-
189
-def _infer_fields_for_folder(subdir: str, md_files: list) -> list:
190
- """Read a sample .md file and extract frontmatter keys as fields."""
191
- # md_files may be relative paths (from _collect_md_files); resolve to absolute
192
- first = md_files[0]
193
- sample = first if os.path.isabs(first) else os.path.join(subdir, first)
194
- frontmatter = _parse_frontmatter(sample)
195
-
196
- fields = []
197
- field_map = {
198
- 'title': {'label': 'Title', 'name': 'title', 'widget': 'string'},
199
- 'date': {'label': 'Date', 'name': 'date', 'widget': 'datetime'},
200
- 'description': {'label': 'Description', 'name': 'description', 'widget': 'text'},
201
- 'image': {'label': 'Image', 'name': 'image', 'widget': 'image', 'required': False},
202
- 'categories': {'label': 'Categories', 'name': 'categories', 'widget': 'list', 'required': False},
203
- 'tags': {'label': 'Tags', 'name': 'tags', 'widget': 'list', 'required': False},
204
- 'draft': {'label': 'Draft', 'name': 'draft', 'widget': 'boolean', 'default': False},
205
- 'author': {'label': 'Author', 'name': 'author', 'widget': 'string', 'required': False},
206
- }
207
-
208
- # Add known fields in a logical order
209
- for key in ['title', 'date', 'description', 'image', 'categories', 'tags', 'author', 'draft']:
210
- if key in frontmatter:
211
- fields.append(field_map[key])
212
-
213
- # Add any remaining frontmatter keys not in our map
214
- for key, value in frontmatter.items():
215
- if key not in field_map and key not in ('type', 'layout', 'url'):
216
- widget = _widget_for_value(value)
217
- fields.append({'label': key.title(), 'name': key, 'widget': widget, 'required': False})
218
-
219
- # Always include body
220
- fields.append({'label': 'Body', 'name': 'body', 'widget': 'markdown'})
221
-
222
- return fields
223
-
224
-
225
-def _infer_fields_for_file(md_path: str) -> list:
226
- """For a single page (_index.md), infer fields from frontmatter."""
227
- frontmatter = _parse_frontmatter(md_path)
228
- fields = []
229
- for key, value in frontmatter.items():
230
- widget = _widget_for_value(value)
231
- fields.append({'label': key.title(), 'name': key, 'widget': widget, 'required': False})
232
- fields.append({'label': 'Body', 'name': 'body', 'widget': 'markdown'})
233
- return fields
234
-
235
-
236
-def _parse_frontmatter(md_path: str) -> dict:
237
- """Parse YAML frontmatter from a .md file."""
238
- try:
239
- with open(md_path, 'r', errors='replace') as f:
240
- content = f.read()
241
- match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
242
- if match:
243
- return yaml.safe_load(match.group(1)) or {}
244
- except Exception:
245
- pass
246
- return {}
247
-
248
-
249
-def _widget_for_value(value) -> str:
250
- if isinstance(value, bool):
251
- return 'boolean'
252
- if isinstance(value, (int, float)):
253
- return 'number'
254
- if isinstance(value, list):
255
- return 'list'
256
- return 'string'
257
-
258
-
259
-def _default_pages_collection() -> dict:
260
- return {
261
- 'name': 'pages',
262
- 'label': 'Pages',
263
- 'folder': 'content',
264
- 'create': True,
265
- 'slug': '{{slug}}',
266
- 'fields': [
267
- {'label': 'Title', 'name': 'title', 'widget': 'string'},
268
- {'label': 'Body', 'name': 'body', 'widget': 'markdown'},
269
- ],
270
- }
--- a/src/utils/decapify.py
+++ b/src/utils/decapify.py
@@ -1,270 +0,0 @@
1 """
2 Generates Decap CMS integration for a Hugo site.
3
4 Writes:
5 static/admin/index.html — Decap CMS admin panel
6 static/admin/config.yml — CMS config mapped to actual content structure
7 """
8
9 import logging
10 import os
11 import re
12 import yaml
13
14 DECAP_CDN = "https://unpkg.com/decap-cms@^3.0.0/dist/decap-cms.js"
15
16 # Whitelabel defaults — override via decapify() kwargs or env vars
17 DEFAULT_CMS_NAME = os.getenv('CMS_NAME', 'Content Manager')
18 DEFAULT_CMS_LOGO = os.getenv('CMS_LOGO_URL', '') # URL or empty
19 DEFAULT_CMS_COLOR = os.getenv('CMS_COLOR', '#2e3748') # top-bar background
20
21
22 def decapify(
23 site_dir: str,
24 cms_name: str = None,
25 cms_logo: str = None,
26 cms_color: str = None,
27 ) -> str:
28 """
29 Add Decap CMS to a Hugo site directory.
30
31 Args:
32 site_dir: Root of the assembled Hugo site (has hugo.toml, content/, themes/).
33 cms_name: Whitelabel name shown in the admin UI (default: 'Content Manager').
34 cms_logo: URL to a logo image for the admin UI (optional).
35 cms_color: Hex color for the admin top bar (default: '#2e3748').
36
37 Returns:
38 Status message.
39 """
40 logging.info(f"Adding Decap CMS to {site_dir} ...")
41
42 admin_dir = os.path.join(site_dir, 'static', 'admin')
43 os.makedirs(admin_dir, exist_ok=True)
44
45 branding = {
46 'name': cms_name or DEFAULT_CMS_NAME,
47 'logo': cms_logo or DEFAULT_CMS_LOGO,
48 'color': cms_color or DEFAULT_CMS_COLOR,
49 }
50
51 _write_admin_index(admin_dir, branding)
52 _write_decap_config(site_dir, admin_dir)
53
54 logging.info("Decap CMS integration complete.")
55 return "Decap CMS integration complete"
56
57
58 # ---------------------------------------------------------------------------
59 # Admin index.html
60 # ---------------------------------------------------------------------------
61
62 def _sanitize_color(color: str) -> str:
63 """Allow only valid CSS hex colors (#rgb or #rrggbb) to prevent style injection."""
64 if re.fullmatch(r'#[0-9a-fA-F]{3}(?:[0-9a-fA-F]{3})?', color):
65 return color
66 return '#2e3748' # fall back to default
67
68
69 def _write_admin_index(admin_dir: str, branding: dict):
70 import html as html_mod
71 name = html_mod.escape(branding['name'])
72 logo_html = ''
73 if branding['logo']:
74 logo_url = html_mod.escape(branding['logo'])
75 logo_html = f'\n <img src="{logo_url}" alt="{name}" style="max-height:40px;margin:8px 0;">'
76
77 color_css = ''
78 if branding['color']:
79 safe_color = _sanitize_color(branding['color'])
80 color_css = f"""
81 <style>
82 [class^="AppHeader"] {{ background-color: {safe_color} !important; }}
83 </style>"""
84
85 html = f"""<!doctype html>
86 <html>
87 <head>
88 <meta charset="utf-8" />
89 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
90 <meta name="robots" content="noindex" />
91 <title>{name}</title>{color_css}
92 </head>
93 <body>{logo_html}
94 <script src="{DECAP_CDN}"></script>
95 </body>
96 </html>
97 """
98 with open(os.path.join(admin_dir, 'index.html'), 'w') as f:
99 f.write(html)
100
101
102 # ---------------------------------------------------------------------------
103 # config.yml
104 # ---------------------------------------------------------------------------
105
106 def _write_decap_config(site_dir: str, admin_dir: str):
107 content_dir = os.path.join(site_dir, 'content')
108 collections = _build_collections(content_dir)
109
110 config = {
111 'backend': {
112 'name': 'git-gateway',
113 'branch': 'main',
114 },
115 'media_folder': 'static/images/uploads',
116 'public_folder': '/images/uploads',
117 'collections': collections,
118 }
119
120 config_path = os.path.join(admin_dir, 'config.yml')
121 with open(config_path, 'w') as f:
122 yaml.dump(config, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
123
124 logging.info(f"Wrote Decap CMS config to {config_path}")
125
126
127 def _collect_md_files(dirpath: str) -> list:
128 """Recursively collect all .md files under dirpath (excluding _index.md)."""
129 found = []
130 for root, dirs, files in os.walk(dirpath):
131 for f in files:
132 if f.endswith('.md') and f != '_index.md':
133 found.append(os.path.join(root, f))
134 return found
135
136
137 def _build_collections(content_dir: str) -> list:
138 """
139 Inspect content/ to build Decap CMS collections.
140 - Subdirs with any .md files (at any depth) → folder collection (e.g. blog)
141 - Subdirs with only a top-level _index.md → file collection (e.g. about, contact)
142 """
143 if not os.path.isdir(content_dir):
144 return [_default_pages_collection()]
145
146 collections = []
147
148 entries = sorted(os.listdir(content_dir))
149 for entry in entries:
150 subdir = os.path.join(content_dir, entry)
151 if not os.path.isdir(subdir):
152 continue
153
154 # Collect all .md files at any depth (excluding _index.md)
155 non_index = _collect_md_files(subdir)
156 has_index = os.path.exists(os.path.join(subdir, '_index.md'))
157
158 if non_index:
159 # Folder collection (blog, posts, etc.) — use shallowest sample for field inference
160 fields = _infer_fields_for_folder(subdir, [os.path.relpath(f, subdir) for f in non_index])
161 collections.append({
162 'name': entry,
163 'label': entry.replace('-', ' ').title(),
164 'folder': f'content/{entry}',
165 'create': True,
166 'slug': '{{slug}}',
167 'fields': fields,
168 })
169 elif has_index:
170 # File collection (single page)
171 fields = _infer_fields_for_file(os.path.join(subdir, '_index.md'))
172 collections.append({
173 'name': entry,
174 'label': entry.replace('-', ' ').title(),
175 'files': [{
176 'name': entry,
177 'label': entry.replace('-', ' ').title(),
178 'file': f'content/{entry}/_index.md',
179 'fields': fields,
180 }],
181 })
182
183 if not collections:
184 collections.append(_default_pages_collection())
185
186 return collections
187
188
189 def _infer_fields_for_folder(subdir: str, md_files: list) -> list:
190 """Read a sample .md file and extract frontmatter keys as fields."""
191 # md_files may be relative paths (from _collect_md_files); resolve to absolute
192 first = md_files[0]
193 sample = first if os.path.isabs(first) else os.path.join(subdir, first)
194 frontmatter = _parse_frontmatter(sample)
195
196 fields = []
197 field_map = {
198 'title': {'label': 'Title', 'name': 'title', 'widget': 'string'},
199 'date': {'label': 'Date', 'name': 'date', 'widget': 'datetime'},
200 'description': {'label': 'Description', 'name': 'description', 'widget': 'text'},
201 'image': {'label': 'Image', 'name': 'image', 'widget': 'image', 'required': False},
202 'categories': {'label': 'Categories', 'name': 'categories', 'widget': 'list', 'required': False},
203 'tags': {'label': 'Tags', 'name': 'tags', 'widget': 'list', 'required': False},
204 'draft': {'label': 'Draft', 'name': 'draft', 'widget': 'boolean', 'default': False},
205 'author': {'label': 'Author', 'name': 'author', 'widget': 'string', 'required': False},
206 }
207
208 # Add known fields in a logical order
209 for key in ['title', 'date', 'description', 'image', 'categories', 'tags', 'author', 'draft']:
210 if key in frontmatter:
211 fields.append(field_map[key])
212
213 # Add any remaining frontmatter keys not in our map
214 for key, value in frontmatter.items():
215 if key not in field_map and key not in ('type', 'layout', 'url'):
216 widget = _widget_for_value(value)
217 fields.append({'label': key.title(), 'name': key, 'widget': widget, 'required': False})
218
219 # Always include body
220 fields.append({'label': 'Body', 'name': 'body', 'widget': 'markdown'})
221
222 return fields
223
224
225 def _infer_fields_for_file(md_path: str) -> list:
226 """For a single page (_index.md), infer fields from frontmatter."""
227 frontmatter = _parse_frontmatter(md_path)
228 fields = []
229 for key, value in frontmatter.items():
230 widget = _widget_for_value(value)
231 fields.append({'label': key.title(), 'name': key, 'widget': widget, 'required': False})
232 fields.append({'label': 'Body', 'name': 'body', 'widget': 'markdown'})
233 return fields
234
235
236 def _parse_frontmatter(md_path: str) -> dict:
237 """Parse YAML frontmatter from a .md file."""
238 try:
239 with open(md_path, 'r', errors='replace') as f:
240 content = f.read()
241 match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
242 if match:
243 return yaml.safe_load(match.group(1)) or {}
244 except Exception:
245 pass
246 return {}
247
248
249 def _widget_for_value(value) -> str:
250 if isinstance(value, bool):
251 return 'boolean'
252 if isinstance(value, (int, float)):
253 return 'number'
254 if isinstance(value, list):
255 return 'list'
256 return 'string'
257
258
259 def _default_pages_collection() -> dict:
260 return {
261 'name': 'pages',
262 'label': 'Pages',
263 'folder': 'content',
264 'create': True,
265 'slug': '{{slug}}',
266 'fields': [
267 {'label': 'Title', 'name': 'title', 'widget': 'string'},
268 {'label': 'Body', 'name': 'body', 'widget': 'markdown'},
269 ],
270 }
--- a/src/utils/decapify.py
+++ b/src/utils/decapify.py
@@ -1,270 +0,0 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
D src/utils/deploy.py
-27
--- a/src/utils/deploy.py
+++ b/src/utils/deploy.py
@@ -1,27 +0,0 @@
1
-"""
2
-This script handles the deployment of a Hugo site, ensuring all prerequisites are met and executing the deployment process.
3
-It may use Cloudflare functions for deployment.
4
-"""
5
-
6
-import logging
7
-
8
-# Function to handle deployment tasks
9
-def deploy(path, zone):
10
- logging.info(f"Starting deployment for {path} to zone {zone}...")
11
- try:
12
- # Check prerequisites
13
- logging.info("Checking prerequisites...")
14
- # Example check for necessary files or configurations
15
- # if not os.path.exists(os.path.join(path, 'config.toml')):
16
- # raise FileNotFoundError("Missing config.toml")
17
-
18
- # Deploy site using Cloudflare functions
19
- logging.info("Deploying site...")
20
- # Example API call to deploy the site
21
- # cloudflare_api.deploy_site(path, zone)
22
-
23
- logging.info("Deployment complete.")
24
- return "Deployment complete"
25
- except Exception as e:
26
- logging.error(f"Error during deployment: {e}")
27
- return "Deployment failed"
--- a/src/utils/deploy.py
+++ b/src/utils/deploy.py
@@ -1,27 +0,0 @@
1 """
2 This script handles the deployment of a Hugo site, ensuring all prerequisites are met and executing the deployment process.
3 It may use Cloudflare functions for deployment.
4 """
5
6 import logging
7
8 # Function to handle deployment tasks
9 def deploy(path, zone):
10 logging.info(f"Starting deployment for {path} to zone {zone}...")
11 try:
12 # Check prerequisites
13 logging.info("Checking prerequisites...")
14 # Example check for necessary files or configurations
15 # if not os.path.exists(os.path.join(path, 'config.toml')):
16 # raise FileNotFoundError("Missing config.toml")
17
18 # Deploy site using Cloudflare functions
19 logging.info("Deploying site...")
20 # Example API call to deploy the site
21 # cloudflare_api.deploy_site(path, zone)
22
23 logging.info("Deployment complete.")
24 return "Deployment complete"
25 except Exception as e:
26 logging.error(f"Error during deployment: {e}")
27 return "Deployment failed"
--- a/src/utils/deploy.py
+++ b/src/utils/deploy.py
@@ -1,27 +0,0 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
D src/utils/generate_decap_config.py
-10
--- a/src/utils/generate_decap_config.py
+++ b/src/utils/generate_decap_config.py
@@ -1,10 +0,0 @@
1
-"""
2
-generate_decap_config — thin wrapper kept for backwards compatibility.
3
-The real implementation lives in decapify.py.
4
-"""
5
-
6
-from utils.decapify import decapify
7
-
8
-
9
-def generate_decap_config(theme_path: str) -> str:
10
- return decapify(theme_path)
--- a/src/utils/generate_decap_config.py
+++ b/src/utils/generate_decap_config.py
@@ -1,10 +0,0 @@
1 """
2 generate_decap_config — thin wrapper kept for backwards compatibility.
3 The real implementation lives in decapify.py.
4 """
5
6 from utils.decapify import decapify
7
8
9 def generate_decap_config(theme_path: str) -> str:
10 return decapify(theme_path)
--- a/src/utils/generate_decap_config.py
+++ b/src/utils/generate_decap_config.py
@@ -1,10 +0,0 @@
 
 
 
 
 
 
 
 
 
 
D src/utils/hugoify.py
-165
--- a/src/utils/hugoify.py
+++ b/src/utils/hugoify.py
@@ -1,165 +0,0 @@
1
-"""
2
-AI-powered HTML → Hugo template conversion.
3
-
4
-For already-Hugo themes, use hugoify_dir() to validate/augment.
5
-For raw HTML, use hugoify_html() to produce Hugo layout files.
6
-"""
7
-
8
-import logging
9
-import os
10
-import json
11
-import re
12
-
13
-from config import call_ai
14
-
15
-SYSTEM = """You are an expert Hugo theme developer. Convert HTML templates to valid Hugo Go template files.
16
-Output only valid Hugo template syntax — no explanations, no markdown fences."""
17
-
18
-
19
-def hugoify_html(html_path: str) -> dict:
20
- """
21
- Convert a raw HTML file to a set of Hugo layout files.
22
-
23
- Returns dict mapping relative layout paths to their content, e.g.:
24
- {
25
- "_default/baseof.html": "<!DOCTYPE html>...",
26
- "partials/header.html": "<header>...",
27
- "partials/footer.html": "<footer>...",
28
- "index.html": "{{ define \"main\" }}...",
29
- }
30
- """
31
- logging.info(f"Hugoifying {html_path} ...")
32
-
33
- with open(html_path, 'r', errors='replace') as f:
34
- html = f.read()
35
-
36
- # Truncate very large files to avoid token limits
37
- if len(html) > 30000:
38
- logging.warning(f"HTML is large ({len(html)} chars), truncating to 30000 for AI analysis")
39
- html = html[:30000]
40
-
41
- prompt = f"""Convert the following HTML file into Hugo layout files.
42
-
43
-Return a JSON object where keys are relative file paths under layouts/ and values are the Hugo template content.
44
-
45
-Required keys to produce:
46
-- "_default/baseof.html" — base template with blocks for head, header, main, footer
47
-- "partials/header.html" — site header/nav extracted as partial
48
-- "partials/footer.html" — footer extracted as partial
49
-- "index.html" — homepage using {{ define "main" }} ... {{ end }}
50
-
51
-Rules:
52
-- Replace hardcoded page titles with {{ .Title }}
53
-- Replace hardcoded site name with {{ .Site.Title }}
54
-- Replace hardcoded URLs with {{ .Site.BaseURL }} or {{ .Permalink }}
55
-- Replace nav links with {{ range .Site.Menus.main }}<a href="{{ .URL }}">{{ .Name }}</a>{{ end }}
56
-- Replace blog post lists with {{ range .Pages }} ... {{ end }}
57
-- Replace copyright year with {{ now.Year }}
58
-- Keep all CSS classes and HTML structure intact
59
-- Use {{ partial "header.html" . }} and {{ partial "footer.html" . }} in baseof.html
60
-
61
-HTML to convert:
62
-{html}
63
-
64
-Return ONLY a valid JSON object, no explanation."""
65
-
66
- response = call_ai(prompt, SYSTEM)
67
- return _parse_layout_json(response)
68
-
69
-
70
-def hugoify_dir(theme_dir: str) -> str:
71
- """
72
- Validate and optionally augment an existing Hugo theme directory.
73
- Returns a status message.
74
- """
75
- logging.info(f"Validating Hugo theme at {theme_dir} ...")
76
-
77
- issues = []
78
- layouts_dir = os.path.join(theme_dir, 'layouts')
79
-
80
- if not os.path.isdir(layouts_dir):
81
- issues.append("Missing layouts/ directory")
82
- return f"Validation failed: {'; '.join(issues)}"
83
-
84
- required = [
85
- os.path.join(layouts_dir, '_default', 'baseof.html'),
86
- ]
87
- for f in required:
88
- if not os.path.exists(f):
89
- issues.append(f"Missing {os.path.relpath(f, theme_dir)}")
90
-
91
- if issues:
92
- logging.warning(f"Issues found: {issues}")
93
- return f"Issues: {'; '.join(issues)}"
94
-
95
- logging.info("Hugo theme validation passed.")
96
- return "Valid Hugo theme"
97
-
98
-
99
-# CLI entry point (used by cli.py)
100
-def hugoify(path: str) -> str:
101
- """
102
- Entry point for the CLI 'hugoify' command.
103
- If path is a Hugo theme dir: validate it.
104
- If path is an HTML file or raw HTML dir: convert it.
105
- """
106
- from utils.theme_finder import find_hugo_theme, find_raw_html_files
107
-
108
- info = find_hugo_theme(path)
109
- if info:
110
- return hugoify_dir(info['theme_dir'])
111
-
112
- if os.path.isfile(path) and path.endswith('.html'):
113
- layouts = hugoify_html(path)
114
- return f"Converted to {len(layouts)} layout files: {list(layouts.keys())}"
115
-
116
- html_files = find_raw_html_files(path)
117
- if html_files:
118
- main = next(
119
- (f for f in html_files if os.path.basename(f).lower() == 'index.html'),
120
- html_files[0]
121
- )
122
- layouts = hugoify_html(main)
123
- return f"Converted to {len(layouts)} layout files"
124
-
125
- return f"Nothing to hugoify at {path}"
126
-
127
-
128
-# ---------------------------------------------------------------------------
129
-# Helpers
130
-# ---------------------------------------------------------------------------
131
-
132
-def _parse_layout_json(response: str) -> dict:
133
- """Extract JSON from AI response, even if surrounded by prose."""
134
- # Try to find JSON block
135
- match = re.search(r'\{.*\}', response, re.DOTALL)
136
- if match:
137
- try:
138
- return json.loads(match.group(0))
139
- except json.JSONDecodeError:
140
- pass
141
-
142
- # Fallback: return a minimal layout
143
- logging.warning("Could not parse AI response as JSON, using fallback layouts")
144
- return {
145
- "_default/baseof.html": _fallback_baseof(),
146
- "partials/header.html": "<header><!-- header --></header>",
147
- "partials/footer.html": "<footer>{{ .Site.Params.copyright }}</footer>",
148
- "index.html": '{{ define "main" }}<main>{{ .Content }}</main>{{ end }}',
149
- }
150
-
151
-
152
-def _fallback_baseof() -> str:
153
- return '''<!DOCTYPE html>
154
-<html lang="{{ with .Site.LanguageCode }}{{ . }}{{ else }}en-US{{ end }}">
155
-<head>
156
- <meta charset="UTF-8">
157
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
158
- <title>{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} | {{ .Site.Title }}{{ end }}</title>
159
-</head>
160
-<body>
161
- {{- partial "header.html" . -}}
162
- {{- block "main" . }}{{- end }}
163
- {{- partial "footer.html" . -}}
164
-</body>
165
-</html>'''
--- a/src/utils/hugoify.py
+++ b/src/utils/hugoify.py
@@ -1,165 +0,0 @@
1 """
2 AI-powered HTML → Hugo template conversion.
3
4 For already-Hugo themes, use hugoify_dir() to validate/augment.
5 For raw HTML, use hugoify_html() to produce Hugo layout files.
6 """
7
8 import logging
9 import os
10 import json
11 import re
12
13 from config import call_ai
14
15 SYSTEM = """You are an expert Hugo theme developer. Convert HTML templates to valid Hugo Go template files.
16 Output only valid Hugo template syntax — no explanations, no markdown fences."""
17
18
19 def hugoify_html(html_path: str) -> dict:
20 """
21 Convert a raw HTML file to a set of Hugo layout files.
22
23 Returns dict mapping relative layout paths to their content, e.g.:
24 {
25 "_default/baseof.html": "<!DOCTYPE html>...",
26 "partials/header.html": "<header>...",
27 "partials/footer.html": "<footer>...",
28 "index.html": "{{ define \"main\" }}...",
29 }
30 """
31 logging.info(f"Hugoifying {html_path} ...")
32
33 with open(html_path, 'r', errors='replace') as f:
34 html = f.read()
35
36 # Truncate very large files to avoid token limits
37 if len(html) > 30000:
38 logging.warning(f"HTML is large ({len(html)} chars), truncating to 30000 for AI analysis")
39 html = html[:30000]
40
41 prompt = f"""Convert the following HTML file into Hugo layout files.
42
43 Return a JSON object where keys are relative file paths under layouts/ and values are the Hugo template content.
44
45 Required keys to produce:
46 - "_default/baseof.html" — base template with blocks for head, header, main, footer
47 - "partials/header.html" — site header/nav extracted as partial
48 - "partials/footer.html" — footer extracted as partial
49 - "index.html" — homepage using {{ define "main" }} ... {{ end }}
50
51 Rules:
52 - Replace hardcoded page titles with {{ .Title }}
53 - Replace hardcoded site name with {{ .Site.Title }}
54 - Replace hardcoded URLs with {{ .Site.BaseURL }} or {{ .Permalink }}
55 - Replace nav links with {{ range .Site.Menus.main }}<a href="{{ .URL }}">{{ .Name }}</a>{{ end }}
56 - Replace blog post lists with {{ range .Pages }} ... {{ end }}
57 - Replace copyright year with {{ now.Year }}
58 - Keep all CSS classes and HTML structure intact
59 - Use {{ partial "header.html" . }} and {{ partial "footer.html" . }} in baseof.html
60
61 HTML to convert:
62 {html}
63
64 Return ONLY a valid JSON object, no explanation."""
65
66 response = call_ai(prompt, SYSTEM)
67 return _parse_layout_json(response)
68
69
70 def hugoify_dir(theme_dir: str) -> str:
71 """
72 Validate and optionally augment an existing Hugo theme directory.
73 Returns a status message.
74 """
75 logging.info(f"Validating Hugo theme at {theme_dir} ...")
76
77 issues = []
78 layouts_dir = os.path.join(theme_dir, 'layouts')
79
80 if not os.path.isdir(layouts_dir):
81 issues.append("Missing layouts/ directory")
82 return f"Validation failed: {'; '.join(issues)}"
83
84 required = [
85 os.path.join(layouts_dir, '_default', 'baseof.html'),
86 ]
87 for f in required:
88 if not os.path.exists(f):
89 issues.append(f"Missing {os.path.relpath(f, theme_dir)}")
90
91 if issues:
92 logging.warning(f"Issues found: {issues}")
93 return f"Issues: {'; '.join(issues)}"
94
95 logging.info("Hugo theme validation passed.")
96 return "Valid Hugo theme"
97
98
99 # CLI entry point (used by cli.py)
100 def hugoify(path: str) -> str:
101 """
102 Entry point for the CLI 'hugoify' command.
103 If path is a Hugo theme dir: validate it.
104 If path is an HTML file or raw HTML dir: convert it.
105 """
106 from utils.theme_finder import find_hugo_theme, find_raw_html_files
107
108 info = find_hugo_theme(path)
109 if info:
110 return hugoify_dir(info['theme_dir'])
111
112 if os.path.isfile(path) and path.endswith('.html'):
113 layouts = hugoify_html(path)
114 return f"Converted to {len(layouts)} layout files: {list(layouts.keys())}"
115
116 html_files = find_raw_html_files(path)
117 if html_files:
118 main = next(
119 (f for f in html_files if os.path.basename(f).lower() == 'index.html'),
120 html_files[0]
121 )
122 layouts = hugoify_html(main)
123 return f"Converted to {len(layouts)} layout files"
124
125 return f"Nothing to hugoify at {path}"
126
127
128 # ---------------------------------------------------------------------------
129 # Helpers
130 # ---------------------------------------------------------------------------
131
132 def _parse_layout_json(response: str) -> dict:
133 """Extract JSON from AI response, even if surrounded by prose."""
134 # Try to find JSON block
135 match = re.search(r'\{.*\}', response, re.DOTALL)
136 if match:
137 try:
138 return json.loads(match.group(0))
139 except json.JSONDecodeError:
140 pass
141
142 # Fallback: return a minimal layout
143 logging.warning("Could not parse AI response as JSON, using fallback layouts")
144 return {
145 "_default/baseof.html": _fallback_baseof(),
146 "partials/header.html": "<header><!-- header --></header>",
147 "partials/footer.html": "<footer>{{ .Site.Params.copyright }}</footer>",
148 "index.html": '{{ define "main" }}<main>{{ .Content }}</main>{{ end }}',
149 }
150
151
152 def _fallback_baseof() -> str:
153 return '''<!DOCTYPE html>
154 <html lang="{{ with .Site.LanguageCode }}{{ . }}{{ else }}en-US{{ end }}">
155 <head>
156 <meta charset="UTF-8">
157 <meta name="viewport" content="width=device-width, initial-scale=1.0">
158 <title>{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} | {{ .Site.Title }}{{ end }}</title>
159 </head>
160 <body>
161 {{- partial "header.html" . -}}
162 {{- block "main" . }}{{- end }}
163 {{- partial "footer.html" . -}}
164 </body>
165 </html>'''
--- a/src/utils/hugoify.py
+++ b/src/utils/hugoify.py
@@ -1,165 +0,0 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
D src/utils/parser.py
-31
--- a/src/utils/parser.py
+++ b/src/utils/parser.py
@@ -1,31 +0,0 @@
1
-"""
2
-This script performs parsing, linting, and validation of web content to ensure it adheres to best practices and standards.
3
-It checks for syntax errors and validates the structure of the content.
4
-"""
5
-
6
-import logging
7
-
8
-# Function to perform parsing, linting, and validation
9
-def parse(path):
10
- logging.info(f"Starting parsing and linting for {path}...")
11
- try:
12
- # Parse input
13
- logging.info("Parsing input...")
14
- # Example parsing logic
15
- # parse_html_structure(path)
16
-
17
- # Lint code
18
- logging.info("Linting code...")
19
- # Example linting logic
20
- # lint_html_css_js(path)
21
-
22
- # Validate structure
23
- logging.info("Validating structure...")
24
- # Example validation logic
25
- # validate_html_structure(path)
26
-
27
- logging.info("Parsing complete.")
28
- return "Parsing complete"
29
- except Exception as e:
30
- logging.error(f"Error during parsing: {e}")
31
- return "Parsing failed"
--- a/src/utils/parser.py
+++ b/src/utils/parser.py
@@ -1,31 +0,0 @@
1 """
2 This script performs parsing, linting, and validation of web content to ensure it adheres to best practices and standards.
3 It checks for syntax errors and validates the structure of the content.
4 """
5
6 import logging
7
8 # Function to perform parsing, linting, and validation
9 def parse(path):
10 logging.info(f"Starting parsing and linting for {path}...")
11 try:
12 # Parse input
13 logging.info("Parsing input...")
14 # Example parsing logic
15 # parse_html_structure(path)
16
17 # Lint code
18 logging.info("Linting code...")
19 # Example linting logic
20 # lint_html_css_js(path)
21
22 # Validate structure
23 logging.info("Validating structure...")
24 # Example validation logic
25 # validate_html_structure(path)
26
27 logging.info("Parsing complete.")
28 return "Parsing complete"
29 except Exception as e:
30 logging.error(f"Error during parsing: {e}")
31 return "Parsing failed"
--- a/src/utils/parser.py
+++ b/src/utils/parser.py
@@ -1,31 +0,0 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
D src/utils/theme_finder.py
-74
--- a/src/utils/theme_finder.py
+++ b/src/utils/theme_finder.py
@@ -1,74 +0,0 @@
1
-"""
2
-Locates the actual Hugo theme and exampleSite within the messy zip-extracted structure.
3
-Themes in themes/ are structured as: {name}/{name}/themes/{theme-name}/
4
-"""
5
-
6
-import }/
7
-"""
8
-
9
-import json
10
-import logging
11
-import os
12
-
13
-
14
-def find_hugo_theme(input_path):
15
- """
16
- Given a path like themes/revolve-hugo or the inner extracted dir,
17
- find the Hugo theme directory (has layouts/), the exampleSite, and the theme name.
18
-
19
- Returns dict with:
20
- theme_dir: path to the Hugo theme (has layouts/, archetypes/, etc.)
21
- example_site: path to exampleSite dir (may be None)
22
- theme_name: name of the theme (used in hugo.toml)
23
- is_hugo_theme: True if input is already a Hugo theme
24
- """
25
- input_path = os.path.abspath(input_path)
26
-
27
- # Walk up to find the theme dir containing layouts/
28
- candidates = []
29
- for root, dirs, files in os.walk(input_path):
30
- # Skip __MACOSX junk
31
- if '__MACOSX' in root:
32
- continue
33
- if 'layouts' in dirs and '_default' in os.listdir(os.path.join(root, 'layouts')):
34
- candidates.append(root)
35
-
36
- if not candidates:
37
- return None
38
-
39
- # Pick the deepest match (most likely the actual theme dir)
40
- theme_dir = max(candidates, key=lambda p: p.count(os.sep))
41
- if len(candidates) > 1:
42
- logging.warning(
43
- f"Multiple Hugo theme candidates found; using {theme_dir!r}. "
44
- f"Others: {[c for c in candidates if c != theme_dir]}"
45
- )
46
-
47
- # Detect exampleSite
48
- example_site = None
49
- for candidate in [
50
- os.path.join(theme_dir, 'exampleSite'),
51
- os.path.join(os.path.dirname(theme_dir), 'exampleSite'),
52
- ]:
53
- if os.path.isdir(candidate):
54
- example_site = candidate
55
- break
56
-
57
- theme_name = os.path.basename(theme_dir)
58
-
59
- return {
60
- 'theme_dir': theme_dir,
61
- 'example_site': example_site,
62
- 'theme_name': theme_name,
63
- 'is_hugo_them return None
64
-
65
-
66
-def find_raw_html_files(input_path):
67
- """Find HTML files in a raw HTML theme (not a Hugo theme)."""
68
- html_files = []
69
- for root, dirs, files in os.walk(input_path):
70
- if '__MACOSX' in root:
71
- continue
72
- for f in files:
73
- if f.endswith('.html') and 'exampleSite' not in root:
74
- html_files.append(os.path.join(root,
--- a/src/utils/theme_finder.py
+++ b/src/utils/theme_finder.py
@@ -1,74 +0,0 @@
1 """
2 Locates the actual Hugo theme and exampleSite within the messy zip-extracted structure.
3 Themes in themes/ are structured as: {name}/{name}/themes/{theme-name}/
4 """
5
6 import }/
7 """
8
9 import json
10 import logging
11 import os
12
13
14 def find_hugo_theme(input_path):
15 """
16 Given a path like themes/revolve-hugo or the inner extracted dir,
17 find the Hugo theme directory (has layouts/), the exampleSite, and the theme name.
18
19 Returns dict with:
20 theme_dir: path to the Hugo theme (has layouts/, archetypes/, etc.)
21 example_site: path to exampleSite dir (may be None)
22 theme_name: name of the theme (used in hugo.toml)
23 is_hugo_theme: True if input is already a Hugo theme
24 """
25 input_path = os.path.abspath(input_path)
26
27 # Walk up to find the theme dir containing layouts/
28 candidates = []
29 for root, dirs, files in os.walk(input_path):
30 # Skip __MACOSX junk
31 if '__MACOSX' in root:
32 continue
33 if 'layouts' in dirs and '_default' in os.listdir(os.path.join(root, 'layouts')):
34 candidates.append(root)
35
36 if not candidates:
37 return None
38
39 # Pick the deepest match (most likely the actual theme dir)
40 theme_dir = max(candidates, key=lambda p: p.count(os.sep))
41 if len(candidates) > 1:
42 logging.warning(
43 f"Multiple Hugo theme candidates found; using {theme_dir!r}. "
44 f"Others: {[c for c in candidates if c != theme_dir]}"
45 )
46
47 # Detect exampleSite
48 example_site = None
49 for candidate in [
50 os.path.join(theme_dir, 'exampleSite'),
51 os.path.join(os.path.dirname(theme_dir), 'exampleSite'),
52 ]:
53 if os.path.isdir(candidate):
54 example_site = candidate
55 break
56
57 theme_name = os.path.basename(theme_dir)
58
59 return {
60 'theme_dir': theme_dir,
61 'example_site': example_site,
62 'theme_name': theme_name,
63 'is_hugo_them return None
64
65
66 def find_raw_html_files(input_path):
67 """Find HTML files in a raw HTML theme (not a Hugo theme)."""
68 html_files = []
69 for root, dirs, files in os.walk(input_path):
70 if '__MACOSX' in root:
71 continue
72 for f in files:
73 if f.endswith('.html') and 'exampleSite' not in root:
74 html_files.append(os.path.join(root,
--- a/src/utils/theme_finder.py
+++ b/src/utils/theme_finder.py
@@ -1,74 +0,0 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
D src/utils/theme_patcher.py
-72
--- a/src/utils/theme_patcher.py
+++ b/src/utils/theme_patcher.py
@@ -1,72 +0,0 @@
1
-"""
2
-Patches common Hugo deprecations in theme layout files so they work with Hugo >= v0.128.
3
-
4
-Call patch_theme(theme_dir) after copying theme files to the output directory.
5
-"""
6
-
7
-import logging
8
-import os
9
-import re
10
-
11
-# Map of (pattern, replacement) for deprecated Hugo template variables/functions
12
-TEMPLATE_PATCHES = [
13
- # .Site.DisqusShortname → .Site.Config.Services.Disqus.Shortname
14
- (r'\.Site\.DisqusShortname', '.Site.Config.Services.Disqus.Shortname'),
15
- # .Site.GoogleAnalytics → .Site.Config.Services.GoogleAnalytics.ID
16
- (r'\.Site\.GoogleAnalytics\b', '.Site.Config.Services.GoogleAnalytics.ID'),
17
- # absLangURL → absLangURL still works but absURL is preferred for simple cases
18
- # safeHTMLAttr is fine, no change needed
19
-]
20
-
21
-# Config key patches: (old_pattern, replacement)
22
-CONFIG_PATCHES = [
23
- # paginate → [pagination] pagerSize
24
- (r'^paginate\s*=\s*(\d+)$', r'[pagination]\n pagerSize = \1'),
25
- # googleAnalytics = "UA-xxx" → [services.googleAnalytics] id = "UA-xxx"
26
- (r'^googleAnalytics\s*=\s*"([^"]+)"', r'[services.googleAnalytics]\n id = "\1"'),
27
- # disqusShortname = "xxx" → [services.disqus] shortname = "xxx"
28
- (r'^disqusShortname\s*=\s*"([^"]+)"', r'[services.disqus]\n shortname = "\1"'),
29
-]
30
-
31
-
32
-def patch_theme(theme_dir: str):
33
- """Patch deprecated Hugo APIs in all layout files under theme_dir/layouts/."""
34
- layouts_dir = os.path.join(theme_dir, 'layouts')
35
- if not os.path.isdir(layouts_dir):
36
- return
37
-
38
- patched = 0
39
- for root, dirs, files in os.walk(layouts_dir):
40
- for fname in files:
41
- if not fname.endswith('.html'):
42
- continue
43
- path = os.path.join(root, fname)
44
- with open(path, 'r', errors='replace') as f:
45
- content = f.read()
46
-
47
- new_content = content
48
- for pattern, replacement in TEMPLATE_PATCHES:
49
- new_content = re.sub(pattern, replacement, new_content)
50
-
51
- if new_content != content:
52
- with open(path, 'w') as f:
53
- f.write(new_content)
54
- patched += 1
55
-
56
- if patched:
57
- logging.info(f"Patched {patched} template file(s) in {theme_dir}")
58
-
59
-
60
-def patch_config(config_path: str):
61
- """Patch deprecated keys in a hugo.toml / config.toml file."""
62
- with open(config_path, 'r') as f:
63
- content = f.read()
64
-
65
- new_content = content
66
- for pattern, replacement in CONFIG_PATCHES:
67
- new_content = re.sub(pattern, replacement, new_content, flags=re.MULTILINE)
68
-
69
- if new_content != content:
70
- with open(config_path, 'w') as f:
71
- f.write(new_content)
72
- logging.info(f"Patched deprecated config keys in {config_path}")
--- a/src/utils/theme_patcher.py
+++ b/src/utils/theme_patcher.py
@@ -1,72 +0,0 @@
1 """
2 Patches common Hugo deprecations in theme layout files so they work with Hugo >= v0.128.
3
4 Call patch_theme(theme_dir) after copying theme files to the output directory.
5 """
6
7 import logging
8 import os
9 import re
10
11 # Map of (pattern, replacement) for deprecated Hugo template variables/functions
12 TEMPLATE_PATCHES = [
13 # .Site.DisqusShortname → .Site.Config.Services.Disqus.Shortname
14 (r'\.Site\.DisqusShortname', '.Site.Config.Services.Disqus.Shortname'),
15 # .Site.GoogleAnalytics → .Site.Config.Services.GoogleAnalytics.ID
16 (r'\.Site\.GoogleAnalytics\b', '.Site.Config.Services.GoogleAnalytics.ID'),
17 # absLangURL → absLangURL still works but absURL is preferred for simple cases
18 # safeHTMLAttr is fine, no change needed
19 ]
20
21 # Config key patches: (old_pattern, replacement)
22 CONFIG_PATCHES = [
23 # paginate → [pagination] pagerSize
24 (r'^paginate\s*=\s*(\d+)$', r'[pagination]\n pagerSize = \1'),
25 # googleAnalytics = "UA-xxx" → [services.googleAnalytics] id = "UA-xxx"
26 (r'^googleAnalytics\s*=\s*"([^"]+)"', r'[services.googleAnalytics]\n id = "\1"'),
27 # disqusShortname = "xxx" → [services.disqus] shortname = "xxx"
28 (r'^disqusShortname\s*=\s*"([^"]+)"', r'[services.disqus]\n shortname = "\1"'),
29 ]
30
31
32 def patch_theme(theme_dir: str):
33 """Patch deprecated Hugo APIs in all layout files under theme_dir/layouts/."""
34 layouts_dir = os.path.join(theme_dir, 'layouts')
35 if not os.path.isdir(layouts_dir):
36 return
37
38 patched = 0
39 for root, dirs, files in os.walk(layouts_dir):
40 for fname in files:
41 if not fname.endswith('.html'):
42 continue
43 path = os.path.join(root, fname)
44 with open(path, 'r', errors='replace') as f:
45 content = f.read()
46
47 new_content = content
48 for pattern, replacement in TEMPLATE_PATCHES:
49 new_content = re.sub(pattern, replacement, new_content)
50
51 if new_content != content:
52 with open(path, 'w') as f:
53 f.write(new_content)
54 patched += 1
55
56 if patched:
57 logging.info(f"Patched {patched} template file(s) in {theme_dir}")
58
59
60 def patch_config(config_path: str):
61 """Patch deprecated keys in a hugo.toml / config.toml file."""
62 with open(config_path, 'r') as f:
63 content = f.read()
64
65 new_content = content
66 for pattern, replacement in CONFIG_PATCHES:
67 new_content = re.sub(pattern, replacement, new_content, flags=re.MULTILINE)
68
69 if new_content != content:
70 with open(config_path, 'w') as f:
71 f.write(new_content)
72 logging.info(f"Patched deprecated config keys in {config_path}")
--- a/src/utils/theme_patcher.py
+++ b/src/utils/theme_patcher.py
@@ -1,72 +0,0 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
D src/utils/translate.py
-24
--- a/src/utils/translate.py
+++ b/src/utils/translate.py
@@ -1,24 +0,0 @@
1
-"""
2
-Translates web content using the configured AI backend.
3
-"""
4
-
5
-import logging
6
-from config import call_ai
7
-
8
-
9
-def translate(path: str, target_language: str = "Spanish") -> str:
10
- logging.info(f"Translating content in {path} ...")
11
- try:
12
- with open(path, 'r', errors='replace') as f:
13
- content = f.read()
14
-
15
- prompt = f"""Translate the following web content to {target_language}.
16
-Preserve all HTML tags and formatting. Only translate visible text.
17
-
18
-Content:
19
-{content[:20000]}"""
20
-
21
- return call_ai(prompt, "You are a professional translator.")
22
- except Exception as e:
23
- logging.error(f"Translation failed: {e}")
24
- return f"Translation failed: {e}"
--- a/src/utils/translate.py
+++ b/src/utils/translate.py
@@ -1,24 +0,0 @@
1 """
2 Translates web content using the configured AI backend.
3 """
4
5 import logging
6 from config import call_ai
7
8
9 def translate(path: str, target_language: str = "Spanish") -> str:
10 logging.info(f"Translating content in {path} ...")
11 try:
12 with open(path, 'r', errors='replace') as f:
13 content = f.read()
14
15 prompt = f"""Translate the following web content to {target_language}.
16 Preserve all HTML tags and formatting. Only translate visible text.
17
18 Content:
19 {content[:20000]}"""
20
21 return call_ai(prompt, "You are a professional translator.")
22 except Exception as e:
23 logging.error(f"Translation failed: {e}")
24 return f"Translation failed: {e}"
--- a/src/utils/translate.py
+++ b/src/utils/translate.py
@@ -1,24 +0,0 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- tests/conftest.py
+++ tests/conftest.py
@@ -1,5 +1,5 @@
1
-"""Add src/ to sys.path so tests can import from utils.* directly."""
2
-import sys
1
+"""Add repo root to sys.path so tests can import hugoifier.*."""
32
import os
3
+import sys
44
5
-sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
5
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
66
--- tests/conftest.py
+++ tests/conftest.py
@@ -1,5 +1,5 @@
1 """Add src/ to sys.path so tests can import from utils.* directly."""
2 import sys
3 import os
 
4
5 sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
6
--- tests/conftest.py
+++ tests/conftest.py
@@ -1,5 +1,5 @@
1 """Add repo root to sys.path so tests can import hugoifier.*."""
 
2 import os
3 import sys
4
5 sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
6
--- tests/test_analyze.py
+++ tests/test_analyze.py
@@ -1,11 +1,11 @@
11
"""Tests for utils.analyze."""
22
import os
33
import tempfile
44
import unittest
55
6
-from utils.analyze import _analyze_hugo_theme
6
+from hugoifier.utils.analyze import _analyze_hugo_theme
77
88
99
class TestAnalyzeHugoTheme(unittest.TestCase):
1010
def _make_theme_info(self, tmp, layouts=None, example_site=None):
1111
theme_dir = os.path.join(tmp, "test-theme")
1212
--- tests/test_analyze.py
+++ tests/test_analyze.py
@@ -1,11 +1,11 @@
1 """Tests for utils.analyze."""
2 import os
3 import tempfile
4 import unittest
5
6 from utils.analyze import _analyze_hugo_theme
7
8
9 class TestAnalyzeHugoTheme(unittest.TestCase):
10 def _make_theme_info(self, tmp, layouts=None, example_site=None):
11 theme_dir = os.path.join(tmp, "test-theme")
12
--- tests/test_analyze.py
+++ tests/test_analyze.py
@@ -1,11 +1,11 @@
1 """Tests for utils.analyze."""
2 import os
3 import tempfile
4 import unittest
5
6 from hugoifier.utils.analyze import _analyze_hugo_theme
7
8
9 class TestAnalyzeHugoTheme(unittest.TestCase):
10 def _make_theme_info(self, tmp, layouts=None, example_site=None):
11 theme_dir = os.path.join(tmp, "test-theme")
12
--- tests/test_cloudflare.py
+++ tests/test_cloudflare.py
@@ -1,9 +1,9 @@
11
"""Tests for utils.cloudflare."""
22
import unittest
33
4
-from utils.cloudflare import configure_cloudflare
4
+from hugoifier.utils.cloudflare import configure_cloudflare
55
66
77
class TestConfigureCloudflare(unittest.TestCase):
88
def test_returns_complete_message(self):
99
result = configure_cloudflare("/some/path", "example.com")
1010
--- tests/test_cloudflare.py
+++ tests/test_cloudflare.py
@@ -1,9 +1,9 @@
1 """Tests for utils.cloudflare."""
2 import unittest
3
4 from utils.cloudflare import configure_cloudflare
5
6
7 class TestConfigureCloudflare(unittest.TestCase):
8 def test_returns_complete_message(self):
9 result = configure_cloudflare("/some/path", "example.com")
10
--- tests/test_cloudflare.py
+++ tests/test_cloudflare.py
@@ -1,9 +1,9 @@
1 """Tests for utils.cloudflare."""
2 import unittest
3
4 from hugoifier.utils.cloudflare import configure_cloudflare
5
6
7 class TestConfigureCloudflare(unittest.TestCase):
8 def test_returns_complete_message(self):
9 result = configure_cloudflare("/some/path", "example.com")
10
--- tests/test_complete.py
+++ tests/test_complete.py
@@ -1,11 +1,16 @@
11
"""Tests for utils.complete helpers."""
22
import os
33
import tempfile
44
import unittest
55
6
-from utils.complete import _pick_main_html, _copy_dir, _find_config, _write_minimal_hugo_toml
6
+from hugoifier.utils.complete import (
7
+ _copy_dir,
8
+ _find_config,
9
+ _pick_main_html,
10
+ _write_minimal_hugo_toml,
11
+)
712
813
914
class TestPickMainHtml(unittest.TestCase):
1015
def test_prefers_index_html(self):
1116
files = ["/path/about.html", "/path/index.html", "/path/contact.html"]
1217
--- tests/test_complete.py
+++ tests/test_complete.py
@@ -1,11 +1,16 @@
1 """Tests for utils.complete helpers."""
2 import os
3 import tempfile
4 import unittest
5
6 from utils.complete import _pick_main_html, _copy_dir, _find_config, _write_minimal_hugo_toml
 
 
 
 
 
7
8
9 class TestPickMainHtml(unittest.TestCase):
10 def test_prefers_index_html(self):
11 files = ["/path/about.html", "/path/index.html", "/path/contact.html"]
12
--- tests/test_complete.py
+++ tests/test_complete.py
@@ -1,11 +1,16 @@
1 """Tests for utils.complete helpers."""
2 import os
3 import tempfile
4 import unittest
5
6 from hugoifier.utils.complete import (
7 _copy_dir,
8 _find_config,
9 _pick_main_html,
10 _write_minimal_hugo_toml,
11 )
12
13
14 class TestPickMainHtml(unittest.TestCase):
15 def test_prefers_index_html(self):
16 files = ["/path/about.html", "/path/index.html", "/path/contact.html"]
17
--- tests/test_config.py
+++ tests/test_config.py
@@ -6,11 +6,12 @@
66
77
class TestCallAiRouting(unittest.TestCase):
88
def _get_config(self):
99
# Reload config to pick up env var changes
1010
import importlib
11
- import config
11
+
12
+ import hugoifier.config as config
1213
importlib.reload(config)
1314
return config
1415
1516
def test_raises_on_unknown_backend(self):
1617
with patch.dict(os.environ, {"HUGOIFIER_BACKEND": "unknown"}):
@@ -41,31 +42,31 @@
4142
cfg.GOOGLE_API_KEY = ""
4243
with self.assertRaises(EnvironmentError):
4344
cfg._call_google("prompt", "system")
4445
4546
def test_anthropic_backend_calls_anthropic(self):
46
- import config
47
+ import hugoifier.config as config
4748
with patch.object(config, '_call_anthropic', return_value="response") as mock_fn:
4849
config.BACKEND = 'anthropic'
4950
result = config.call_ai("hello")
5051
mock_fn.assert_called_once()
5152
self.assertEqual(result, "response")
5253
5354
def test_openai_backend_calls_openai(self):
54
- import config
55
+ import hugoifier.config as config
5556
with patch.object(config, '_call_openai', return_value="response") as mock_fn:
5657
config.BACKEND = 'openai'
5758
result = config.call_ai("hello")
5859
mock_fn.assert_called_once()
5960
self.assertEqual(result, "response")
6061
6162
def test_google_backend_calls_google(self):
62
- import config
63
+ import hugoifier.config as config
6364
with patch.object(config, '_call_google', return_value="response") as mock_fn:
6465
config.BACKEND = 'google'
6566
result = config.call_ai("hello")
6667
mock_fn.assert_called_once()
6768
self.assertEqual(result, "response")
6869
6970
7071
if __name__ == "__main__":
7172
unittest.main()
7273
--- tests/test_config.py
+++ tests/test_config.py
@@ -6,11 +6,12 @@
6
7 class TestCallAiRouting(unittest.TestCase):
8 def _get_config(self):
9 # Reload config to pick up env var changes
10 import importlib
11 import config
 
12 importlib.reload(config)
13 return config
14
15 def test_raises_on_unknown_backend(self):
16 with patch.dict(os.environ, {"HUGOIFIER_BACKEND": "unknown"}):
@@ -41,31 +42,31 @@
41 cfg.GOOGLE_API_KEY = ""
42 with self.assertRaises(EnvironmentError):
43 cfg._call_google("prompt", "system")
44
45 def test_anthropic_backend_calls_anthropic(self):
46 import config
47 with patch.object(config, '_call_anthropic', return_value="response") as mock_fn:
48 config.BACKEND = 'anthropic'
49 result = config.call_ai("hello")
50 mock_fn.assert_called_once()
51 self.assertEqual(result, "response")
52
53 def test_openai_backend_calls_openai(self):
54 import config
55 with patch.object(config, '_call_openai', return_value="response") as mock_fn:
56 config.BACKEND = 'openai'
57 result = config.call_ai("hello")
58 mock_fn.assert_called_once()
59 self.assertEqual(result, "response")
60
61 def test_google_backend_calls_google(self):
62 import config
63 with patch.object(config, '_call_google', return_value="response") as mock_fn:
64 config.BACKEND = 'google'
65 result = config.call_ai("hello")
66 mock_fn.assert_called_once()
67 self.assertEqual(result, "response")
68
69
70 if __name__ == "__main__":
71 unittest.main()
72
--- tests/test_config.py
+++ tests/test_config.py
@@ -6,11 +6,12 @@
6
7 class TestCallAiRouting(unittest.TestCase):
8 def _get_config(self):
9 # Reload config to pick up env var changes
10 import importlib
11
12 import hugoifier.config as config
13 importlib.reload(config)
14 return config
15
16 def test_raises_on_unknown_backend(self):
17 with patch.dict(os.environ, {"HUGOIFIER_BACKEND": "unknown"}):
@@ -41,31 +42,31 @@
42 cfg.GOOGLE_API_KEY = ""
43 with self.assertRaises(EnvironmentError):
44 cfg._call_google("prompt", "system")
45
46 def test_anthropic_backend_calls_anthropic(self):
47 import hugoifier.config as config
48 with patch.object(config, '_call_anthropic', return_value="response") as mock_fn:
49 config.BACKEND = 'anthropic'
50 result = config.call_ai("hello")
51 mock_fn.assert_called_once()
52 self.assertEqual(result, "response")
53
54 def test_openai_backend_calls_openai(self):
55 import hugoifier.config as config
56 with patch.object(config, '_call_openai', return_value="response") as mock_fn:
57 config.BACKEND = 'openai'
58 result = config.call_ai("hello")
59 mock_fn.assert_called_once()
60 self.assertEqual(result, "response")
61
62 def test_google_backend_calls_google(self):
63 import hugoifier.config as config
64 with patch.object(config, '_call_google', return_value="response") as mock_fn:
65 config.BACKEND = 'google'
66 result = config.call_ai("hello")
67 mock_fn.assert_called_once()
68 self.assertEqual(result, "response")
69
70
71 if __name__ == "__main__":
72 unittest.main()
73
--- tests/test_decapify.py
+++ tests/test_decapify.py
@@ -1,17 +1,17 @@
11
"""Tests for utils.decapify."""
22
import os
33
import tempfile
44
import unittest
55
6
-from utils.decapify import (
6
+from hugoifier.utils.decapify import (
7
+ _build_collections,
8
+ _infer_fields_for_file,
9
+ _infer_fields_for_folder,
10
+ _parse_frontmatter,
711
_sanitize_color,
812
_widget_for_value,
9
- _parse_frontmatter,
10
- _infer_fields_for_folder,
11
- _infer_fields_for_file,
12
- _build_collections,
1313
decapify,
1414
)
1515
1616
1717
class TestSanitizeColor(unittest.TestCase):
1818
--- tests/test_decapify.py
+++ tests/test_decapify.py
@@ -1,17 +1,17 @@
1 """Tests for utils.decapify."""
2 import os
3 import tempfile
4 import unittest
5
6 from utils.decapify import (
 
 
 
 
7 _sanitize_color,
8 _widget_for_value,
9 _parse_frontmatter,
10 _infer_fields_for_folder,
11 _infer_fields_for_file,
12 _build_collections,
13 decapify,
14 )
15
16
17 class TestSanitizeColor(unittest.TestCase):
18
--- tests/test_decapify.py
+++ tests/test_decapify.py
@@ -1,17 +1,17 @@
1 """Tests for utils.decapify."""
2 import os
3 import tempfile
4 import unittest
5
6 from hugoifier.utils.decapify import (
7 _build_collections,
8 _infer_fields_for_file,
9 _infer_fields_for_folder,
10 _parse_frontmatter,
11 _sanitize_color,
12 _widget_for_value,
 
 
 
 
13 decapify,
14 )
15
16
17 class TestSanitizeColor(unittest.TestCase):
18
--- tests/test_deploy.py
+++ tests/test_deploy.py
@@ -1,9 +1,9 @@
11
"""Tests for utils.deploy."""
22
import unittest
33
4
-from utils.deploy import deploy
4
+from hugoifier.utils.deploy import deploy
55
66
77
class TestDeploy(unittest.TestCase):
88
def test_returns_complete_message(self):
99
result = deploy("/some/path", "example.com")
1010
--- tests/test_deploy.py
+++ tests/test_deploy.py
@@ -1,9 +1,9 @@
1 """Tests for utils.deploy."""
2 import unittest
3
4 from utils.deploy import deploy
5
6
7 class TestDeploy(unittest.TestCase):
8 def test_returns_complete_message(self):
9 result = deploy("/some/path", "example.com")
10
--- tests/test_deploy.py
+++ tests/test_deploy.py
@@ -1,9 +1,9 @@
1 """Tests for utils.deploy."""
2 import unittest
3
4 from hugoifier.utils.deploy import deploy
5
6
7 class TestDeploy(unittest.TestCase):
8 def test_returns_complete_message(self):
9 result = deploy("/some/path", "example.com")
10
--- tests/test_generate_decap_config.py
+++ tests/test_generate_decap_config.py
@@ -1,11 +1,11 @@
11
"""Tests for utils.generate_decap_config (thin wrapper around decapify)."""
22
import os
33
import tempfile
44
import unittest
55
6
-from utils.generate_decap_config import generate_decap_config
6
+from hugoifier.utils.generate_decap_config import generate_decap_config
77
88
99
class TestGenerateDecapConfig(unittest.TestCase):
1010
def test_creates_admin_directory(self):
1111
with tempfile.TemporaryDirectory() as tmp:
1212
--- tests/test_generate_decap_config.py
+++ tests/test_generate_decap_config.py
@@ -1,11 +1,11 @@
1 """Tests for utils.generate_decap_config (thin wrapper around decapify)."""
2 import os
3 import tempfile
4 import unittest
5
6 from utils.generate_decap_config import generate_decap_config
7
8
9 class TestGenerateDecapConfig(unittest.TestCase):
10 def test_creates_admin_directory(self):
11 with tempfile.TemporaryDirectory() as tmp:
12
--- tests/test_generate_decap_config.py
+++ tests/test_generate_decap_config.py
@@ -1,11 +1,11 @@
1 """Tests for utils.generate_decap_config (thin wrapper around decapify)."""
2 import os
3 import tempfile
4 import unittest
5
6 from hugoifier.utils.generate_decap_config import generate_decap_config
7
8
9 class TestGenerateDecapConfig(unittest.TestCase):
10 def test_creates_admin_directory(self):
11 with tempfile.TemporaryDirectory() as tmp:
12
--- tests/test_hugoify.py
+++ tests/test_hugoify.py
@@ -1,11 +1,11 @@
11
"""Tests for utils.hugoify."""
22
import os
33
import tempfile
44
import unittest
55
6
-from utils.hugoify import _parse_layout_json, _fallback_baseof, hugoify_dir
6
+from hugoifier.utils.hugoify import _fallback_baseof, _parse_layout_json, hugoify_dir
77
88
99
class TestParseLayoutJson(unittest.TestCase):
1010
def test_parses_valid_json(self):
1111
response = '{"_default/baseof.html": "<!doctype html>", "index.html": "{{ define \\"main\\" }}{{ end }}"}'
1212
--- tests/test_hugoify.py
+++ tests/test_hugoify.py
@@ -1,11 +1,11 @@
1 """Tests for utils.hugoify."""
2 import os
3 import tempfile
4 import unittest
5
6 from utils.hugoify import _parse_layout_json, _fallback_baseof, hugoify_dir
7
8
9 class TestParseLayoutJson(unittest.TestCase):
10 def test_parses_valid_json(self):
11 response = '{"_default/baseof.html": "<!doctype html>", "index.html": "{{ define \\"main\\" }}{{ end }}"}'
12
--- tests/test_hugoify.py
+++ tests/test_hugoify.py
@@ -1,11 +1,11 @@
1 """Tests for utils.hugoify."""
2 import os
3 import tempfile
4 import unittest
5
6 from hugoifier.utils.hugoify import _fallback_baseof, _parse_layout_json, hugoify_dir
7
8
9 class TestParseLayoutJson(unittest.TestCase):
10 def test_parses_valid_json(self):
11 response = '{"_default/baseof.html": "<!doctype html>", "index.html": "{{ define \\"main\\" }}{{ end }}"}'
12
--- tests/test_parser.py
+++ tests/test_parser.py
@@ -1,9 +1,9 @@
11
"""Tests for utils.parser."""
22
import unittest
33
4
-from utils.parser import parse
4
+from hugoifier.utils.parser import parse
55
66
77
class TestParse(unittest.TestCase):
88
def test_returns_complete_message(self):
99
result = parse("/some/path")
1010
--- tests/test_parser.py
+++ tests/test_parser.py
@@ -1,9 +1,9 @@
1 """Tests for utils.parser."""
2 import unittest
3
4 from utils.parser import parse
5
6
7 class TestParse(unittest.TestCase):
8 def test_returns_complete_message(self):
9 result = parse("/some/path")
10
--- tests/test_parser.py
+++ tests/test_parser.py
@@ -1,9 +1,9 @@
1 """Tests for utils.parser."""
2 import unittest
3
4 from hugoifier.utils.parser import parse
5
6
7 class TestParse(unittest.TestCase):
8 def test_returns_complete_message(self):
9 result = parse("/some/path")
10
--- tests/test_theme_finder.py
+++ tests/test_theme_finder.py
@@ -1,11 +1,11 @@
11
"""Tests for utils.theme_finder."""
22
import os
33
import tempfile
44
import unittest
55
6
-from utils.theme_finder import find_hugo_theme, find_raw_html_files
6
+from hugoifier.utils.theme_finder import find_hugo_theme, find_raw_html_files
77
88
99
def _make_hugo_theme(base_dir, theme_name="test-theme"):
1010
"""Create a minimal Hugo theme structure."""
1111
layouts = os.path.join(base_dir, theme_name, "layouts", "_default")
1212
--- tests/test_theme_finder.py
+++ tests/test_theme_finder.py
@@ -1,11 +1,11 @@
1 """Tests for utils.theme_finder."""
2 import os
3 import tempfile
4 import unittest
5
6 from utils.theme_finder import find_hugo_theme, find_raw_html_files
7
8
9 def _make_hugo_theme(base_dir, theme_name="test-theme"):
10 """Create a minimal Hugo theme structure."""
11 layouts = os.path.join(base_dir, theme_name, "layouts", "_default")
12
--- tests/test_theme_finder.py
+++ tests/test_theme_finder.py
@@ -1,11 +1,11 @@
1 """Tests for utils.theme_finder."""
2 import os
3 import tempfile
4 import unittest
5
6 from hugoifier.utils.theme_finder import find_hugo_theme, find_raw_html_files
7
8
9 def _make_hugo_theme(base_dir, theme_name="test-theme"):
10 """Create a minimal Hugo theme structure."""
11 layouts = os.path.join(base_dir, theme_name, "layouts", "_default")
12
--- tests/test_theme_patcher.py
+++ tests/test_theme_patcher.py
@@ -1,11 +1,11 @@
11
"""Tests for utils.theme_patcher."""
22
import os
33
import tempfile
44
import unittest
55
6
-from utils.theme_patcher import patch_theme, patch_config
6
+from hugoifier.utils.theme_patcher import patch_config, patch_theme
77
88
99
class TestPatchTheme(unittest.TestCase):
1010
def _write_layout(self, layouts_dir, name, content):
1111
path = os.path.join(layouts_dir, name)
1212
--- tests/test_theme_patcher.py
+++ tests/test_theme_patcher.py
@@ -1,11 +1,11 @@
1 """Tests for utils.theme_patcher."""
2 import os
3 import tempfile
4 import unittest
5
6 from utils.theme_patcher import patch_theme, patch_config
7
8
9 class TestPatchTheme(unittest.TestCase):
10 def _write_layout(self, layouts_dir, name, content):
11 path = os.path.join(layouts_dir, name)
12
--- tests/test_theme_patcher.py
+++ tests/test_theme_patcher.py
@@ -1,11 +1,11 @@
1 """Tests for utils.theme_patcher."""
2 import os
3 import tempfile
4 import unittest
5
6 from hugoifier.utils.theme_patcher import patch_config, patch_theme
7
8
9 class TestPatchTheme(unittest.TestCase):
10 def _write_layout(self, layouts_dir, name, content):
11 path = os.path.join(layouts_dir, name)
12
--- tests/test_translate.py
+++ tests/test_translate.py
@@ -9,12 +9,12 @@
99
def test_translates_file_content(self):
1010
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False) as f:
1111
f.write("<p>Hello world</p>")
1212
path = f.name
1313
try:
14
- from utils.translate import translate
15
- with patch("utils.translate.call_ai", return_value="<p>Hola mundo</p>") as mock_ai:
14
+ from hugoifier.utils.translate import translate
15
+ with patch("hugoifier.utils.translate.call_ai", return_value="<p>Hola mundo</p>") as mock_ai:
1616
result = translate(path, target_language="Spanish")
1717
self.assertEqual(result, "<p>Hola mundo</p>")
1818
call_args = mock_ai.call_args[0][0]
1919
self.assertIn("Spanish", call_args)
2020
self.assertIn("Hello world", call_args)
@@ -24,21 +24,21 @@
2424
def test_uses_target_language_param(self):
2525
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False) as f:
2626
f.write("<p>Bonjour</p>")
2727
path = f.name
2828
try:
29
- from utils.translate import translate
30
- with patch("utils.translate.call_ai", return_value="<p>Hallo</p>") as mock_ai:
29
+ from hugoifier.utils.translate import translate
30
+ with patch("hugoifier.utils.translate.call_ai", return_value="<p>Hallo</p>") as mock_ai:
3131
translate(path, target_language="German")
3232
call_args = mock_ai.call_args[0][0]
3333
self.assertIn("German", call_args)
3434
finally:
3535
os.unlink(path)
3636
3737
def test_returns_error_on_missing_file(self):
38
- from utils.translate import translate
38
+ from hugoifier.utils.translate import translate
3939
result = translate("/nonexistent/file.html")
4040
self.assertIn("failed", result.lower())
4141
4242
4343
if __name__ == "__main__":
4444
unittest.main()
4545
--- tests/test_translate.py
+++ tests/test_translate.py
@@ -9,12 +9,12 @@
9 def test_translates_file_content(self):
10 with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False) as f:
11 f.write("<p>Hello world</p>")
12 path = f.name
13 try:
14 from utils.translate import translate
15 with patch("utils.translate.call_ai", return_value="<p>Hola mundo</p>") as mock_ai:
16 result = translate(path, target_language="Spanish")
17 self.assertEqual(result, "<p>Hola mundo</p>")
18 call_args = mock_ai.call_args[0][0]
19 self.assertIn("Spanish", call_args)
20 self.assertIn("Hello world", call_args)
@@ -24,21 +24,21 @@
24 def test_uses_target_language_param(self):
25 with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False) as f:
26 f.write("<p>Bonjour</p>")
27 path = f.name
28 try:
29 from utils.translate import translate
30 with patch("utils.translate.call_ai", return_value="<p>Hallo</p>") as mock_ai:
31 translate(path, target_language="German")
32 call_args = mock_ai.call_args[0][0]
33 self.assertIn("German", call_args)
34 finally:
35 os.unlink(path)
36
37 def test_returns_error_on_missing_file(self):
38 from utils.translate import translate
39 result = translate("/nonexistent/file.html")
40 self.assertIn("failed", result.lower())
41
42
43 if __name__ == "__main__":
44 unittest.main()
45
--- tests/test_translate.py
+++ tests/test_translate.py
@@ -9,12 +9,12 @@
9 def test_translates_file_content(self):
10 with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False) as f:
11 f.write("<p>Hello world</p>")
12 path = f.name
13 try:
14 from hugoifier.utils.translate import translate
15 with patch("hugoifier.utils.translate.call_ai", return_value="<p>Hola mundo</p>") as mock_ai:
16 result = translate(path, target_language="Spanish")
17 self.assertEqual(result, "<p>Hola mundo</p>")
18 call_args = mock_ai.call_args[0][0]
19 self.assertIn("Spanish", call_args)
20 self.assertIn("Hello world", call_args)
@@ -24,21 +24,21 @@
24 def test_uses_target_language_param(self):
25 with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False) as f:
26 f.write("<p>Bonjour</p>")
27 path = f.name
28 try:
29 from hugoifier.utils.translate import translate
30 with patch("hugoifier.utils.translate.call_ai", return_value="<p>Hallo</p>") as mock_ai:
31 translate(path, target_language="German")
32 call_args = mock_ai.call_args[0][0]
33 self.assertIn("German", call_args)
34 finally:
35 os.unlink(path)
36
37 def test_returns_error_on_missing_file(self):
38 from hugoifier.utils.translate import translate
39 result = translate("/nonexistent/file.html")
40 self.assertIn("failed", result.lower())
41
42
43 if __name__ == "__main__":
44 unittest.main()
45

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button