]>
git.scottworley.com Git - paperdoorknob/blob - glowfic.py
1 # paperdoorknob: Print glowfic
3 # This program is free software: you can redistribute it and/or modify it
4 # under the terms of the GNU General Public License as published by the
5 # Free Software Foundation, version 3.
8 from dataclasses
import dataclass
10 from urllib
.parse
import parse_qsl
, urlencode
, urlparse
, urlunparse
12 from typing
import Iterable
14 from bs4
import BeautifulSoup
15 from bs4
.element
import Tag
17 from images
import ImageStore
18 from texify
import Texifier
21 def _removeViewFromURL(url
: str) -> str:
23 old_qs
= parse_qsl(u
.query
)
24 new_qs
= [(k
, v
) for k
, v
in old_qs
if k
!= 'view']
25 return urlunparse(u
._replace
(query
=urlencode(new_qs
)))
28 def nonFlatURL(url
: str) -> str:
29 return _removeViewFromURL(url
)
32 def flatURL(url
: str) -> str:
33 u
= urlparse(_removeViewFromURL(url
))
34 qs
= parse_qsl(u
.query
) + [('view', 'flat')]
35 return urlunparse(u
._replace
(query
=urlencode(qs
)))
38 @dataclass(frozen
=True)
42 screen_name
: Tag |
None
46 # We avoid the name "post" because the Glowfic community uses the term
48 # * The Glowfic software sometimes uses "post" to refer to a whole thread
49 # (in the URL), sometimes uses "post" to refer to chunks (in the CSS),
50 # but mostly uses "post" to refer to just the first chunk in a thread
51 # (in the HTML and UI). The non-first chunks are "replies".
52 # * Readers and this software don't need to distinguish first-chunks and
54 # * Humans in the community tend to use "posts" to mean chunks.
59 def __init__(self
, dom
: BeautifulSoup
) -> None:
62 def title(self
) -> str |
None:
63 span
= self
._dom
.findChild("span", id="post-title")
64 if not isinstance(span
, Tag
):
66 return span
.text
.strip()
68 def chunkDOMs(self
) -> Iterable
[Tag
]:
72 text
= body
.find_next("div", class_
="post-post")
73 assert isinstance(text
, Tag
)
76 def the_replies() -> Iterable
[Tag
]:
77 rs
= self
._dom
.find_all("div", class_
="post-reply")
78 assert all(isinstance(r
, Tag
) for r
in rs
)
81 return itertools
.chain([text()], the_replies())
84 def makeChunk(chunk_dom
: Tag
, image_store
: ImageStore
) -> Chunk
:
86 def getIcon() -> str |
None:
87 icon_div
= chunk_dom
.findChild('div', class_
='post-icon')
90 assert isinstance(icon_div
, Tag
)
91 icon_img
= icon_div
.findChild('img')
94 assert isinstance(icon_img
, Tag
)
95 return image_store
.get_image(icon_img
.attrs
['src'])
97 def getByClass(css_class
: str) -> Tag |
None:
98 tag
= chunk_dom
.findChild('div', class_
=css_class
)
99 assert tag
is None or isinstance(tag
, Tag
)
102 def stripHREF(tag
: Tag
) -> None:
103 for c
in tag
.findChildren("a"):
104 if "href" in c
.attrs
:
107 def getMeta(css_class
: str) -> Tag |
None:
108 tag
= getByClass(css_class
)
114 content
= chunk_dom
.findChild('div', class_
='post-content')
115 assert isinstance(content
, Tag
)
117 return Chunk(getIcon(),
118 getMeta('post-character'),
119 getMeta('post-screenname'),
120 getMeta('post-author'),
124 def renderChunk(texifier
: Texifier
, chunk
: Chunk
) -> bytes:
127 br
'\glowicon{%s}' % chunk
.icon
.encode('UTF-8') if chunk
.icon
else b
'',
129 texifier
.texify(chunk
.character
) if chunk
.character
else b
'',
131 texifier
.texify(chunk
.screen_name
) if chunk
.screen_name
else b
'',
133 texifier
.texify(chunk
.author
) if chunk
.author
else b
'',
135 texifier
.texify(chunk
.content
)])
138 ContentOnlyLayout
= br
'''
139 \newcommand{\glowhead}[4]{}
143 BelowIconLayout
= br
'''
144 \newcommand{\glowhead}[4]{\wrapstuffclear
147 \begin{varwidth}{0.5\textwidth}
148 \smash{\parbox[t][0pt]{0pt}{
149 \setlength{\fboxrule}{0.2pt}
150 \setlength{\fboxsep}{0pt}
152 \fbox{\hspace{107mm}}
157 {#1}{\\*}#2\ifnotempty
158 {#2}{\\*}#3\ifnotempty
170 # Why is \textwidth not the width of the text?
171 # Why is the width of the text .765\textwidth?
172 BesideIconLayout
= br
'''
173 \newcommand{\glowhead}[4]{
179 \parbox[b]{.765\textwidth}{
182 {#2}{\\*}#3\ifnotempty