]>
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 abc
import ABC
, abstractmethod
9 from dataclasses
import dataclass
11 from urllib
.parse
import parse_qsl
, urlencode
, urlparse
, urlunparse
13 from typing
import Iterable
15 from bs4
import BeautifulSoup
16 from bs4
.element
import Tag
18 from images
import ImageStore
19 from texify
import Texifier
22 def _removeViewFromURL(url
: str) -> str:
24 old_qs
= parse_qsl(u
.query
)
25 new_qs
= [(k
, v
) for k
, v
in old_qs
if k
!= 'view']
26 return urlunparse(u
._replace
(query
=urlencode(new_qs
)))
29 def nonFlatURL(url
: str) -> str:
30 return _removeViewFromURL(url
)
33 def flatURL(url
: str) -> str:
34 u
= urlparse(_removeViewFromURL(url
))
35 qs
= parse_qsl(u
.query
) + [('view', 'flat')]
36 return urlunparse(u
._replace
(query
=urlencode(qs
)))
39 @dataclass(frozen
=True)
43 screen_name
: Tag |
None
47 # We avoid the name "post" because the Glowfic community uses the term
49 # * The Glowfic software sometimes uses "post" to refer to a whole thread
50 # (in the URL), sometimes uses "post" to refer to chunks (in the CSS),
51 # but mostly uses "post" to refer to just the first chunk in a thread
52 # (in the HTML and UI). The non-first chunks are "replies".
53 # * Readers and this software don't need to distinguish first-chunks and
55 # * Humans in the community tend to use "posts" to mean chunks.
58 def chunkDOMs(html
: BeautifulSoup
) -> Iterable
[Tag
]:
62 text
= body
.find_next("div", class_
="post-post")
63 assert isinstance(text
, Tag
)
66 def the_replies() -> Iterable
[Tag
]:
67 rs
= html
.find_all("div", class_
="post-reply")
68 assert all(isinstance(r
, Tag
) for r
in rs
)
71 return itertools
.chain([text()], the_replies())
74 def makeChunk(chunk_dom
: Tag
, image_store
: ImageStore
) -> Chunk
:
76 def getIcon() -> str |
None:
77 icon_div
= chunk_dom
.findChild('div', class_
='post-icon')
80 assert isinstance(icon_div
, Tag
)
81 icon_img
= icon_div
.findChild('img')
84 assert isinstance(icon_img
, Tag
)
85 return image_store
.get_image(icon_img
.attrs
['src'])
87 def getByClass(css_class
: str) -> Tag |
None:
88 tag
= chunk_dom
.findChild('div', class_
=css_class
)
89 assert tag
is None or isinstance(tag
, Tag
)
92 def stripHREF(tag
: Tag
) -> None:
93 for c
in tag
.findChildren("a"):
97 def getMeta(css_class
: str) -> Tag |
None:
98 tag
= getByClass(css_class
)
104 content
= chunk_dom
.findChild('div', class_
='post-content')
105 assert isinstance(content
, Tag
)
107 return Chunk(getIcon(),
108 getMeta('post-character'),
109 getMeta('post-screenname'),
110 getMeta('post-author'),
114 def renderIcon(icon_path
: str |
None, image_size
: float) -> bytes |
None:
115 if icon_path
is None:
117 return b
'\\includegraphics[width=%fmm,height=%fmm,keepaspectratio]{%s}' % (
118 image_size
, image_size
, icon_path
.encode('UTF-8'))
124 def renderChunk(self
, chunk
: Chunk
) -> bytes:
125 raise NotImplementedError()
128 class ContentOnlyLayout(Layout
):
130 def __init__(self
, texifier
: Texifier
) -> None:
131 self
._texifier
= texifier
133 def renderChunk(self
, chunk
: Chunk
) -> bytes:
134 return self
._texifier
.texify(chunk
.content
) + b
'\n'
137 class BelowIconLayout(Layout
):
139 def __init__(self
, texifier
: Texifier
, image_size
: float) -> None:
140 self
._texifier
= texifier
141 self
._image
_size
= image_size
143 def renderChunk(self
, chunk
: Chunk
) -> bytes:
144 icon
= renderIcon(chunk
.icon
, self
._image
_size
)
145 meta
= [icon
] if icon
else []
146 meta
+= [self
._texifier
.texify(x
)
147 for x
in [chunk
.character
, chunk
.screen_name
, chunk
.author
]
150 return b
'''\\wrapstuffclear
151 \\begin{wrapstuff}[l]
153 \\begin{varwidth}{0.5\\textwidth}
154 \\smash{\\parbox[t][0pt]{0pt}{
155 \\setlength{\\fboxrule}{0.2pt}
156 \\setlength{\\fboxsep}{0pt}
158 \\fbox{\\hspace{107mm}}
173 self
._texifier
.texify(chunk
.content
))
176 class BesideIconLayout(Layout
):
178 def __init__(self
, texifier
: Texifier
, image_size
: float) -> None:
179 self
._texifier
= texifier
180 self
._image
_size
= image_size
182 def renderChunk(self
, chunk
: Chunk
) -> bytes:
183 icon
= renderIcon(chunk
.icon
, self
._image
_size
)
190 # Why is \textwidth not the width of the text?
191 # Why is the width of the text .765\textwidth?
192 return b
'''\\noindent\\fbox{
194 \\parbox[b]{.765\\textwidth}{
200 \\vspace{-0.75em}\\\\*
206 icon
if icon
else b
'',
207 b
'\\\\*'.join(self
._texifier
.texify(x
) for x
in meta
if x
is not None),
208 self
._texifier
.texify(chunk
.content
))