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") 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)