]>
git.scottworley.com Git - paperdoorknob/blob - glowfic.py
a4f3ca9b2e24eb5707c86cd3b03d1dc64db83bb6
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
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 @dataclass(frozen
=True)
25 screen_name
: str |
None
29 # We avoid the name "post" because the Glowfic community uses the term
31 # * The Glowfic software sometimes uses "post" to refer to a whole thread
32 # (in the URL), sometimes uses "post" to refer to chunks (in the CSS),
33 # but mostly uses "post" to refer to just the first chunk in a thread
34 # (in the HTML and UI). The non-first chunks are "replies".
35 # * Readers and this software don't need to distinguish first-chunks and
37 # * Humans in the community tend to use "posts" to mean chunks.
40 def chunkDOMs(html
: BeautifulSoup
) -> Iterable
[Tag
]:
44 text
= body
.find_next("div", class_
="post-post")
45 assert isinstance(text
, Tag
)
48 def the_replies() -> Iterable
[Tag
]:
49 rs
= html
.find_all("div", class_
="post-reply")
50 assert all(isinstance(r
, Tag
) for r
in rs
)
53 return itertools
.chain([text()], the_replies())
56 def makeChunk(chunk_dom
: Tag
, image_store
: ImageStore
) -> Chunk
:
58 def getIcon() -> str |
None:
59 icon_div
= chunk_dom
.find_next('div', class_
='post-icon')
62 icon_img
= icon_div
.find_next('img')
65 assert isinstance(icon_img
, Tag
)
66 return image_store
.get_image(icon_img
.attrs
['src'])
68 def getTextByClass(css_class
: str) -> str |
None:
69 div
= chunk_dom
.find_next('div', class_
=css_class
)
72 return div
.text
.strip()
74 content
= chunk_dom
.find_next('div', class_
='post-content')
75 assert isinstance(content
, Tag
)
77 return Chunk(getIcon(),
78 getTextByClass('post-character'),
79 getTextByClass('post-screenname'),
80 getTextByClass('post-author'),
84 def renderIcon(icon_path
: str |
None, image_size
: float) -> bytes:
85 return b
'\\includegraphics[width=%fmm,height=%fmm,keepaspectratio]{%s}' % (
86 image_size
, image_size
, icon_path
.encode('UTF-8')) if icon_path
else b
''
92 def renderChunk(self
, chunk
: Chunk
) -> bytes:
93 raise NotImplementedError()
96 class ContentOnlyLayout(Layout
):
98 def __init__(self
, texifier
: Texifier
) -> None:
99 self
._texifier
= texifier
101 def renderChunk(self
, chunk
: Chunk
) -> bytes:
102 return self
._texifier
.texify(chunk
.content
)
105 class BelowIconLayout(Layout
):
107 def __init__(self
, texifier
: Texifier
, image_size
: float) -> None:
108 self
._texifier
= texifier
109 self
._image
_size
= image_size
111 def renderChunk(self
, chunk
: Chunk
) -> bytes:
112 icon_width
= b
'0.25\\textwidth' # TODO: Make this configurable
113 return b
'''\\begin{wrapfigure}{l}{%s}
127 renderIcon(chunk
.icon
, self
._image
_size
),
128 chunk
.character
.encode('UTF-8') if chunk
.character
else b
'',
129 chunk
.screen_name
.encode('UTF-8') if chunk
.screen_name
else b
'',
130 chunk
.author
.encode('UTF-8') if chunk
.author
else b
'',
131 self
._texifier
.texify(chunk
.content
))