diff --git a/PyPost.py b/PyPost.py index df1b703..074d0f0 100644 --- a/PyPost.py +++ b/PyPost.py @@ -8,6 +8,7 @@ from jinja2 import Environment, FileSystemLoader import base64 import random import time +import yaml import marko from marko.ext.gfm import GFM @@ -19,7 +20,7 @@ from htmlhandler import htmlhandler as Handler from lua import plugin_manager plugin_manager = plugin_manager.PluginManager() -plugin_manager.load_all() # load plugins +plugin_manager.load_all() # load plugins # Use absolute paths ROOT = Path(os.path.abspath(".")) @@ -34,7 +35,7 @@ RUST_PARSER_PATH = ROOT / "fastmd" / "target" / "release" / f"fastmd{exe_ext}" if not RUST_PARSER_PATH.exists(): RUST_PARSER_PATH = ROOT / "fastmd" / "target" / "debug" / f"fastmd{exe_ext}" -# Create Python Markdown parser with table support (fallback for small files) +# Python Markdown parser with table support markdown_parser = marko.Markdown(extensions=[GFM]) # Threshold for switching to Rust parser (number of lines) @@ -45,8 +46,22 @@ Logger = Logger() # Global obfuscate flag, default True obfuscate = True +def split_yaml_front_matter(md_path: Path) -> tuple[str, str]: + """Return (yaml_text, markdown_text). YAML is '' if none exists.""" + try: + text = md_path.read_text(encoding="utf-8") + except Exception as e: + Logger.log_error(f"Could not read {md_path}: {e}") + return '', '' + if text.startswith("---"): + parts = text.split("---", 2) + if len(parts) >= 3: + yaml_text = parts[1].strip() + markdown_text = parts[2].lstrip("\n") + return yaml_text, markdown_text + return '', text + def count_lines_in_file(file_path: Path) -> int: - """Count the number of lines in a file.""" try: with open(file_path, 'r', encoding='utf-8') as f: return sum(1 for _ in f) @@ -55,24 +70,18 @@ def count_lines_in_file(file_path: Path) -> int: return 0 def should_use_rust_parser(md_path: Path) -> bool: - """Determine if we should use the Rust parser based on file size.""" if not RUST_PARSER_PATH.exists(): return False - line_count = count_lines_in_file(md_path) use_rust = line_count > RUST_PARSER_THRESHOLD - if use_rust: Logger.log_rust_usage(f"Using Rust parser for {md_path} ({line_count} lines)") else: Logger.log_debug(f"Using Python parser for {md_path} ({line_count} lines)") - return use_rust def parse_markdown_with_rust(md_path: Path) -> str: - """Parse markdown using the Rust parser.""" try: - # Run the Rust parser result = subprocess.run( [str(RUST_PARSER_PATH), str(md_path)], capture_output=True, @@ -90,31 +99,27 @@ def parse_markdown_with_rust(md_path: Path) -> str: raise def render_markdown(md_path: Path): - """Render a single markdown file to an obfuscated HTML file.""" - try: - text = md_path.read_text(encoding="utf-8") - except Exception as e: - Logger.log_error(f"Could not read {md_path}: {e}") - return + """Render a single markdown file to HTML, stripping YAML front matter.""" + yaml_text, markdown_text = split_yaml_front_matter(md_path) - # Decide which parser to use based on file size + # Choose parser if should_use_rust_parser(md_path): try: html_body = parse_markdown_with_rust(md_path) except Exception as e: Logger.log_warning(f"Rust parser failed for {md_path}, falling back to Python parser: {e}") - html_body = markdown_parser.convert(text) + html_body = markdown_parser.convert(markdown_text) else: - html_body = markdown_parser.convert(text) + html_body = markdown_parser.convert(markdown_text) # Extract title from filename or first H1 title = md_path.stem - for line in text.splitlines(): + for line in markdown_text.splitlines(): if line.startswith("# "): title = line[2:].strip() break - - # Call pre_template hook properly + + # Plugin pre_template hook Logger.log_debug(f"Calling pre_template hook for {md_path}") modified = plugin_manager.run_hook("pre_template", str(md_path), html_body) if modified is not None: @@ -123,13 +128,11 @@ def render_markdown(md_path: Path): else: Logger.log_debug("pre_template hook returned None") - # Create clean HTML structure - # Pick two different hashes from hash_list + # Jinja template env = Environment(loader=FileSystemLoader("html/base")) template = env.get_template("template.html") hash1, hash2 = random.sample(hash_list, 2) - # Load these variable for Jinja to use. clean_jinja_html = template.render( title=title, html_body=html_body, @@ -139,23 +142,36 @@ def render_markdown(md_path: Path): timestamp=time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), ) + # Plugin post_render hook post_mod = plugin_manager.run_hook("post_render", str(md_path), clean_jinja_html) if post_mod is not None: clean_jinja_html = post_mod - # Ensure html directory exists + # Write output HTML_DIR.mkdir(exist_ok=True) - - # Maintain relative directory structure in html/ relative_path = md_path.relative_to(MARKDOWN_DIR) out_path = HTML_DIR / relative_path.with_suffix(".html") - - # Create parent directories if needed out_path.parent.mkdir(parents=True, exist_ok=True) - out_path.write_text(clean_jinja_html, encoding="utf-8") Logger.log_debug(f"Rendered: {md_path} -> {out_path}") +def extract_summary(md_path: Path) -> tuple[str, str] | None: + yaml_text, _ = split_yaml_front_matter(md_path) + if not yaml_text: + Logger.log_debug(f"No YAML front matter in {md_path}") + return None + try: + data = yaml.safe_load(yaml_text) + summary = data.get("summary") + if summary: + html_name = md_path.with_suffix(".html").name + return html_name, summary + else: + Logger.log_debug(f"No 'summary' key in YAML of {md_path}") + except Exception as e: + Logger.log_warning(f"Failed to parse YAML summary in {md_path}: {e}") + return None + def remove_html(md_path: Path): relative_path = md_path.relative_to(MARKDOWN_DIR) out_path = HTML_DIR / relative_path.with_suffix(".html") @@ -170,20 +186,15 @@ def initial_scan(markdown_dir: Path): def build_rust_parser() -> bool: fastmd_dir = ROOT / "fastmd" - if not fastmd_dir.exists(): Logger.log_error(f"fastmd directory not found at {fastmd_dir}") return False - cargo_toml = fastmd_dir / "Cargo.toml" if not cargo_toml.exists(): Logger.log_error(f"Cargo.toml not found at {cargo_toml}") return False - Logger.log_info("Attempting to build Rust parser with 'cargo build --release'...") - try: - # Run cargo build --release in the fastmd directory result = subprocess.run( ["cargo", "build", "--release"], cwd=str(fastmd_dir), @@ -191,11 +202,9 @@ def build_rust_parser() -> bool: text=True, check=True ) - Logger.log_info("Rust parser built successfully!") Logger.log_debug(f"Build output: {result.stdout}") return True - except subprocess.CalledProcessError as e: Logger.log_error(f"Failed to build Rust parser: {e}") Logger.log_error(f"Build stderr: {e.stderr}") @@ -208,7 +217,7 @@ def build_rust_parser() -> bool: return False if __name__ == "__main__": - # Check for markdown directory + # Handle alternative markdown folder if not MARKDOWN_DIR.exists(): alt_root = ROOT / "PyPost" if alt_root.exists() and alt_root.is_dir(): @@ -216,7 +225,6 @@ if __name__ == "__main__": ROOT = alt_root MARKDOWN_DIR = ROOT / "markdown" HTML_DIR = ROOT / "html" - # Update Rust parser path for new root RUST_PARSER_PATH = ROOT / "fastmd" / "target" / "release" / f"fastmd{exe_ext}" if not RUST_PARSER_PATH.exists(): RUST_PARSER_PATH = ROOT / "fastmd" / "target" / "debug" / f"fastmd{exe_ext}" @@ -224,18 +232,14 @@ if __name__ == "__main__": Logger.log_error(f"Markdown directory not found: {MARKDOWN_DIR}") Logger.log_warning("Please create a 'markdown' directory or use a 'PyPost' directory with one inside it.") sys.exit(1) - - # Check if Rust parser exists, if not try to build it + + # Build Rust parser if missing if not RUST_PARSER_PATH.exists(): Logger.log_warning(f"Rust parser not found at {RUST_PARSER_PATH}") - - # Try to build the Rust parser if build_rust_parser(): - # Update path after successful build RUST_PARSER_PATH = ROOT / "fastmd" / "target" / "release" / f"fastmd{exe_ext}" if not RUST_PARSER_PATH.exists(): RUST_PARSER_PATH = ROOT / "fastmd" / "target" / "debug" / f"fastmd{exe_ext}" - if RUST_PARSER_PATH.exists(): Logger.log_info(f"Rust parser built and found at: {RUST_PARSER_PATH}") else: @@ -246,13 +250,12 @@ if __name__ == "__main__": Logger.log_warning("Will use Python parser for all files") else: Logger.log_info(f"Rust parser found at: {RUST_PARSER_PATH}") - - # Log parser strategy + if RUST_PARSER_PATH.exists(): Logger.log_info(f"Will use Rust parser for files with more than {RUST_PARSER_THRESHOLD} lines") else: Logger.log_warning("Using Python parser for all files") - + initial_scan(MARKDOWN_DIR) event_handler = Handler() observer = Observer() @@ -264,4 +267,4 @@ if __name__ == "__main__": time.sleep(1) except KeyboardInterrupt: observer.stop() - observer.join() \ No newline at end of file + observer.join() diff --git a/css/indexer.css b/css/indexer.css index b49bee0..225226f 100644 --- a/css/indexer.css +++ b/css/indexer.css @@ -1,39 +1,80 @@ +/* RESET & BASE LAYOUT */ * { box-sizing: border-box; } -body { - font-family: Arial, sans-serif; +html { + height: 100%; +} + +body { + display: flex; + flex-direction: column; + min-height: 100vh; margin: 0; - padding: 20px; + padding: 0; + font-family: Arial, sans-serif; background-color: #fff; color: #0f1111; font-size: 16px; } -h1 { +.page-container { + display: flex; + flex-direction: column; + flex: 1; + width: 100%; +} + +main { + flex: 1; + padding: 20px; +} + +/* FOOTER */ +.footer { + border-top: solid#ccc 1px; + margin-top: auto; + width: 100%; + text-align: left; + padding: 1em; + font-size: 14px; +} + +.footer hr { + border: 1px solid #ccc; +} + +.footer a { + text-decoration: none; + color: #0066cc; + font-style: italic; +} + +/* TYPOGRAPHY */ +h1 { color: #333; font-size: clamp(1.5rem, 5vw, 2rem); margin: 0 0 1rem 0; } -hr { +hr { width: 100%; border: none; border-top: 1px solid #ddd; } /* LIST STYLES */ -ul { +ul { padding-left: clamp(20px, 10vw, 100px); margin-top: 0.5em; list-style: none; } -li { - list-style: none; - background: url("../../css/icons/item.webp") no-repeat left center; - background-size: 15px 20px; +li { + list-style: none; + background: url("../../css/icons/item.webp") no-repeat left center; + background-size: 15px 20px; padding: 12px 0 12px 30px; font-size: clamp(1rem, 2.5vw, 1.125rem); line-height: 1.5; @@ -44,7 +85,7 @@ li { align-items: center; } -li:hover { +li:hover { font-size: clamp(1.05rem, 2.6vw, 1.2rem); padding-left: 35px; } @@ -55,7 +96,7 @@ li a { word-break: break-word; } -#available { +#available { padding-left: clamp(20px, 5vw, 40px); margin-bottom: 0.5em; font-size: clamp(1rem, 2.5vw, 1.125rem); @@ -67,7 +108,7 @@ li a { margin-right: 8px; } -#nojs { +#nojs { display: inline-flex; align-items: center; color: #333; @@ -82,14 +123,14 @@ li a { } /* NOSCRIPT LIST */ -#nonenormalul { - list-style: disc inside; - margin: 1em 0; +#nonenormalul { + list-style: disc inside; + margin: 1em 0; padding-left: clamp(20px, 5vw, 40px); } -#nonenormalul li { - list-style: inherit; +#nonenormalul li { + list-style: inherit; margin: 0.5em 0; padding: 8px 0; background: none; @@ -97,16 +138,17 @@ li a { font-size: clamp(0.9rem, 2.2vw, 1rem); } -#nonenormalul li:hover { +#nonenormalul li:hover { font-size: clamp(0.95rem, 2.3vw, 1.05rem); } +/* BUTTONS */ button { margin: 5px 5px 5px 0; background-image: linear-gradient(#f7f8fa, #e7e9ec); border: 1px solid #adb1b8; border-radius: 3px; - box-shadow: rgba(255,255,255,.6) 0 1px 0 inset; + box-shadow: rgba(255, 255, 255, .6) 0 1px 0 inset; color: #0f1111; cursor: pointer; display: inline-block; @@ -124,23 +166,24 @@ button { transition: all 0.2s ease; } -button:active { +button:active { border-color: #a2a6ac; transform: translateY(1px); } -button:hover { +button:hover { border-color: #979aa1; background-image: linear-gradient(#e7e9ec, #d7d9dc); } -button:focus { +button:focus { border-color: #e77600; box-shadow: rgba(228, 121, 17, .5) 0 0 3px 2px; outline: 0; } -button:disabled, button[disabled] { +button:disabled, +button[disabled] { color: #999; border-color: #ccc; cursor: not-allowed; @@ -167,13 +210,78 @@ input#searchbox:focus { outline: 0; } +/* ARTICLE STYLING */ +article { + max-width: 800px; + margin: 1rem 0 1rem 4em; + padding: 1rem; + background-image: linear-gradient(#f7f8fa, #e7e9ec); + border: 1px solid #adb1b8; + box-shadow: rgba(255, 255, 255, .6) 0 1px 0 inset, 0 1px 3px rgba(0, 0, 0, 0.05); + border-radius: 6px; + line-height: 1.6; + font-size: clamp(0.95rem, 2vw, 1.1rem); + transition: background-color 0.3s ease, color 0.3s ease; +} + +article h2 { + font-size: clamp(1.15rem, 3.5vw, 1.5rem); + margin: 0.5rem 0 0.75rem 0; + color: #333; +} + +article p { + margin: 0.5rem 0; +} + +article a { + color: #1a73e8; + text-decoration: underline; + transition: color 0.2s ease; +} + +article a:hover { + color: #0f52ba; +} + +article blockquote { + margin: 0.75rem 0; + padding: 0.5rem 1rem; + border-left: 3px solid #e77600; + background-color: #fff9f2; + font-style: italic; + color: #555; +} + +article code, +article pre { + font-family: "Courier New", Courier, monospace; + background-color: #f0f0f0; + padding: 0.15rem 0.3rem; + border-radius: 3px; + font-size: 0.9em; + overflow-x: auto; +} + +article pre { + padding: 0.75rem; + background-color: #f4f4f4; +} + +article img { + max-width: 100%; + height: auto; + margin: 0.5rem 0; + border-radius: 4px; +} + /* DARK MODE */ body.dark-mode { background-color: #1e1e1e; color: #e0e0e0; } -body.dark-mode h1 { +body.dark-mode h1 { color: #f0f0f0; } @@ -181,11 +289,11 @@ body.dark-mode hr { border-top-color: #444; } -body.dark-mode #nojs { +body.dark-mode #nojs { color: #f0f0f0; } -body.dark-mode li { +body.dark-mode li { background-image: url("../../css/icons/item.webp"); } @@ -193,7 +301,7 @@ body.dark-mode button { background-image: linear-gradient(#3a3a3a, #2a2a2a); color: #e0e0e0; border-color: #555; - box-shadow: rgba(0,0,0,.6) 0 1px 0 inset; + box-shadow: rgba(0, 0, 0, .6) 0 1px 0 inset; } body.dark-mode button:hover { @@ -219,44 +327,111 @@ body.dark-mode a:visited { color: #9a7aff; } +body.dark-mode article { + background-image: none; + background-color: #2e2e2e; + color: #e0e0e0; + border-color: #555; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); +} + +body.dark-mode article h1, +body.dark-mode article h2, +body.dark-mode article h3, +body.dark-mode article h4, +body.dark-mode article h5, +body.dark-mode article h6 { + color: #f0f0f0; +} + +body.dark-mode article a { + color: #4ea1ff; +} + +body.dark-mode article a:hover { + color: #1e90ff; +} + +body.dark-mode article blockquote { + border-left-color: #e77600; + background-color: #3a3a3a; + color: #ccc; +} + +body.dark-mode article code, +body.dark-mode article pre { + background-color: #3a3a3a; + color: #e0e0e0; +} + /* MOBILE OPTIMIZATIONS */ @media (max-width: 768px) { - body { - padding: 15px; + main { + padding: 15px; } li { - padding: 14px 0 14px 35px; - background-size: 18px 23px; + padding: 14px 0 14px 35px; + background-size: 18px 23px; } button { - width: 100%; - margin: 5px 0; + width: 100%; + margin: 5px 0; } input#searchbox { - max-width: 100%; + max-width: 100%; } } -@media (max-width: 480px) { - body { - padding: 10px; +@media (max-width: 1400px) { + main { + padding: 10px; } ul { - padding-left: 10px; + padding-left: 10px; } #available { - padding-left: 10px; + padding-left: 10px; + } + + article { + margin-left: auto; + } + + button { + font-size: clamp(0.8rem, 2vw, 0.9rem); + padding: 10px; + min-height: auto; + margin: 5px 0; + white-space: normal; + } + + button.page-number { + display: none; + } + + #pagination-controls { + display: flex; + justify-content: flex-start; + gap: 8px; + } + + #pagination-controls button { + width: auto; + padding: 8px 12px; + min-height: auto; + white-space: nowrap; + margin: 0; } } /* HIGH DPI DISPLAYS */ @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { li { - background-size: 15px 20px; + background-size: 15px 20px; } -} \ No newline at end of file +} diff --git a/html/base/index.html b/html/base/index.html index 09d6d6d..4427136 100644 --- a/html/base/index.html +++ b/html/base/index.html @@ -13,6 +13,7 @@
+