#7 new features

Merged
likho merged 15 commits from likho/sorting into likho/master 2 years ago
4 changed files with 163 additions and 117 deletions
  1. 3 6
      README.md
  2. 18 1
      example/settings.toml
  3. 5 4
      example/timeline.css
  4. 137 106
      microblog.py

+ 3 - 6
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

+ 18 - 1
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 <p></p> tags to each line.
 tag_paragraphs=true
 # adds <br> or user defined string between each line
 # 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]
+thumbnail_dir="./thumbs"
+fullsize_dir="./images"

+ 5 - 4
example/timeline.css

@@ -35,6 +35,7 @@
 .profile {
     vertical-align: middle;
     padding-left: 10px;
+    border:1px solid black;
 }
 .avatar {
     vertical-align: middle;
@@ -42,17 +43,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 {
@@ -77,7 +78,7 @@ div.panel img:hover {
 .header {
     background: black;
     color: white;
-    font-size: large;
+    font-size: 1.1em;
     font-weight: bold;
     padding: 0.5em;
 }

+ 137 - 106
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  = '''
-<div class=\"postcell\" id=\"%i\">
-    <div class=\"timestamp\">%s<a href=#%i>(#%i)</a></div>
-    <div class=\"message\">%s</div>
-'''
+    fmt  = conf["format"]
     if "buttons" in conf:
         b = make_buttons(conf["buttons"], num)
-        fmt += b
-    fmt += "</div>"
-    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("<div class=\"gallery\">")
-    for index in reversed(i):
-        image = w.pop(index)
-        template = '''
+    template = '''
 <div class=\"panel\">
     <a href=\"%s\"><img src=\"%s\" class=\"embed\"></a>
 </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["thumbnail_dir"], image)
+            full = "%s/%s" % (conf["fullsize_dir"], 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("</div>")
     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,"<p>")
             words.append("</p>")
@@ -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
-def get_posts(filename, config):
-    class Post:
-        def __init__(self, ts, msg):
-            self.timestamp = ts # string
-            self.message = msg  # list
+class Post:
+    def __init__(self, ts, msg):
+        self.timestamp = ts.strip() # 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
+    # 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):
     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)