diff options
| author | alex <alex@pdp7.net> | 2023-10-13 16:11:25 +0200 |
|---|---|---|
| committer | alex <alex@pdp7.net> | 2023-10-13 16:11:25 +0200 |
| commit | 250201b433c0a99f6cabcb2596bfe43f1a5a3968 (patch) | |
| tree | 8dc5de16ed4315b3ab4fcc001b7dbd74d475c4b3 /blog_experiment/blog | |
| parent | 1a80ac63854ba4ea28f81194ad15314771e979d5 (diff) | |
Moving to prod!
Diffstat (limited to 'blog_experiment/blog')
| -rw-r--r-- | blog_experiment/blog/__init__.py | 54 | ||||
| -rw-r--r-- | blog_experiment/blog/__main__.py | 36 | ||||
| -rw-r--r-- | blog_experiment/blog/blog_pages.py | 139 | ||||
| -rw-r--r-- | blog_experiment/blog/gemtext.py | 223 | ||||
| -rw-r--r-- | blog_experiment/blog/html.py | 101 | ||||
| -rw-r--r-- | blog_experiment/blog/meta.py | 13 | ||||
| -rw-r--r-- | blog_experiment/blog/page.py | 37 | ||||
| -rw-r--r-- | blog_experiment/blog/pretty.py | 5 |
8 files changed, 0 insertions, 608 deletions
diff --git a/blog_experiment/blog/__init__.py b/blog_experiment/blog/__init__.py deleted file mode 100644 index 81332ddd..00000000 --- a/blog_experiment/blog/__init__.py +++ /dev/null @@ -1,54 +0,0 @@ -import pathlib -import re - -import bicephalus - -import htmlgenerator as h - -from blog import blog_pages, page, html, pretty, gemtext - - -class SimplePage(page.BasePage): - def __init__(self, request, url, title): - super().__init__(request) - self.url = url - self.title = title - - def get_gemini_content(self): - return ( - bicephalus.Status.OK, - "text/gemini", - pathlib.Path(f"static{self.url}index.gmi").read_text(), - ) - - def get_http_content(self): - return ( - bicephalus.Status.OK, - "text/html", - pretty.pretty_html(h.render( - h.HTML( - h.HEAD( - h.TITLE(self.title), - ), - h.BODY(*html.gemini_to_html(gemtext.parse(self.get_gemini_content()[2]))) - ), {})), - ) - - -def handler(request: bicephalus.Request) -> bicephalus.Response: - if request.path == "/": - return blog_pages.Root(request).response() - if re.match(r"/\d{4}/\d{2}/.*/", request.path): - blog_file = pathlib.Path("content") / (request.path[1:-1] + ".gmi") - if blog_file.exists(): - return blog_pages.EntryPage(request, blog_file).response() - if request.path == "/feed/" and request.proto == bicephalus.Proto.HTTP: - return blog_pages.Root(request).feed() - if request.path == "/about/": - return SimplePage(request, request.path, "About Álex Córcoles").response() - if request.path == "/laspelis/": - return SimplePage(request, request.path, "laspelis").response() - if re.match(r"/laspelis/\d+/?", request.path): - return SimplePage(request, request.path.removesuffix("/") + "/", request.path).response() - - return page.NotFound(request).response() diff --git a/blog_experiment/blog/__main__.py b/blog_experiment/blog/__main__.py deleted file mode 100644 index 21033902..00000000 --- a/blog_experiment/blog/__main__.py +++ /dev/null @@ -1,36 +0,0 @@ -import argparse -import logging -import sys - -from bicephalus import main as bicephalus_main -from bicephalus import otel -from bicephalus import ssl - -import blog - -from blog import meta - - -def main(): - otel.configure_logging(logging.INFO) - - parser = argparse.ArgumentParser() - parser.add_argument("--key-cert", nargs=2, metavar=("KEY", "CERT",), help="Path to a key and a file") - parser.add_argument("schema") - parser.add_argument("host") - args = parser.parse_args() - meta.SCHEMA = args.schema - meta.HOST = args.host - - if args.key_cert: - key, cert = args.key_cert - with ssl.ssl_context_from_files(cert, key) as ssl_context: - bicephalus_main.main(blog.handler, ssl_context, 8000) - sys.exit(0) - - with ssl.temporary_ssl_context("localhost") as ssl_context: - bicephalus_main.main(blog.handler, ssl_context, 8000) - sys.exit(0) - -if __name__ == "__main__": - main() diff --git a/blog_experiment/blog/blog_pages.py b/blog_experiment/blog/blog_pages.py deleted file mode 100644 index 7802c00b..00000000 --- a/blog_experiment/blog/blog_pages.py +++ /dev/null @@ -1,139 +0,0 @@ -import datetime -import itertools -import pathlib -import textwrap - -import bicephalus - -import htmlgenerator as h - -from feedgen import feed - -from blog import html, page, gemtext, meta, pretty - - -def gemini_links(): - return "\n".join([f"=> {url} {text}" for text, url in meta.LINKS]) - - -class Entry: - def __init__(self, path: pathlib.Path): - assert path.is_relative_to(pathlib.Path("content")), f"bad path {path}" - self.path = path - self.content = path.read_text() - - @property - def title(self): - return self.content.splitlines()[0][2:] - - @property - def posted(self): - return datetime.date.fromisoformat(self.content.splitlines()[1]) - - @property - def uri(self): - return f"/{self.path.parts[1]}/{self.path.parts[2]}/{self.path.stem}/" - - @property - def edit_url(self): - return f"https://github.com/alexpdp7/gemini_blog/edit/master/content{self.uri[:-1]}.gmi" - - def html(self): - parsed = gemtext.parse(self.content) - - assert isinstance(parsed[0], gemtext.Header) - assert parsed[0].level == 1 - assert isinstance(parsed[1], gemtext.Line) - assert parsed[2] == gemtext.Line("") - - result = html.gemini_to_html(parsed[3:]) - result.append(h.P(h.A("Editar", href=self.edit_url))) - return result - - -class Root(page.BasePage): - def entries(self): - entries = map(Entry, pathlib.Path("content").glob("*/*/*.gmi")) - return sorted(entries, key=lambda e: e.posted, reverse=True) - - def get_gemini_content(self): - posts = "\n".join([f"=> {e.uri} {e.posted} {e.title}" for e in self.entries()]) - content = ( - textwrap.dedent( - f"""\ - # {meta.TITLE} - - ## {meta.SUBTITLE} - - """ - ) - + gemini_links() - + f"\n{meta.EMAIL_TEXT}\n" - + "\n" - + posts - ) - return bicephalus.Status.OK, "text/gemini", content - - def get_http_content(self): - posts = [ - (h.H3(h.A(f"{e.title} ({e.posted})", href=e.uri))) for e in self.entries() - ] - return ( - bicephalus.Status.OK, - "text/html", - html.html_template(*itertools.chain(posts)), - ) - - def feed(self): - fg = feed.FeedGenerator() - fg.title(meta.TITLE) - fg.subtitle(meta.SUBTITLE) - fg.link(href=f"{meta.SCHEMA}://{meta.HOST}", rel="self") - - for entry in self.entries()[0:10]: - fe = fg.add_entry() - url = f"{meta.SCHEMA}://{meta.HOST}/{entry.uri}" - fe.link(href=url) - fe.published(datetime.datetime.combine(entry.posted, datetime.datetime.min.time(), tzinfo=datetime.timezone.utc)) - fe.title(entry.title) - html = h.render(h.BaseElement(*entry.html()), {}) - html = pretty.pretty_html(html) - fe.content(html, type="html") - - return bicephalus.Response( - status=bicephalus.Status.OK, - content_type="application/rss+xml", - content=fg.rss_str(pretty=True), - ) - - -class EntryPage(page.BasePage): - def __init__(self, request, path): - super().__init__(request) - self.path = path - self.entry = Entry(path) - - def get_gemini_content(self): - content = ( - textwrap.dedent(f"""\ - => gemini://{meta.HOST} alex.corcoles.net - {meta.EMAIL_TEXT} - - """) + - self.entry.content + - textwrap.dedent(f"""\ - => {self.entry.edit_url} Editar - """) - ) - - return bicephalus.Status.OK, "text/gemini", content - - def get_http_content(self): - return ( - bicephalus.Status.OK, - "text/html", - html.html_template( - *self.entry.html(), - page_title=f"{self.entry.title} - {self.entry.posted}", - ), - ) diff --git a/blog_experiment/blog/gemtext.py b/blog_experiment/blog/gemtext.py deleted file mode 100644 index 66298e3f..00000000 --- a/blog_experiment/blog/gemtext.py +++ /dev/null @@ -1,223 +0,0 @@ -import dataclasses -import re -import typing - - -def parse(s): - """ - >>> parse('''# Header 1 - ... - ... ## Header 2 - ... - ... ### Header 3 - ... - ... * List 1 - ... * List 2 - ... - ... > First line quote. - ... > Second line of quote. - ... - ... ``` - ... Fenced - ... Lines - ... ``` - ... - ... Paragraph. - ... - ... Another paragraph. - ... ''') - [Header(level=1, text='Header 1'), - Line(text=''), - Header(level=2, text='Header 2'), - Line(text=''), - Header(level=3, text='Header 3'), - Line(text=''), - List(items=[ListItem(text='List 1'), - ListItem(text='List 2')]), - Line(text=''), - BlockQuote(lines=[BlockQuoteLine(text='First line quote.'), - BlockQuoteLine(text='Second line of quote.')]), - Line(text=''), - Pre(content='Fenced\\nLines\\n'), - Line(text=''), - Line(text='Paragraph.'), - Line(text=''), - Line(text='Another paragraph.')] - """ - - lines = s.splitlines() - - i = 0 - gem = [] - - while i < len(lines): - line = parse_line(lines[i]) - - if isinstance(line, Link): - gem.append(line) - i = i + 1 - continue - - if isinstance(line, Header): - gem.append(line) - i = i + 1 - continue - - if isinstance(line, ListItem): - items = [] - while i < len(lines) and isinstance(parse_line(lines[i]), ListItem): - items.append(parse_line(lines[i])) - i = i + 1 - gem.append(List(items)) - continue - - if isinstance(line, BlockQuoteLine): - quotes = [] - while i < len(lines) and isinstance(parse_line(lines[i]), BlockQuoteLine): - quotes.append(parse_line(lines[i])) - i = i + 1 - gem.append(BlockQuote(quotes)) - continue - - if isinstance(line, PreFence): - content = "" - i = i + 1 - while i < len(lines) and not isinstance(parse_line(lines[i]), PreFence): - content += lines[i] - content += "\n" - i = i + 1 - gem.append(Pre(content)) - i = i + 1 - continue - - gem.append(line) - i = i + 1 - - return gem - - -def parse_line(l): - if Link.is_link(l): - return Link(l) - if Header.is_header(l): - return Header(l) - if ListItem.is_list_item(l): - return ListItem(l) - if BlockQuoteLine.is_block_quote_line(l): - return BlockQuoteLine(l) - if PreFence.is_pre_fence(l): - return PreFence() - return Line(l) - - -@dataclasses.dataclass -class Link: - """ - >>> Link("=> http://example.com") - Link(url='http://example.com', text=None) - - >>> Link("=> http://example.com Example text") - Link(url='http://example.com', text='Example text') - """ - - url: str - text: typing.Optional[str] - - def __init__(self, line: str): - assert Link.is_link(line) - parts = line.split(None, 2) - self.url = parts[1] - self.text = parts[2] if len(parts) > 2 else None - - @staticmethod - def is_link(line: str): - return line.startswith("=>") - -@dataclasses.dataclass -class Header: - """ - >>> Header("# Level one") - Header(level=1, text='Level one') - - >>> Header("## Level two") - Header(level=2, text='Level two') - - >>> Header("### Level three") - Header(level=3, text='Level three') - """ - - level: int - text: str - - def __init__(self, line: str): - assert Header.is_header(line) - hashes, self.text = line.split(None, 1) - self.level = len(hashes) - - @staticmethod - def is_header(line: str): - return re.match("#{1,3} .*", line) - -@dataclasses.dataclass -class ListItem: - """ - >>> ListItem("* foo") - ListItem(text='foo') - """ - - text: str - - def __init__(self, line: str): - assert ListItem.is_list_item(line) - self.text = line[2:] - - @staticmethod - def is_list_item(line: str): - return line.startswith("* ") - - -@dataclasses.dataclass -class BlockQuoteLine: - """ - >>> BlockQuoteLine("> foo") - BlockQuoteLine(text='foo') - - >>> BlockQuoteLine(">foo") - BlockQuoteLine(text='foo') - """ - - text: str - - def __init__(self, line: str): - assert BlockQuoteLine.is_block_quote_line(line) - self.text = line[2:] if line.startswith("> ") else line[1:] - - @staticmethod - def is_block_quote_line(line: str): - return line.startswith(">") - - -class PreFence: - @staticmethod - def is_pre_fence(line: str): - return line == "```" - - -@dataclasses.dataclass -class Line: - text: str - - -@dataclasses.dataclass -class List: - items: typing.List[ListItem] - - -@dataclasses.dataclass -class BlockQuote: - lines: typing.List[BlockQuoteLine] - - -@dataclasses.dataclass -class Pre: - content: str diff --git a/blog_experiment/blog/html.py b/blog_experiment/blog/html.py deleted file mode 100644 index 8aa92f4a..00000000 --- a/blog_experiment/blog/html.py +++ /dev/null @@ -1,101 +0,0 @@ -import itertools - -import htmlgenerator as h - -from blog import meta, pretty, gemtext - - -def html_template(*content, page_title=None): - title = [h.A(meta.TITLE, href=f"{meta.SCHEMA}://{meta.HOST}")] - if page_title: - title += f" - {page_title}" - - title = h.BaseElement(*title) - - links = list(itertools.chain(*[(h.A(text, href=href), ", ") for text, href in meta.LINKS])) - - links += h.BaseElement(f" {meta.EMAIL_TEXT}") - - return pretty.pretty_html(h.render( - h.HTML( - h.HEAD( - h.TITLE(meta.TITLE + (f" - {page_title}" if page_title else "")), - h.LINK(rel="alternate", type="application/rss+xml", title=meta.TITLE, href=f"{meta.SCHEMA}://{meta.HOST}/feed/"), - ), - h.BODY( - h.H1(title), - h.H2(meta.SUBTITLE), - h.P(*links), - *content, - ), - doctype="html", - ), - {}, - )) - - -def gemini_to_html(parsed): - i = 0 - result = [] - while i < len(parsed): - gem_element = parsed[i] - - if isinstance(gem_element, gemtext.Header): - header = [h.H1, h.H2, h.H3, h.H4, h.H5, h.H6][gem_element.level - 1] - result.append(header(gem_element.text)) - i = i + 1 - continue - - if isinstance(gem_element, gemtext.List): - result.append(h.UL(*[h.LI(i.text) for i in gem_element.items])) - i = i + 1 - continue - - if isinstance(gem_element, gemtext.Link): - url = gem_element.url - if url.startswith("gemini://"): - if url.startswith("gemini://alex.corcoles.net/"): - url = url.replace("gemini://alex.corcoles.net/", f"{meta.SCHEMA}://{meta.HOST}/") - else: - url = url.replace("gemini://", "https://portal.mozz.us/gemini/") - - result.append(h.P(h.A(gem_element.text or gem_element.url, href=url))) - i = i + 1 - continue - - if gem_element == gemtext.Line(""): - i = i + 1 - continue - - if isinstance(gem_element, gemtext.BlockQuote): - content = [] - for line in gem_element.lines: - if line.text: - content.append(line.text) - content.append(h.BR()) - result.append(h.BLOCKQUOTE(*content)) - i = i + 1 - continue - - if isinstance(gem_element, gemtext.Line): - paragraph = [gem_element.text] - i = i + 1 - while i < len(parsed): - gem_element = parsed[i] - if isinstance(gem_element, gemtext.Line) and gem_element.text != "": - paragraph.append(h.BR()) - paragraph.append(gem_element.text) - i = i + 1 - else: - break - result.append(h.P(*paragraph)) - continue - - if isinstance(gem_element, gemtext.Pre): - result.append(h.PRE(gem_element.content)) - i = i + 1 - continue - - assert False, f"unknown element {gem_element}" - - return result diff --git a/blog_experiment/blog/meta.py b/blog_experiment/blog/meta.py deleted file mode 100644 index e92b2acb..00000000 --- a/blog_experiment/blog/meta.py +++ /dev/null @@ -1,13 +0,0 @@ -TITLE = "El blog es mío" -SUBTITLE = "Hay otros como él, pero este es el mío" -HOST = None -SCHEMA = None - -LINKS = ( - ("GitHub", "https://github.com/alexpdp7/"), - ("LinkedIn", "https://es.linkedin.com/in/alexcorcoles"), - ("Project Euler", "https://projecteuler.net/profile/koalillo.png"), - ("Stack Exchange", "https://stackexchange.com/users/13361/alex"), -) - -EMAIL_TEXT = "escríbeme cogiendo el dominio de esta web y cambiando el primer punto por una arroba" diff --git a/blog_experiment/blog/page.py b/blog_experiment/blog/page.py deleted file mode 100644 index fcc4841a..00000000 --- a/blog_experiment/blog/page.py +++ /dev/null @@ -1,37 +0,0 @@ -import bicephalus - - -class BasePage: - def __init__(self, request): - self.request = request - - def response(self): - if self.request.proto == bicephalus.Proto.GEMINI: - status, content_type, content = self.get_gemini_content() - elif self.request.proto == bicephalus.Proto.HTTP: - status, content_type, content = self.get_http_content() - else: - assert False, f"unknown protocol {self.request.proto}" - - return bicephalus.Response( - content=content.encode("utf8"), - content_type=content_type, - status=bicephalus.Status.OK, - ) - - -class NotFound(BasePage): - def get_gemini_content(self): - # TODO: does not work! - return ( - bicephalus.Status.NOT_FOUND, - "text/gemini", - f"{self.request.path} not found", - ) - - def get_http_content(self): - return ( - bicephalus.Status.NOT_FOUND, - "text/html", - f"{self.request.path} not found", - ) diff --git a/blog_experiment/blog/pretty.py b/blog_experiment/blog/pretty.py deleted file mode 100644 index 2ae916a7..00000000 --- a/blog_experiment/blog/pretty.py +++ /dev/null @@ -1,5 +0,0 @@ -from lxml import etree, html - - -def pretty_html(s): - return etree.tostring(html.fromstring(s), pretty_print=True).decode("utf8") |
