From ae6650c775600aba136aeea85495ebcdf5e9275e Mon Sep 17 00:00:00 2001 From: likho Date: Sat, 15 Apr 2023 08:58:33 -0700 Subject: [PATCH] squash merge new features (branch 'sorting') --- README.md | 9 +- example/settings.toml | 19 +++- example/timeline.css | 28 ++--- microblog.py | 245 ++++++++++++++++++++++++------------------ 4 files changed, 169 insertions(+), 132 deletions(-) diff --git a/README.md b/README.md index 4017cc4..93edd89 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,9 @@ Simple and stylish text-to-html microblog generator. ## Requirements - python3 date make toml curl pycurl urllib + python3 make dateutil toml curl pycurl urllib -* `date` is `date` from GNU Core Utilities. -* `toml` is a Python module. +* `dateutil`, `toml` are Python modules. * `make` (optional), method for invoking the script. * `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 . -This script generates three text files after operation. - -* `lastfullpage.txt`, holds an integer for the last page rendered by the paginator. +This script generate a text file after operation. * `updatedfiles.txt`, a list of files updated by the script for use in automated uploads. ## Configuration diff --git a/example/settings.toml b/example/settings.toml index 151a206..3b0ba1d 100644 --- a/example/settings.toml +++ b/example/settings.toml @@ -6,8 +6,25 @@ relative_css=["./style.css", "./timeline.css"] [post] 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

tags to each line. tag_paragraphs=true # adds
or user defined string between each line # line_separator="
" +format=""" +
+
{__timestamp__} + (#{__num__}) +
+
{__msg__}
+ {__btn__} +
+""" + +[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" diff --git a/example/timeline.css b/example/timeline.css index 2db28a2..139ca73 100644 --- a/example/timeline.css +++ b/example/timeline.css @@ -2,17 +2,15 @@ @media only screen and (min-width: 768px) { .column { float: left; - width: 30%; - /* background-color: #1a1a1a; */ + width: 32%; } .timeline { float: right; width: 67%; - /* background-color: #1a1a1a; */ } } .postcell { - border: 1px solid gray; + border: 1px solid red; text-align: left; margin: 0.25em 0 } @@ -30,11 +28,13 @@ margin: 0.5em } .hashtag { + color: green; font-weight: bold; } .profile { vertical-align: middle; padding-left: 10px; + border:1px solid blue; } .avatar { vertical-align: middle; @@ -42,17 +42,17 @@ height: 50px; } .handle{ - font-size: large; + font-size: 1.1em; font-weight: bold; } .email{ text-align:left; - font-size: x-small; + font-size: 0.8em; text-decoration:none; } .bio { vertical-align: middle; - font-size: small; + font-size: 0.9em; margin: 1em } .gallery { @@ -61,26 +61,18 @@ align-items: center; width: 100%; } -div.panel { +.gallery .panel { margin: 2px; width: auto } -div.panel img { +.gallery .panel img { width: 100%; height: 100%; } - -div.panel img:hover { +.gallery .panel img:hover { border: 1px solid #777; filter: invert(100%); } -.header { - background: black; - color: white; - font-size: large; - font-weight: bold; - padding: 0.5em; -} /* Clear floats after the columns */ .row:after { content: ""; diff --git a/microblog.py b/microblog.py index 7f8d0b0..248a3d8 100644 --- a/microblog.py +++ b/microblog.py @@ -1,5 +1,6 @@ -import sys, os, subprocess +import sys, os, traceback +import dateutil.parser # returns html-formatted string 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 def make_post(num, timestamp, conf, msg): - fmt = ''' -
- -
%s
-''' + fmt = conf["format"] if "buttons" in conf: b = make_buttons(conf["buttons"], num) - fmt += b - fmt += "
" - return fmt % (num, timestamp, num, num, msg) + else: + b = "" + return fmt.format( + __timestamp__=timestamp, __num__=num, __msg__=msg, __btn__=b) -def make_gallery(i, w): +def make_gallery(indices, w, conf=None): tag = [] - if i == []: + if indices == []: return tag - tag.append("
") - for index in reversed(i): - image = w.pop(index) - template = ''' + template = '''
''' + tag.append("
") + 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("
") return tag - def markup(message, config): def is_image(s, image_formats): l = s.rsplit('.', maxsplit=1) @@ -96,7 +104,8 @@ def markup(message, config): images.append(i) if len(images) > 0: # 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: words.insert(0,"

") words.append("

") @@ -109,42 +118,49 @@ def markup(message, config): # apply basic HTML formatting - only div class here is gallery 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): - 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) taginfos = [] tagcloud = dict() # (tag, count) @@ -159,7 +175,7 @@ def get_posts(filename, config): count -= 1 index -= 1 timeline.append( - make_post(count, post.timestamp, config, markedup) + make_post(count, post.get_short_time(), config, markedup) ) for tag in tags: if tagcloud.get(tag) == None: @@ -183,22 +199,18 @@ def make_tagcloud(d, rell): return output class Paginator: - def __init__(self, x, ppp, loc="pages"): - if x <= 0: - print("Error: No posts (x=%i" % x, file=sys.stderr) + def __init__(self, post_count, ppp, loc=None): + if post_count <= 0: 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.TOTAL_PAGES = int(x/self.PPP) + self.TOTAL_PAGES = int(post_count/self.PPP) self.SUBDIR = loc - if not os.path.exists(loc): - os.makedirs(loc) 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 = [] 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 ) - def paginate(self, template, tagcloud, timeline, override=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 + def paginate(self, template, tagcloud, timeline, is_tagline=False): outfile = "%s/%s" % (self.SUBDIR, self.FILENAME) - #print(outfile, file=sys.stderr) timeline.reverse() # reorder from oldest to newest - start = 0 if override else self.lastfullpage + start = 0 for i in range(start, self.TOTAL_PAGES): fn = outfile % i with open(fn, 'w') as f: @@ -243,27 +251,40 @@ class Paginator: sliced = timeline[prev:curr] sliced.reverse() 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 +import argparse 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(): - argc = len(sys.argv) - if argc < 3: - msg = '''This is microblog.py. (%s/3 arguments given) -\tpython microblog.py [template] [content] -''' - print(msg % argc, file=sys.stderr) + p = argparse.ArgumentParser() + p.add_argument("template", help="an html template file") + p.add_argument("content", help="text file for microblog content") + p.add_argument("--sort", \ + help="sorts content from oldest to newest" + " (this is a separate operation from page generation)", \ + action="store_true") + args = p.parse_args() + if args.sort: + sort(args.content) exit() - # script = argv[0] - template = sys.argv[1] - content = sys.argv[2] - return template, content + return args.template, args.content # assume relative path - def demote_css(template, cssl, level=1): + def demote_css(template, css_list, level=1): prepend = "" if level == 1: prepend = '.' @@ -271,7 +292,7 @@ if __name__ == "__main__": for i in range(level): prepend = ("../%s" % prepend) tpl = template - for css in cssl: + for css in css_list: tpl = tpl.replace(css, ("%s%s" % (prepend, css) )) return tpl @@ -281,30 +302,28 @@ if __name__ == "__main__": html = "" with open(template,'r') as f: html = f.read() - count = len(timeline) try: - p = config["postsperpage"] - if subdir == None: - pagectrl = Paginator(count, p) - else: - pagectrl = Paginator(count, p, subdir) - if not os.path.exists(subdir): - os.mkdir(subdir) - except: - print("Error: value <= 0 submitted to paginator constructor", - file=sys.stderr) + count = len(timeline) + p = config["postsperpage"] + pagectrl = Paginator(count, p, subdir) + except ZeroDivisionError as e: + print("error: ",e, ". check 'postsperpage' in config", file=sys.stderr) + exit() + except Exception as e: + print("error: ",e, ("(number of posts = %i)" % count), file=sys.stderr) exit() latest = timeline if count <= pagectrl.PPP else timeline[:pagectrl.PPP] - lvl = 1 if subdir == None: # if top level page - ovr = False + lvl = 1 tcloud = make_tagcloud(tagcloud, "./tags/%s/latest.html") print(pagectrl.singlepage(html, tcloud, latest)) tcloud = make_tagcloud(tagcloud, "../tags/%s/latest.html") 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 - ovr = True + is_tagline = True lvl = 2 newhtml = demote_css(html, config["relative_css"], lvl) tcloud = make_tagcloud(tagcloud, "../%s/latest.html") @@ -312,10 +331,9 @@ if __name__ == "__main__": with open(fn, 'w') as f: pagectrl.written.append(fn) f.write( - pagectrl.singlepage( - newhtml, tcloud, latest, p=".")) - pagectrl.paginate( - newhtml, tcloud, timeline, ovr) + pagectrl.singlepage(newhtml, tcloud, latest, p=".") + ) + pagectrl.paginate(newhtml, tcloud, timeline, is_tagline) return pagectrl.written import toml @@ -331,7 +349,6 @@ if __name__ == "__main__": def main(): tpl, content = get_args() - # read settings file cfg = load_settings() if cfg == None: 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) return tl, tc, tg = get_posts(content, cfg["post"]) + if tl == []: + return # main timeline updated = [] updated += writepage(tpl, tl, tc, cfg["page"]) # timeline per tag + if tc != dict() and tg != dict(): + if not os.path.exists("tags"): + os.mkdir("tags") for key in tg.keys(): tagline = [] for index in tg[key]: @@ -361,4 +383,13 @@ if __name__ == "__main__": print(filename, file=f) # sys.stderr) if "latestpage" in cfg: 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)