.-----------------------------------------------------------------------------.
| This script fetches gemtext pages using the spartan:// protocol.            |
|                                                                             |
| It's not a full browser, but rather a reference implementation originally   |
| designed by Michael Lazar to:                                               |
|                                                                             |
| * retrieve single pages                                                     |
| * follow redirects                                                          |
| * upload files for specific page requests using "--infile"                  |
|                                                                             |
| The primary goal was to demonstrate full interaction with the spartan       |
| protocol and as a template for further spartan development.                 |
|                                                                             |
| Main Additions to the script include:                                       |
|                                                                             |
| * gemtext (gmi) rendering                                                   |
| * smart word-wrap                                                           |
| * local file support (sv path/to/filename.gmi)                              |
| * a new built-in help                                                       |
|                                                                             |
| The script retains the original spartan code and remains a single-page      |
| retriever at its core.                                                      |
|                                                                             |
| For more information about spartan: finger spartanware@happynetbox.com      |
'-----------------------------------------------------------------------------'

#!/usr/bin/env python3

"""
.-------------------------------------------------------------------.
| This project is licensed under the Blue Oak Model License 1.0.0.  |
| See: https://blueoakcouncil.org/license/1.0.0                     |
'-------------------------------------------------------------------'

.-------------------------------------------------------------------.
| Version 3.0 : Local File Support (06may2025)                      |
|-------------------------------------------------------------------|
|                                                                   |
| Added support for local Gemtext files:                            |
| - Can now pass a .gmi file or path directly on the command line   |
| - All other extensions (txt, etc) will be displayed not rendered  |
|                                                                   |
| Unified Dispatcher:                                               |
| - New logic routes either to file renderer or Spartan fetcher     |
|                                                                   |
| Official Name: 'Spartan Viewer' and/or sv                         |
'-------------------------------------------------------------------'

.-------------------------------------------------------------------.
| Version 2.0 : GemText Rendering, Word-wrap... (04may2025)         |
|-------------------------------------------------------------------|
|                                                                   |
| Added Gemtext rendering:                                          |
| - Styled output for headers, links, blockquotes, lists, and       |
|   horizontal rules using ANSI escape codes.                       |
| - Smart paragraph spacing and formatting.                         |
|                                                                   |
| Smart word-wrap                                                   |
| - Dynamically adapts text wrapping to fit the current terminal    |
|   width using shutil.get_terminal_size() and textwrap             |
|                                                                   |
| Added ANSI-styled Gemtext output for use with tools like less -R  |
|                                                                   |
| Enhanced Help Section                                             |
| - custom help, multiple -flags or no arguments                    |
'-------------------------------------------------------------------'

.-------------------------------------------------------------------.
| Version 1.0 : Initial release                                     |
|-------------------------------------------------------------------|
| Original work by Michael Lazar                                    |
|                                                                   |
| A reference spartan:// protocol client                            |
|                                                                   |
| Copyright (c) Michael Lazar                                       |
| Blue Oak Model License 1.0.0                                      |
'-------------------------------------------------------------------'
"""

import argparse
import shutil
import socket
import sys
import textwrap
import os
from urllib.parse import urlparse, unquote_to_bytes, quote_from_bytes

# --- BEGIN NEW USER ADDITIONS: New Help Section ---

custom_help = f"""
sv: Spartan/Gemtext Viewer - Version 3.0
----------------------------------------
Fetches and renders a Spartan URL or local .gmi file as styled Gemtext.
Output is ANSI-formatted and auto-wraps to fit your terminal width.

Usage:
  {os.path.basename(sys.argv[0])} spartan://host/resource
  {os.path.basename(sys.argv[0])} path/to/file.gmi
  {os.path.basename(sys.argv[0])} spartan://host/search --infile query.txt
  \033[1m{os.path.basename(sys.argv[0])} spartan://host/page.gmi | less -R\033[0m

Features:
  - Spartan Fetching     Connects to a Spartan server and fetches content
  - Local File Support   View local .gmi files directly (offline mode)
  - Gemtext Rendering    Styled headers, links, lists, quotes, rules
  - Smart Word-Wrap      Adapts to terminal size for clean display
  - --infile Support     Sends file contents as Spartan input (e.g. forms)
  - Redirect Handling    Follows simple Spartan redirects (status 3)

License: Blue Oak Model License 1.0.0
Author: Michael Lazar | Enhancements: spartanware@happynetbox.com
"""

# Show help if no arguments or any help flag is passed
if len(sys.argv) == 1 or any(arg.lower() in ("/?", "-?", "-h", "--h", "--help", "-help") for arg in sys.argv[1:]):
    print(custom_help)
    sys.exit(0)

# --- END NEW USER ADDITIONS: New Help Section ---


# --- BEGIN NEW USER FUNCTION: Gemtext Renderer (spartanware@happynetbox.com / ChatGPT) ---

def render_gemtext(fp):
    # Dynamically determine the terminal width
    term_width = shutil.get_terminal_size((80, 20)).columns
    wrapper = textwrap.TextWrapper(width=term_width, replace_whitespace=False)

    in_preformatted = False
    last_line_empty = False  # Track whether the last line was empty

    for raw_line in fp:
        line = raw_line.decode("utf-8", errors="replace").rstrip()

        # Toggle preformatted text block on ``` lines
        if line.strip() == "```":
            in_preformatted = not in_preformatted
            continue  # Don't show ``` lines

        if in_preformatted:
            print(line)  # Print preformatted block verbatim
            last_line_empty = False
            continue

        # Render headers
        if line.startswith("### "):
            print(f"\033[4m{line[4:]}\033[0m")  # Underlined (h3)
            last_line_empty = False
        elif line.startswith("## "):
            print(f"\033[1m{line[3:]}\033[0m")  # Bold (h2)
            last_line_empty = False
        elif line.startswith("# "):
            print(f"\033[1;4m{line[2:]}\033[0m")  # Bold + Underlined (h1)
            last_line_empty = False

        # Render links
        elif line.startswith("=>"):
            parts = line[2:].strip().split(maxsplit=1)
            url = parts[0]
            label = parts[1] if len(parts) > 1 else url
            print(f"\033[38;5;110m→ {label} [{url}]\033[0m")
            last_line_empty = False

        # Render blockquotes
        elif line.startswith("> "):
            print(f"\033[2m{line[2:]}\033[0m")  # Dim
            last_line_empty = False

        # Render list items
        elif line.startswith("* "):
            print(f"* {line[2:]}")
            last_line_empty = False

        # Horizontal rules (3 to 10 dashes)
        elif 3 <= len(line.strip()) <= 10 and set(line.strip()) == {"-"}:
            print("________________________________________________________________________")
            last_line_empty = False

        # Paragraph spacing
        elif line.strip() == "":
            if not last_line_empty:
                print()
                last_line_empty = True

        # Word-wrap default lines
        else:
            for wrapped_line in wrapper.wrap(line):
                print(wrapped_line)
            last_line_empty = False

# --- END Gemtext Renderer (spartanware@happynetbox.com / ChatGPT) ---


# --- BEGIN ORIGINAL FUNCTION FROM MICHAEL LAZAR ---

def fetch_url(url, infile=None):
    url_parts = urlparse(url)
    if url_parts.scheme != "spartan":
        raise ValueError("Unrecognized URL scheme")

    host = url_parts.hostname
    port = url_parts.port or 300
    path = url_parts.path or "/"
    query = url_parts.query

    redirect_url = None

    with socket.create_connection((host, port)) as sock:
        if infile:
            data = infile.read()
        elif query:
            data = unquote_to_bytes(query)
        else:
            data = b""

        encoded_host = host.encode("idna")
        encoded_path = quote_from_bytes(unquote_to_bytes(path)).encode("ascii")
        sock.send(b"%s %s %d\r\n" % (encoded_host, encoded_path, len(data)))
        sock.send(data)

        fp = sock.makefile("rb")
        response = fp.readline(4096).decode("ascii").strip("\r\n")
        print(response, file=sys.stderr, flush=True)

        parts = response.split(" ", maxsplit=1)
        code, meta = int(parts[0]), parts[1]
        if code == 2:
            render_gemtext(fp)
        elif code == 3:
            redirect_url = url_parts._replace(path=meta).geturl()

    if redirect_url:
        fetch_url(redirect_url)

# --- END ORIGINAL FUNCTION FROM MICHAEL LAZAR ---


# --- BEGIN MODIFIED ARGUMENT PARSING (Based on Michael Lazar) ---
# --- Modified for New Help Support re: command-line order consistency ---

parser = argparse.ArgumentParser(add_help=False)  # Disable default -h/--help
parser.add_argument("url")
parser.add_argument("--infile", type=argparse.FileType("rb"))
args = parser.parse_args()

# --- END MODIFIED ARGUMENT PARSING CODE ---

# --- BEGIN NEW USER ADDITIONS: Local Gemtext File Support (spartanware@happynetbox.com / ChatGPT) ---

def try_local_file_or_spartan(url, infile=None):
    if os.path.isfile(url):
        ext = os.path.splitext(url)[1].lower()
        with open(url, "rb") as fp:
            if ext == ".gmi":
                render_gemtext(fp)
            else:
                for line in fp:
                    print(line.decode("utf-8", errors="replace").rstrip())
    else:
        fetch_url(url, infile)

# --- END NEW USER ADDITIONS: Local Gemtext File Support ---


# --- BEGIN MODIFIED FINAL CALL: Local or Spartan Dispatch ---

try:
    try_local_file_or_spartan(args.url, args.infile)
except ValueError as e:
    print(f"Error: {e}", file=sys.stderr)
except KeyboardInterrupt:
    pass

# --- END MODIFIED FINAL CALL ---

.-----------------------------. .--------------. .---------------------------.
|spartanviewer@happynetbox.com|-| May 08, 2025 |-| neocities.org/spartanware |
'-----------------------------' '--------------' '---------------------------'