squash merge new features (branch 'sorting')
This commit is contained in:
		
							parent
							
								
									04117c6479
								
							
						
					
					
						commit
						ae6650c775
					
				| @ -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 | ||||
|  | ||||
| @ -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] | ||||
| path_to_thumb="./thumbs" | ||||
| path_to_fullsize="./images" | ||||
|  | ||||
| @ -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: ""; | ||||
|  | ||||
							
								
								
									
										245
									
								
								microblog.py
									
									
									
									
									
								
							
							
						
						
									
										245
									
								
								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["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("</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 | ||||
| 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) | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user