squash merge new features (branch 'sorting')

This commit is contained in:
likho 2023-04-15 08:58:33 -07:00
parent 04117c6479
commit ae6650c775
4 changed files with 169 additions and 132 deletions

View File

@ -5,10 +5,9 @@ Simple and stylish text-to-html microblog generator.
## Requirements ## Requirements
python3 date make toml curl pycurl urllib python3 make dateutil toml curl pycurl urllib
* `date` is `date` from GNU Core Utilities. * `dateutil`, `toml` are Python modules.
* `toml` is a Python module.
* `make` (optional), method for invoking the script. * `make` (optional), method for invoking the script.
* `curl`, `pycurl` and `urllib` (optional), for uploading multiple files to neocities (`neouploader.py`). * `curl`, `pycurl` and `urllib` (optional), for uploading multiple files to neocities (`neouploader.py`).
@ -26,9 +25,7 @@ Use a Makefile (or another script) to simplify invocation.
cp example/Makefile . cp example/Makefile .
This script generates three text files after operation. This script generate a text file after operation.
* `lastfullpage.txt`, holds an integer for the last page rendered by the paginator.
* `updatedfiles.txt`, a list of files updated by the script for use in automated uploads. * `updatedfiles.txt`, a list of files updated by the script for use in automated uploads.
## Configuration ## Configuration

View File

@ -6,8 +6,25 @@ relative_css=["./style.css", "./timeline.css"]
[post] [post]
accepted_images= ["jpg", "JPG", "png", "PNG"] accepted_images= ["jpg", "JPG", "png", "PNG"]
buttons = {reply = "mailto:user@host.tld", test = "https://toml.io/en/v1.0.0#array-of-tables", interact="https://yoursite.tld/cgi?postid="}
# true = add <p></p> tags to each line. # true = add <p></p> tags to each line.
tag_paragraphs=true tag_paragraphs=true
# adds <br> or user defined string between each line # adds <br> or user defined string between each line
# line_separator="<br>" # line_separator="<br>"
format="""
<div class="postcell" id="{__num__}">
<div class="timestamp">{__timestamp__}
<a href=#{__num__}>(#{__num__})</a>
</div>
<div class="message">{__msg__}</div>
{__btn__}
</div>
"""
[post.buttons]
reply = "mailto:user@host.tld"
test = "https://toml.io/en/v1.0.0#array-of-tables"
interact = "https://yoursite.tld/cgi?postid="
[post.gallery]
path_to_thumb="./thumbs"
path_to_fullsize="./images"

View File

@ -2,17 +2,15 @@
@media only screen and (min-width: 768px) { @media only screen and (min-width: 768px) {
.column { .column {
float: left; float: left;
width: 30%; width: 32%;
/* background-color: #1a1a1a; */
} }
.timeline { .timeline {
float: right; float: right;
width: 67%; width: 67%;
/* background-color: #1a1a1a; */
} }
} }
.postcell { .postcell {
border: 1px solid gray; border: 1px solid red;
text-align: left; text-align: left;
margin: 0.25em 0 margin: 0.25em 0
} }
@ -30,11 +28,13 @@
margin: 0.5em margin: 0.5em
} }
.hashtag { .hashtag {
color: green;
font-weight: bold; font-weight: bold;
} }
.profile { .profile {
vertical-align: middle; vertical-align: middle;
padding-left: 10px; padding-left: 10px;
border:1px solid blue;
} }
.avatar { .avatar {
vertical-align: middle; vertical-align: middle;
@ -42,17 +42,17 @@
height: 50px; height: 50px;
} }
.handle{ .handle{
font-size: large; font-size: 1.1em;
font-weight: bold; font-weight: bold;
} }
.email{ .email{
text-align:left; text-align:left;
font-size: x-small; font-size: 0.8em;
text-decoration:none; text-decoration:none;
} }
.bio { .bio {
vertical-align: middle; vertical-align: middle;
font-size: small; font-size: 0.9em;
margin: 1em margin: 1em
} }
.gallery { .gallery {
@ -61,26 +61,18 @@
align-items: center; align-items: center;
width: 100%; width: 100%;
} }
div.panel { .gallery .panel {
margin: 2px; margin: 2px;
width: auto width: auto
} }
div.panel img { .gallery .panel img {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.gallery .panel img:hover {
div.panel img:hover {
border: 1px solid #777; border: 1px solid #777;
filter: invert(100%); filter: invert(100%);
} }
.header {
background: black;
color: white;
font-size: large;
font-weight: bold;
padding: 0.5em;
}
/* Clear floats after the columns */ /* Clear floats after the columns */
.row:after { .row:after {
content: ""; content: "";

View File

@ -1,5 +1,6 @@
import sys, os, subprocess import sys, os, traceback
import dateutil.parser
# returns html-formatted string # returns html-formatted string
def make_buttons(btn_dict, msg_id): def make_buttons(btn_dict, msg_id):
@ -16,34 +17,41 @@ def make_buttons(btn_dict, msg_id):
# apply div classes for use with .css # apply div classes for use with .css
def make_post(num, timestamp, conf, msg): def make_post(num, timestamp, conf, msg):
fmt = ''' fmt = conf["format"]
<div class=\"postcell\" id=\"%i\">
<div class=\"timestamp\">%s<a href=#%i>(#%i)</a></div>
<div class=\"message\">%s</div>
'''
if "buttons" in conf: if "buttons" in conf:
b = make_buttons(conf["buttons"], num) b = make_buttons(conf["buttons"], num)
fmt += b else:
fmt += "</div>" b = ""
return fmt % (num, timestamp, num, num, msg) return fmt.format(
__timestamp__=timestamp, __num__=num, __msg__=msg, __btn__=b)
def make_gallery(i, w): def make_gallery(indices, w, conf=None):
tag = [] tag = []
if i == []: if indices == []:
return tag return tag
tag.append("<div class=\"gallery\">") template = '''
for index in reversed(i):
image = w.pop(index)
template = '''
<div class=\"panel\"> <div class=\"panel\">
<a href=\"%s\"><img src=\"%s\" class=\"embed\"></a> <a href=\"%s\"><img src=\"%s\" class=\"embed\"></a>
</div> </div>
''' '''
tag.append("<div class=\"gallery\">")
for index in reversed(indices):
image = w.pop(index)
is_path = image[0] == '.' or image[0] == '/'
if conf and not is_path:
thumb = "%s/%s" % (conf["path_to_thumb"], image)
full = "%s/%s" % (conf["path_to_fullsize"], image)
tag.append(template % (full,thumb))
continue
elif not conf and not is_path:
msg = ("Warning: no path defined for image %s!" % image)
print(msg,file=sys.stderr)
else:
pass
tag.append(template % (image, image)) tag.append(template % (image, image))
tag.append("</div>") tag.append("</div>")
return tag return tag
def markup(message, config): def markup(message, config):
def is_image(s, image_formats): def is_image(s, image_formats):
l = s.rsplit('.', maxsplit=1) l = s.rsplit('.', maxsplit=1)
@ -96,7 +104,8 @@ def markup(message, config):
images.append(i) images.append(i)
if len(images) > 0: if len(images) > 0:
# function invokes pop() which modifies list 'words' # function invokes pop() which modifies list 'words'
gallery = make_gallery(images, words) gc = config["gallery"] if "gallery" in config else None
gallery = make_gallery(images, words, gc)
if ptags and len(words) > 0: if ptags and len(words) > 0:
words.insert(0,"<p>") words.insert(0,"<p>")
words.append("</p>") words.append("</p>")
@ -109,42 +118,49 @@ def markup(message, config):
# apply basic HTML formatting - only div class here is gallery # apply basic HTML formatting - only div class here is gallery
from html import escape from html import escape
# iterate through posts and get information about them class Post:
def __init__(self, ts, msg):
self.timestamp = ts.strip() # string
self.message = msg # list
# format used for sorting
def get_epoch_time(self):
t = dateutil.parser.parse(self.timestamp)
return int(t.timestamp())
# format used for display
def get_short_time(self):
t = dateutil.parser.parse(self.timestamp)
return t.strftime("%y %b %d")
def parse_txt(filename):
content = []
with open(filename, 'r') as f:
content = f.readlines()
posts = [] # list of posts - same order as file
message = [] # list of lines
# {-1 = init;; 0 = timestamp is next, 1 = message is next}
state = -1
timestamp = ""
for line in content:
if state == -1:
state = 0
continue
elif state == 0:
timestamp = line
state = 1
elif state == 1:
if len(line) > 1:
message.append(line)
else:
p = Post(timestamp, message)
posts.append(p)
# reset
message = []
state = 0
return posts
def get_posts(filename, config): def get_posts(filename, config):
class Post:
def __init__(self, ts, msg):
self.timestamp = ts # string
self.message = msg # list
def parse_txt(filename):
content = []
with open(filename, 'r') as f:
content = f.readlines()
posts = [] # list of posts - same order as file
message = [] # list of lines
# {-1 = init;; 0 = timestamp is next, 1 = message is next}
state = -1
timestamp = ""
for line in content:
if state == -1:
state = 0
continue
elif state == 0:
cmd = ['date', '-d', line, '+%y %b %d']
result = subprocess.run(cmd, stdout=subprocess.PIPE)
timestamp = result.stdout.decode('utf-8')
state = 1
elif state == 1:
if len(line) > 1:
message.append(line)
else:
p = Post(timestamp, message)
posts.append(p)
# reset
message = []
state = 0
return posts
posts = parse_txt(filename) posts = parse_txt(filename)
taginfos = [] taginfos = []
tagcloud = dict() # (tag, count) tagcloud = dict() # (tag, count)
@ -159,7 +175,7 @@ def get_posts(filename, config):
count -= 1 count -= 1
index -= 1 index -= 1
timeline.append( timeline.append(
make_post(count, post.timestamp, config, markedup) make_post(count, post.get_short_time(), config, markedup)
) )
for tag in tags: for tag in tags:
if tagcloud.get(tag) == None: if tagcloud.get(tag) == None:
@ -183,22 +199,18 @@ def make_tagcloud(d, rell):
return output return output
class Paginator: class Paginator:
def __init__(self, x, ppp, loc="pages"): def __init__(self, post_count, ppp, loc=None):
if x <= 0: if post_count <= 0:
print("Error: No posts (x=%i" % x, file=sys.stderr)
raise Exception raise Exception
self.TOTAL_POSTS = x if not loc:
loc = "pages"
if loc and not os.path.exists(loc):
os.mkdir(loc)
self.TOTAL_POSTS = post_count
self.PPP = ppp self.PPP = ppp
self.TOTAL_PAGES = int(x/self.PPP) self.TOTAL_PAGES = int(post_count/self.PPP)
self.SUBDIR = loc self.SUBDIR = loc
if not os.path.exists(loc):
os.makedirs(loc)
self.FILENAME = "%i.html" self.FILENAME = "%i.html"
try:
with open ("lastfullpage.txt", 'r') as f:
self.lastfullpage = int(f.read())
except : #possible exceptions FileNotFoundError, ValueError
self.lastfullpage = 0
self.written = [] self.written = []
def toc(self, current_page=None, path=None): #style 1 def toc(self, current_page=None, path=None): #style 1
@ -226,14 +238,10 @@ class Paginator:
postcount=self.TOTAL_POSTS, tags=tc, pages=toc, timeline=tl postcount=self.TOTAL_POSTS, tags=tc, pages=toc, timeline=tl
) )
def paginate(self, template, tagcloud, timeline, override=False): def paginate(self, template, tagcloud, timeline, is_tagline=False):
# override boolean currently reprsents whether or not
# it is a main timeline or a tagline being paginated
## effort-saving feature does not work for taglines currently
outfile = "%s/%s" % (self.SUBDIR, self.FILENAME) outfile = "%s/%s" % (self.SUBDIR, self.FILENAME)
#print(outfile, file=sys.stderr)
timeline.reverse() # reorder from oldest to newest timeline.reverse() # reorder from oldest to newest
start = 0 if override else self.lastfullpage start = 0
for i in range(start, self.TOTAL_PAGES): for i in range(start, self.TOTAL_PAGES):
fn = outfile % i fn = outfile % i
with open(fn, 'w') as f: with open(fn, 'w') as f:
@ -243,27 +251,40 @@ class Paginator:
sliced = timeline[prev:curr] sliced = timeline[prev:curr]
sliced.reverse() sliced.reverse()
f.write(self.singlepage(template, tagcloud, sliced, i, ".")) f.write(self.singlepage(template, tagcloud, sliced, i, "."))
if not override:
with open("lastfullpage.txt", 'w') as f:
f.write(str(self.TOTAL_PAGES))
return return
import argparse
if __name__ == "__main__": if __name__ == "__main__":
def sort(filename):
def export(new_content, new_filename):
with open(new_filename, 'w') as f:
print(file=f)
for post in new_content:
print(post.timestamp, file=f)
print("".join(post.message), file=f)
return
posts = parse_txt(filename)
posts.sort(key=lambda e: e.get_epoch_time())
outfile = ("%s.sorted" % filename)
print("Sorted text written to ", outfile)
export(reversed(posts), outfile)
def get_args(): def get_args():
argc = len(sys.argv) p = argparse.ArgumentParser()
if argc < 3: p.add_argument("template", help="an html template file")
msg = '''This is microblog.py. (%s/3 arguments given) p.add_argument("content", help="text file for microblog content")
\tpython microblog.py [template] [content] p.add_argument("--sort", \
''' help="sorts content from oldest to newest"
print(msg % argc, file=sys.stderr) " (this is a separate operation from page generation)", \
action="store_true")
args = p.parse_args()
if args.sort:
sort(args.content)
exit() exit()
# script = argv[0] return args.template, args.content
template = sys.argv[1]
content = sys.argv[2]
return template, content
# assume relative path # assume relative path
def demote_css(template, cssl, level=1): def demote_css(template, css_list, level=1):
prepend = "" prepend = ""
if level == 1: if level == 1:
prepend = '.' prepend = '.'
@ -271,7 +292,7 @@ if __name__ == "__main__":
for i in range(level): for i in range(level):
prepend = ("../%s" % prepend) prepend = ("../%s" % prepend)
tpl = template tpl = template
for css in cssl: for css in css_list:
tpl = tpl.replace(css, ("%s%s" % (prepend, css) )) tpl = tpl.replace(css, ("%s%s" % (prepend, css) ))
return tpl return tpl
@ -281,30 +302,28 @@ if __name__ == "__main__":
html = "" html = ""
with open(template,'r') as f: with open(template,'r') as f:
html = f.read() html = f.read()
count = len(timeline)
try: try:
p = config["postsperpage"] count = len(timeline)
if subdir == None: p = config["postsperpage"]
pagectrl = Paginator(count, p) pagectrl = Paginator(count, p, subdir)
else: except ZeroDivisionError as e:
pagectrl = Paginator(count, p, subdir) print("error: ",e, ". check 'postsperpage' in config", file=sys.stderr)
if not os.path.exists(subdir): exit()
os.mkdir(subdir) except Exception as e:
except: print("error: ",e, ("(number of posts = %i)" % count), file=sys.stderr)
print("Error: value <= 0 submitted to paginator constructor",
file=sys.stderr)
exit() exit()
latest = timeline if count <= pagectrl.PPP else timeline[:pagectrl.PPP] latest = timeline if count <= pagectrl.PPP else timeline[:pagectrl.PPP]
lvl = 1
if subdir == None: # if top level page if subdir == None: # if top level page
ovr = False lvl = 1
tcloud = make_tagcloud(tagcloud, "./tags/%s/latest.html") tcloud = make_tagcloud(tagcloud, "./tags/%s/latest.html")
print(pagectrl.singlepage(html, tcloud, latest)) print(pagectrl.singlepage(html, tcloud, latest))
tcloud = make_tagcloud(tagcloud, "../tags/%s/latest.html") tcloud = make_tagcloud(tagcloud, "../tags/%s/latest.html")
pagectrl.paginate( pagectrl.paginate(
demote_css(html, config["relative_css"], lvl), tcloud, timeline, ovr) demote_css(html, config["relative_css"], lvl),
tcloud, timeline
)
else: # if timelines per tag else: # if timelines per tag
ovr = True is_tagline = True
lvl = 2 lvl = 2
newhtml = demote_css(html, config["relative_css"], lvl) newhtml = demote_css(html, config["relative_css"], lvl)
tcloud = make_tagcloud(tagcloud, "../%s/latest.html") tcloud = make_tagcloud(tagcloud, "../%s/latest.html")
@ -312,10 +331,9 @@ if __name__ == "__main__":
with open(fn, 'w') as f: with open(fn, 'w') as f:
pagectrl.written.append(fn) pagectrl.written.append(fn)
f.write( f.write(
pagectrl.singlepage( pagectrl.singlepage(newhtml, tcloud, latest, p=".")
newhtml, tcloud, latest, p=".")) )
pagectrl.paginate( pagectrl.paginate(newhtml, tcloud, timeline, is_tagline)
newhtml, tcloud, timeline, ovr)
return pagectrl.written return pagectrl.written
import toml import toml
@ -331,7 +349,6 @@ if __name__ == "__main__":
def main(): def main():
tpl, content = get_args() tpl, content = get_args()
# read settings file
cfg = load_settings() cfg = load_settings()
if cfg == None: if cfg == None:
print("exit: no settings.toml found.", file=sys.stderr) print("exit: no settings.toml found.", file=sys.stderr)
@ -343,10 +360,15 @@ if __name__ == "__main__":
print("exit: table 'page' absent in settings.toml", file=sys.stderr) print("exit: table 'page' absent in settings.toml", file=sys.stderr)
return return
tl, tc, tg = get_posts(content, cfg["post"]) tl, tc, tg = get_posts(content, cfg["post"])
if tl == []:
return
# main timeline # main timeline
updated = [] updated = []
updated += writepage(tpl, tl, tc, cfg["page"]) updated += writepage(tpl, tl, tc, cfg["page"])
# timeline per tag # timeline per tag
if tc != dict() and tg != dict():
if not os.path.exists("tags"):
os.mkdir("tags")
for key in tg.keys(): for key in tg.keys():
tagline = [] tagline = []
for index in tg[key]: for index in tg[key]:
@ -361,4 +383,13 @@ if __name__ == "__main__":
print(filename, file=f) # sys.stderr) print(filename, file=f) # sys.stderr)
if "latestpage" in cfg: if "latestpage" in cfg:
print(cfg["latestpage"], file=f) print(cfg["latestpage"], file=f)
main() try:
main()
except KeyError as e:
traceback.print_exc()
print("\n\tA key may be missing from your settings file.", file=sys.stderr)
except dateutil.parser._parser.ParserError as e:
traceback.print_exc()
print("\n\tFailed to interpret a date from string..",
"\n\tYour file of posts may be malformed.",
"\n\tCheck if your file starts with a line break.", file=sys.stderr)