Hugoifier

hugoifier / hugoifier / utils / complete.py
Source Blame History 377 lines
91515c0… lmata 1 """
91515c0… lmata 2 Full end-to-end pipeline: detect → copy → configure → decap.
91515c0… lmata 3
91515c0… lmata 4 For already-Hugo themes: assembles a clean, standalone site.
91515c0… lmata 5 For raw HTML themes: calls hugoify first, then assembles.
91515c0… lmata 6 """
91515c0… lmata 7
91515c0… lmata 8 import logging
91515c0… lmata 9 import os
91515c0… lmata 10 import shutil
91515c0… lmata 11 from pathlib import Path
91515c0… lmata 12
91515c0… lmata 13 from .decapify import decapify
77c5e9a… noreply 14 from .hugoify import hugoify_html, hugoify_nextjs
77c5e9a… noreply 15 from .theme_finder import find_hugo_theme, find_nextjs_app, find_raw_html_files
91515c0… lmata 16 from .theme_patcher import patch_config, patch_theme
91515c0… lmata 17
91515c0… lmata 18
91515c0… lmata 19 def complete(
91515c0… lmata 20 input_path: str,
91515c0… lmata 21 output_dir: str = None,
91515c0… lmata 22 cms_name: str = None,
91515c0… lmata 23 cms_logo: str = None,
91515c0… lmata 24 cms_color: str = None,
91515c0… lmata 25 ) -> str:
91515c0… lmata 26 """
91515c0… lmata 27 Run the full pipeline for a theme.
91515c0… lmata 28
91515c0… lmata 29 Args:
91515c0… lmata 30 input_path: Path to a theme directory (from themes/) or raw HTML dir.
91515c0… lmata 31 output_dir: Where to write the output site. Defaults to output/{theme-name}.
91515c0… lmata 32 cms_name: Whitelabel CMS name for Decap admin UI.
91515c0… lmata 33 cms_logo: Whitelabel logo URL for Decap admin UI.
91515c0… lmata 34 cms_color: Whitelabel top-bar color for Decap admin UI.
91515c0… lmata 35
91515c0… lmata 36 Returns:
91515c0… lmata 37 Path to the generated site, or error message.
91515c0… lmata 38 """
91515c0… lmata 39 logging.info(f"Starting pipeline for {input_path} ...")
91515c0… lmata 40
91515c0… lmata 41 branding = {'cms_name': cms_name, 'cms_logo': cms_logo, 'cms_color': cms_color}
91515c0… lmata 42 info = find_hugo_theme(input_path)
91515c0… lmata 43
91515c0… lmata 44 if info:
91515c0… lmata 45 return _assemble_hugo_site(info, output_dir, branding)
77c5e9a… noreply 46
77c5e9a… noreply 47 # Next.js path (check before raw HTML since Next.js projects may contain .html files)
77c5e9a… noreply 48 nextjs_info = find_nextjs_app(input_path)
77c5e9a… noreply 49 if nextjs_info:
77c5e9a… noreply 50 return _convert_nextjs(input_path, nextjs_info, output_dir, branding)
77c5e9a… noreply 51
77c5e9a… noreply 52 # Raw HTML path
77c5e9a… noreply 53 html_files = find_raw_html_files(input_path)
77c5e9a… noreply 54 if not html_files:
77c5e9a… noreply 55 raise ValueError(f"No Hugo theme, Next.js app, or HTML files found in {input_path}")
77c5e9a… noreply 56 return _convert_raw_html(input_path, html_files, output_dir, branding)
91515c0… lmata 57
91515c0… lmata 58
91515c0… lmata 59 # ---------------------------------------------------------------------------
91515c0… lmata 60 # Hugo theme path
91515c0… lmata 61 # ---------------------------------------------------------------------------
91515c0… lmata 62
91515c0… lmata 63 def _assemble_hugo_site(info: dict, output_dir: str = None, branding: dict = None) -> str:
91515c0… lmata 64 theme_dir = info['theme_dir']
91515c0… lmata 65 example_site = info['example_site']
91515c0… lmata 66 theme_name = info['theme_name']
91515c0… lmata 67
91515c0… lmata 68 if output_dir is None:
91515c0… lmata 69 output_dir = str(Path(__file__).parents[2] / 'output' / theme_name)
91515c0… lmata 70
91515c0… lmata 71 logging.info(f"Building site at {output_dir} ...")
91515c0… lmata 72 os.makedirs(output_dir, exist_ok=True)
91515c0… lmata 73
91515c0… lmata 74 # 1. Copy theme files → themes/{theme_name}/
91515c0… lmata 75 dest_theme = os.path.join(output_dir, 'themes', theme_name)
91515c0… lmata 76 _copy_dir(theme_dir, dest_theme, exclude={'exampleSite', '__MACOSX', '.DS_Store'})
91515c0… lmata 77 logging.info(f"Copied theme to {dest_theme}")
91515c0… lmata 78 patch_theme(dest_theme)
91515c0… lmata 79
91515c0… lmata 80 # 2. Copy exampleSite content/static/data → site root
91515c0… lmata 81 if example_site:
91515c0… lmata 82 for subdir in ('content', 'data', 'i18n'):
91515c0… lmata 83 src = os.path.join(example_site, subdir)
91515c0… lmata 84 if os.path.isdir(src):
91515c0… lmata 85 _copy_dir(src, os.path.join(output_dir, subdir))
91515c0… lmata 86 logging.info(f"Copied {subdir}/ from exampleSite")
91515c0… lmata 87
91515c0… lmata 88 # Static: merge exampleSite/static into output/static
91515c0… lmata 89 src_static = os.path.join(example_site, 'static')
91515c0… lmata 90 if os.path.isdir(src_static):
91515c0… lmata 91 _copy_dir(src_static, os.path.join(output_dir, 'static'))
91515c0… lmata 92 logging.info("Copied static/ from exampleSite")
91515c0… lmata 93
91515c0… lmata 94 # Write hugo.toml from exampleSite config
91515c0… lmata 95 config_toml = _find_config(example_site)
91515c0… lmata 96 if config_toml:
91515c0… lmata 97 _write_hugo_toml(config_toml, output_dir, theme_name)
91515c0… lmata 98 else:
91515c0… lmata 99 _write_minimal_hugo_toml(output_dir, theme_name)
91515c0… lmata 100 else:
91515c0… lmata 101 _write_minimal_hugo_toml(output_dir, theme_name)
91515c0… lmata 102 # Create minimal content/_index.md
91515c0… lmata 103 content_dir = os.path.join(output_dir, 'content')
91515c0… lmata 104 os.makedirs(content_dir, exist_ok=True)
91515c0… lmata 105 index_md = os.path.join(content_dir, '_index.md')
91515c0… lmata 106 if not os.path.exists(index_md):
91515c0… lmata 107 with open(index_md, 'w') as f:
91515c0… lmata 108 f.write('---\ntitle: Home\n---\n')
91515c0… lmata 109
91515c0… lmata 110 # 3. Generate Decap CMS config
91515c0… lmata 111 b = branding or {}
91515c0… lmata 112 decapify(
91515c0… lmata 113 output_dir,
91515c0… lmata 114 cms_name=b.get('cms_name'), cms_logo=b.get('cms_logo'), cms_color=b.get('cms_color'),
91515c0… lmata 115 )
91515c0… lmata 116
91515c0… lmata 117 logging.info(f"Done. Site ready at: {output_dir}")
91515c0… lmata 118 logging.info(f"Run: cd {output_dir} && hugo serve")
91515c0… lmata 119 return output_dir
91515c0… lmata 120
91515c0… lmata 121
91515c0… lmata 122 # ---------------------------------------------------------------------------
77c5e9a… noreply 123 # Next.js path
77c5e9a… noreply 124 # ---------------------------------------------------------------------------
77c5e9a… noreply 125
77c5e9a… noreply 126 def _convert_nextjs(
77c5e9a… noreply 127 input_path: str, nextjs_info: dict, output_dir: str = None, branding: dict = None
77c5e9a… noreply 128 ) -> str:
77c5e9a… noreply 129 app_dir = nextjs_info['app_dir']
77c5e9a… noreply 130 theme_name = nextjs_info.get('app_name', os.path.basename(os.path.abspath(input_path)))
77c5e9a… noreply 131
77c5e9a… noreply 132 if output_dir is None:
77c5e9a… noreply 133 output_dir = str(Path(__file__).parents[2] / 'output' / theme_name)
77c5e9a… noreply 134
77c5e9a… noreply 135 logging.info(f"Converting Next.js app: {theme_name}")
77c5e9a… noreply 136
77c5e9a… noreply 137 # Convert: capture rendered HTML if dev server running, else AI fallback
77c5e9a… noreply 138 hugo_layouts = hugoify_nextjs(nextjs_info)
77c5e9a… noreply 139
77c5e9a… noreply 140 os.makedirs(output_dir, exist_ok=True)
77c5e9a… noreply 141
77c5e9a… noreply 142 # Extract captured CSS if present (from rendered HTML capture)
77c5e9a… noreply 143 captured_css = hugo_layouts.pop('_captured_css', {})
77c5e9a… noreply 144
77c5e9a… noreply 145 # Write converted layouts
77c5e9a… noreply 146 theme_layouts_dir = os.path.join(output_dir, 'themes', theme_name, 'layouts')
77c5e9a… noreply 147 os.makedirs(os.path.join(theme_layouts_dir, '_default'), exist_ok=True)
77c5e9a… noreply 148 os.makedirs(os.path.join(theme_layouts_dir, 'partials'), exist_ok=True)
77c5e9a… noreply 149
77c5e9a… noreply 150 for filename, content in hugo_layouts.items():
77c5e9a… noreply 151 # Fix common AI mistake: partial "partials/X.html" → partial "X.html"
77c5e9a… noreply 152 if isinstance(content, str):
77c5e9a… noreply 153 content = content.replace('partial "partials/', 'partial "')
77c5e9a… noreply 154 dest = os.path.join(theme_layouts_dir, filename)
77c5e9a… noreply 155 os.makedirs(os.path.dirname(dest), exist_ok=True)
77c5e9a… noreply 156 with open(dest, 'w') as f:
77c5e9a… noreply 157 f.write(content)
77c5e9a… noreply 158
77c5e9a… noreply 159 # Copy public/ assets to theme static/
77c5e9a… noreply 160 public_dir = os.path.join(app_dir, 'public')
77c5e9a… noreply 161 theme_static = os.path.join(output_dir, 'themes', theme_name, 'static')
77c5e9a… noreply 162 if os.path.isdir(public_dir):
77c5e9a… noreply 163 _copy_dir(public_dir, theme_static)
77c5e9a… noreply 164 logging.info("Copied public/ assets to static/")
77c5e9a… noreply 165
77c5e9a… noreply 166 # Write captured CSS (from rendered HTML capture)
77c5e9a… noreply 167 css_dest = os.path.join(theme_static, 'css')
77c5e9a… noreply 168 os.makedirs(css_dest, exist_ok=True)
77c5e9a… noreply 169 for css_name, css_content in captured_css.items():
77c5e9a… noreply 170 with open(os.path.join(css_dest, css_name), 'w') as f:
77c5e9a… noreply 171 f.write(css_content)
77c5e9a… noreply 172 logging.info(f"Wrote captured CSS: {css_name}")
77c5e9a… noreply 173
77c5e9a… noreply 174 # Also copy source CSS files (globals.css etc.)
77c5e9a… noreply 175 for css_file in nextjs_info.get('css_files', []):
77c5e9a… noreply 176 if os.path.isfile(css_file):
77c5e9a… noreply 177 shutil.copy2(css_file, os.path.join(css_dest, os.path.basename(css_file)))
77c5e9a… noreply 178 logging.info("Copied CSS files")
77c5e9a… noreply 179
77c5e9a… noreply 180 _write_minimal_hugo_toml(output_dir, theme_name)
3d0f0d4… lmata 181 _write_default_layouts(output_dir, theme_name)
77c5e9a… noreply 182
77c5e9a… noreply 183 # Create minimal content
77c5e9a… noreply 184 content_dir = os.path.join(output_dir, 'content')
77c5e9a… noreply 185 os.makedirs(content_dir, exist_ok=True)
77c5e9a… noreply 186 with open(os.path.join(content_dir, '_index.md'), 'w') as f:
77c5e9a… noreply 187 f.write('---\ntitle: Home\n---\n')
77c5e9a… noreply 188
77c5e9a… noreply 189 b = branding or {}
77c5e9a… noreply 190 decapify(
77c5e9a… noreply 191 output_dir,
77c5e9a… noreply 192 cms_name=b.get('cms_name'), cms_logo=b.get('cms_logo'), cms_color=b.get('cms_color'),
77c5e9a… noreply 193 )
77c5e9a… noreply 194
77c5e9a… noreply 195 logging.info(f"Done. Site ready at: {output_dir}")
77c5e9a… noreply 196 logging.info(f"Run: cd {output_dir} && hugo serve")
77c5e9a… noreply 197 return output_dir
77c5e9a… noreply 198
77c5e9a… noreply 199
77c5e9a… noreply 200 # ---------------------------------------------------------------------------
91515c0… lmata 201 # Raw HTML path
91515c0… lmata 202 # ---------------------------------------------------------------------------
91515c0… lmata 203
91515c0… lmata 204 def _convert_raw_html(
91515c0… lmata 205 input_path: str, html_files: list, output_dir: str = None, branding: dict = None
91515c0… lmata 206 ) -> str:
91515c0… lmata 207 theme_name = os.path.basename(os.path.abspath(input_path))
91515c0… lmata 208
91515c0… lmata 209 if output_dir is None:
91515c0… lmata 210 output_dir = str(Path(__file__).parents[2] / 'output' / theme_name)
91515c0… lmata 211
91515c0… lmata 212 logging.info(f"Converting raw HTML theme: {theme_name}")
91515c0… lmata 213
77c5e9a… noreply 214 # Direct HTML extraction — use the actual HTML as-is, no AI reinterpretation
91515c0… lmata 215 main_html = _pick_main_html(html_files)
77c5e9a… noreply 216 logging.info(f"Extracting {main_html} ...")
91515c0… lmata 217 hugo_layouts = hugoify_html(main_html)
91515c0… lmata 218
91515c0… lmata 219 os.makedirs(output_dir, exist_ok=True)
91515c0… lmata 220
91515c0… lmata 221 # Write converted layouts
91515c0… lmata 222 theme_layouts_dir = os.path.join(output_dir, 'themes', theme_name, 'layouts')
91515c0… lmata 223 os.makedirs(os.path.join(theme_layouts_dir, '_default'), exist_ok=True)
91515c0… lmata 224 os.makedirs(os.path.join(theme_layouts_dir, 'partials'), exist_ok=True)
91515c0… lmata 225
91515c0… lmata 226 for filename, content in hugo_layouts.items():
91515c0… lmata 227 dest = os.path.join(theme_layouts_dir, filename)
91515c0… lmata 228 os.makedirs(os.path.dirname(dest), exist_ok=True)
91515c0… lmata 229 with open(dest, 'w') as f:
91515c0… lmata 230 f.write(content)
91515c0… lmata 231
77c5e9a… noreply 232 # Copy ALL static assets from the HTML theme directory
77c5e9a… noreply 233 theme_static = os.path.join(output_dir, 'themes', theme_name, 'static')
77c5e9a… noreply 234 for item in os.listdir(input_path):
77c5e9a… noreply 235 src = os.path.join(input_path, item)
77c5e9a… noreply 236 if os.path.isdir(src) and item not in ('__MACOSX', '.git', 'node_modules'):
77c5e9a… noreply 237 _copy_dir(src, os.path.join(theme_static, item))
77c5e9a… noreply 238 elif os.path.isfile(src) and not item.endswith('.html'):
77c5e9a… noreply 239 # Copy non-HTML files (images, fonts, etc.) to static root
77c5e9a… noreply 240 os.makedirs(theme_static, exist_ok=True)
77c5e9a… noreply 241 shutil.copy2(src, os.path.join(theme_static, item))
91515c0… lmata 242
91515c0… lmata 243 _write_minimal_hugo_toml(output_dir, theme_name)
3d0f0d4… lmata 244 _write_default_layouts(output_dir, theme_name)
91515c0… lmata 245
91515c0… lmata 246 # Create minimal content
91515c0… lmata 247 content_dir = os.path.join(output_dir, 'content')
91515c0… lmata 248 os.makedirs(content_dir, exist_ok=True)
91515c0… lmata 249 with open(os.path.join(content_dir, '_index.md'), 'w') as f:
91515c0… lmata 250 f.write('---\ntitle: Home\n---\n')
91515c0… lmata 251
91515c0… lmata 252 b = branding or {}
91515c0… lmata 253 decapify(
91515c0… lmata 254 output_dir,
91515c0… lmata 255 cms_name=b.get('cms_name'), cms_logo=b.get('cms_logo'), cms_color=b.get('cms_color'),
91515c0… lmata 256 )
91515c0… lmata 257
91515c0… lmata 258 logging.info(f"Done. Site ready at: {output_dir}")
91515c0… lmata 259 return output_dir
91515c0… lmata 260
91515c0… lmata 261
91515c0… lmata 262 # ---------------------------------------------------------------------------
91515c0… lmata 263 # Helpers
91515c0… lmata 264 # ---------------------------------------------------------------------------
91515c0… lmata 265
91515c0… lmata 266 def _copy_dir(src: str, dest: str, exclude: set = None):
91515c0… lmata 267 """Copy src → dest, skipping excluded names."""
91515c0… lmata 268 exclude = exclude or set()
91515c0… lmata 269 if not os.path.isdir(src):
91515c0… lmata 270 return
91515c0… lmata 271 os.makedirs(dest, exist_ok=True)
91515c0… lmata 272 for item in os.listdir(src):
91515c0… lmata 273 if item in exclude or item.startswith('._'):
91515c0… lmata 274 continue
91515c0… lmata 275 s = os.path.join(src, item)
91515c0… lmata 276 d = os.path.join(dest, item)
91515c0… lmata 277 if os.path.isdir(s):
91515c0… lmata 278 _copy_dir(s, d, exclude)
91515c0… lmata 279 else:
91515c0… lmata 280 shutil.copy2(s, d)
91515c0… lmata 281
91515c0… lmata 282
91515c0… lmata 283 def _find_config(example_site: str) -> str | None:
91515c0… lmata 284 """Find config.toml or hugo.toml in exampleSite."""
91515c0… lmata 285 for name in ('hugo.toml', 'config.toml'):
91515c0… lmata 286 p = os.path.join(example_site, name)
91515c0… lmata 287 if os.path.exists(p):
91515c0… lmata 288 return p
91515c0… lmata 289 # config/_default/config.toml pattern
91515c0… lmata 290 p = os.path.join(example_site, 'config', '_default', 'config.toml')
91515c0… lmata 291 if os.path.exists(p):
91515c0… lmata 292 return p
91515c0… lmata 293 return None
91515c0… lmata 294
91515c0… lmata 295
91515c0… lmata 296 def _write_hugo_toml(source_config: str, output_dir: str, theme_name: str):
91515c0… lmata 297 """Copy source config to hugo.toml, ensuring theme = theme_name and modern key names."""
91515c0… lmata 298 import re
91515c0… lmata 299 with open(source_config, 'r') as f:
91515c0… lmata 300 content = f.read()
91515c0… lmata 301
91515c0… lmata 302 # Suppress noisy but harmless warnings from example content
91515c0… lmata 303 if 'ignoreLogs' not in content:
91515c0… lmata 304 content += "\nignorelogs = ['warning-goldmark-raw-html']\n"
91515c0… lmata 305
91515c0… lmata 306 # Ensure theme is set correctly
91515c0… lmata 307 if re.search(r'^theme\s*=', content, re.MULTILINE):
91515c0… lmata 308 content = re.sub(r'^theme\s*=.*$', f'theme = "{theme_name}"', content, flags=re.MULTILINE)
91515c0… lmata 309 else:
91515c0… lmata 310 content = f'theme = "{theme_name}"\n' + content
91515c0… lmata 311
91515c0… lmata 312 dest = os.path.join(output_dir, 'hugo.toml')
91515c0… lmata 313 with open(dest, 'w') as f:
91515c0… lmata 314 f.write(content)
91515c0… lmata 315 patch_config(dest)
91515c0… lmata 316 logging.info("Wrote hugo.toml")
91515c0… lmata 317
91515c0… lmata 318
91515c0… lmata 319 def _write_minimal_hugo_toml(output_dir: str, theme_name: str):
91515c0… lmata 320 dest = os.path.join(output_dir, 'hugo.toml')
91515c0… lmata 321 safe_name = theme_name.replace('"', '')
91515c0… lmata 322 title = safe_name.replace('-', ' ').title()
91515c0… lmata 323 with open(dest, 'w') as f:
91515c0… lmata 324 f.write(f'''baseURL = "http://localhost:1313/"
91515c0… lmata 325 languageCode = "en-us"
91515c0… lmata 326 title = "{title}"
91515c0… lmata 327 theme = "{safe_name}"
91515c0… lmata 328 ''')
91515c0… lmata 329 logging.info("Wrote minimal hugo.toml")
3d0f0d4… lmata 330
3d0f0d4… lmata 331
3d0f0d4… lmata 332 def _write_default_layouts(output_dir: str, theme_name: str):
3d0f0d4… lmata 333 """Write single.html and list.html if they don't already exist."""
3d0f0d4… lmata 334 layouts_dir = os.path.join(output_dir, 'themes', theme_name, 'layouts', '_default')
3d0f0d4… lmata 335 os.makedirs(layouts_dir, exist_ok=True)
3d0f0d4… lmata 336
3d0f0d4… lmata 337 single = os.path.join(layouts_dir, 'single.html')
3d0f0d4… lmata 338 if not os.path.exists(single):
3d0f0d4… lmata 339 with open(single, 'w') as f:
3d0f0d4… lmata 340 f.write('''{{ define "main" }}
3d0f0d4… lmata 341 <div style="max-width:48rem;margin:0 auto;padding:3rem 1.5rem">
3d0f0d4… lmata 342 <a href="/" style="color:#515be3;font-size:0.875rem;display:inline-block;margin-bottom:2rem">&larr; Back to home</a>
3d0f0d4… lmata 343 <h1 style="font-size:2.25rem;font-weight:700;margin-bottom:1rem">{{ .Title }}</h1>
3d0f0d4… lmata 344 {{ with .Params.description }}<p style="color:#9ca3af;font-size:1.125rem;margin-bottom:1.5rem">{{ . }}</p>{{ end }}
3d0f0d4… lmata 345 {{ with .Date }}<time style="color:#6b7280;font-size:0.875rem;display:block;margin-bottom:2.5rem">{{ .Format "January 2, 2006" }}</time>{{ end }}
3d0f0d4… lmata 346 <article>{{ .Content }}</article>
3d0f0d4… lmata 347 </div>
3d0f0d4… lmata 348 {{ end }}
3d0f0d4… lmata 349 ''')
3d0f0d4… lmata 350 logging.info("Wrote default single.html")
3d0f0d4… lmata 351
3d0f0d4… lmata 352 list_html = os.path.join(layouts_dir, 'list.html')
3d0f0d4… lmata 353 if not os.path.exists(list_html):
3d0f0d4… lmata 354 with open(list_html, 'w') as f:
3d0f0d4… lmata 355 f.write('''{{ define "main" }}
3d0f0d4… lmata 356 <div style="max-width:48rem;margin:0 auto;padding:3rem 1.5rem">
3d0f0d4… lmata 357 <a href="/" style="color:#515be3;font-size:0.875rem;display:inline-block;margin-bottom:2rem">&larr; Back to home</a>
3d0f0d4… lmata 358 <h1 style="font-size:2.25rem;font-weight:700;margin-bottom:3rem">{{ .Title }}</h1>
3d0f0d4… lmata 359 {{ range .Pages }}
3d0f0d4… lmata 360 <a href="{{ .Permalink }}" style="display:block;padding:1.5rem;border-radius:1rem;border:1px solid #374151;margin-bottom:1.5rem;text-decoration:none;color:inherit">
3d0f0d4… lmata 361 <h2 style="font-size:1.25rem;font-weight:600;margin-bottom:0.5rem">{{ .Title }}</h2>
3d0f0d4… lmata 362 {{ with .Params.description }}<p style="color:#9ca3af">{{ . }}</p>{{ end }}
3d0f0d4… lmata 363 {{ with .Date }}<time style="color:#6b7280;font-size:0.875rem;margin-top:0.75rem;display:block">{{ .Format "January 2, 2006" }}</time>{{ end }}
3d0f0d4… lmata 364 </a>
3d0f0d4… lmata 365 {{ end }}
3d0f0d4… lmata 366 </div>
3d0f0d4… lmata 367 {{ end }}
3d0f0d4… lmata 368 ''')
3d0f0d4… lmata 369 logging.info("Wrote default list.html")
91515c0… lmata 370
91515c0… lmata 371
91515c0… lmata 372 def _pick_main_html(html_files: list) -> str:
91515c0… lmata 373 """Pick the most likely 'main' HTML file (index.html or first one)."""
91515c0… lmata 374 for f in html_files:
91515c0… lmata 375 if os.path.basename(f).lower() in ('index.html', 'home.html', 'main.html'):
91515c0… lmata 376 return f
91515c0… lmata 377 return html_files[0]

Keyboard Shortcuts

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