# paperdoorknob: Print glowfic # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation, version 3. from dataclasses import dataclass import itertools from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse from typing import Iterable from bs4 import BeautifulSoup from bs4.element import Tag from images import ImageStore from texify import Texifier def _removeViewFromURL(url: str) -> str: u = urlparse(url) old_qs = parse_qsl(u.query) new_qs = [(k, v) for k, v in old_qs if k != 'view'] return urlunparse(u._replace(query=urlencode(new_qs))) def nonFlatURL(url: str) -> str: return _removeViewFromURL(url) def flatURL(url: str) -> str: u = urlparse(_removeViewFromURL(url)) qs = parse_qsl(u.query) + [('view', 'flat')] return urlunparse(u._replace(query=urlencode(qs))) @dataclass(frozen=True) class Chunk: icon: str | None character: Tag | None screen_name: Tag | None author: Tag | None content: Tag # We avoid the name "post" because the Glowfic community uses the term # inconsistently: # * The Glowfic software sometimes uses "post" to refer to a whole thread # (in the URL), sometimes uses "post" to refer to chunks (in the CSS), # but mostly uses "post" to refer to just the first chunk in a thread # (in the HTML and UI). The non-first chunks are "replies". # * Readers and this software don't need to distinguish first-chunks and # non-first-chunks. # * Humans in the community tend to use "posts" to mean chunks. class Thread: def __init__(self, html: BeautifulSoup) -> None: self._html = html def chunkDOMs(self) -> Iterable[Tag]: def text() -> Tag: body = self._html.body assert body text = body.find_next("div", class_="post-post") assert isinstance(text, Tag) return text def the_replies() -> Iterable[Tag]: rs = self._html.find_all("div", class_="post-reply") assert all(isinstance(r, Tag) for r in rs) return rs return itertools.chain([text()], the_replies()) def makeChunk(chunk_dom: Tag, image_store: ImageStore) -> Chunk: def getIcon() -> str | None: icon_div = chunk_dom.findChild('div', class_='post-icon') if icon_div is None: return None assert isinstance(icon_div, Tag) icon_img = icon_div.findChild('img') if icon_img is None: return None assert isinstance(icon_img, Tag) return image_store.get_image(icon_img.attrs['src']) def getByClass(css_class: str) -> Tag | None: tag = chunk_dom.findChild('div', class_=css_class) assert tag is None or isinstance(tag, Tag) return tag def stripHREF(tag: Tag) -> None: for c in tag.findChildren("a"): if "href" in c.attrs: del c.attrs["href"] def getMeta(css_class: str) -> Tag | None: tag = getByClass(css_class) if tag is None: return None stripHREF(tag) return tag content = chunk_dom.findChild('div', class_='post-content') assert isinstance(content, Tag) return Chunk(getIcon(), getMeta('post-character'), getMeta('post-screenname'), getMeta('post-author'), content) def renderChunk(texifier: Texifier, chunk: Chunk) -> bytes: return b''.join([ br'\glowhead{', br'\glowicon{%s}' % chunk.icon.encode('UTF-8') if chunk.icon else b'', b'}{', texifier.texify(chunk.character) if chunk.character else b'', b'}{', texifier.texify(chunk.screen_name) if chunk.screen_name else b'', b'}{', texifier.texify(chunk.author) if chunk.author else b'', b'}', texifier.texify(chunk.content)]) ContentOnlyLayout = br''' \newcommand{\glowhead}[4]{} ''' BelowIconLayout = br''' \newcommand{\glowhead}[4]{\wrapstuffclear \begin{wrapstuff}[l] \fbox{ \begin{varwidth}{0.5\textwidth} \smash{\parbox[t][0pt]{0pt}{ \setlength{\fboxrule}{0.2pt} \setlength{\fboxsep}{0pt} \vspace{-3.4pt} \fbox{\hspace{107mm}} }\\*} \vspace{-1em} \begin{center} #1\ifnotempty {#1}{\\*}#2\ifnotempty {#2}{\\*}#3\ifnotempty {#3}{\\*}#4 \end{center} \end{varwidth} } \end{wrapstuff} \strut \noindent}''' # Why is \textwidth not the width of the text? # Why is the width of the text .765\textwidth? BesideIconLayout = br''' \newcommand{\glowhead}[4]{ \strut \noindent\fbox{ #1 \parbox[b]{.765\textwidth}{ \begin{center} #2\ifnotempty {#2}{\\*}#3\ifnotempty {#3}{\\*}#4 \end{center} } }\\* \vspace{-0.75em}\\* \noindent}'''