squash merge toml
This commit is contained in:
		
							parent
							
								
									c787cf3ac8
								
							
						
					
					
						commit
						04117c6479
					
				
							
								
								
									
										16
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								README.md
									
									
									
									
									
								
							| @ -5,19 +5,22 @@ Simple and stylish text-to-html microblog generator. | ||||
| 
 | ||||
| ## Requirements | ||||
| 
 | ||||
|     python3 date make curl pycurl urllib | ||||
|     python3 date make toml curl pycurl urllib | ||||
| 
 | ||||
| `date` is `date` from GNU Core Utilities. `make` is only used to demonstrate the examples in `example/`. The latter three are optional; `curl`, `pycurl` and `urllib` are only used for the uploader script. | ||||
| * `date` is `date` from GNU Core Utilities.  | ||||
| * `toml` is a Python module.  | ||||
| * `make` (optional), method for invoking the script.  | ||||
| * `curl`, `pycurl` and `urllib` (optional), for uploading multiple files to neocities (`neouploader.py`). | ||||
| 
 | ||||
| ### Usage | ||||
| 
 | ||||
| Send three arguments minimum to `python`. The fourth argument for an e-mail address is optional. | ||||
| 
 | ||||
|     python microblog.py ./template.html ./content.txt user@mailhost.tld | ||||
|     python microblog.py ./template.html ./content.txt | ||||
| 
 | ||||
| The resulting web page is outputted from standard output. Therefore: | ||||
| 
 | ||||
|     python microblog.py ./template.html ./content.txt user@mailhost.tld >  result.html | ||||
|     python microblog.py ./template.html ./content.txt >  result.html | ||||
| 
 | ||||
| Use a Makefile (or another script) to simplify invocation. | ||||
| 
 | ||||
| @ -25,10 +28,13 @@ Use a Makefile (or another script) to simplify invocation. | ||||
| 
 | ||||
| This script generates three text files after operation. | ||||
| 
 | ||||
| * `postsperpage.txt`, holds an integer for the number of posts to render per page (default: 20). This is a configuration file that is created if it does not exist). | ||||
| * `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. | ||||
| 
 | ||||
| ## Configuration | ||||
| 
 | ||||
| Settings are read from `settings.toml`. See `example/settings.toml`. | ||||
| 
 | ||||
| ### Writing Content | ||||
| 
 | ||||
| See `example/demo.txt`. | ||||
|  | ||||
							
								
								
									
										13
									
								
								example/settings.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								example/settings.toml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| latestpage="result.html" | ||||
| 
 | ||||
| [page] | ||||
| postsperpage = 20 | ||||
| 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>" | ||||
| @ -1,12 +1,15 @@ | ||||
| .column { | ||||
|     float: left; | ||||
|     width: 30%; | ||||
| /*     background-color: #1a1a1a; */ | ||||
| } | ||||
| .timeline { | ||||
|     float: right; | ||||
|     width: 67%; | ||||
| /*     background-color: #1a1a1a; */ | ||||
| 
 | ||||
| @media only screen and (min-width: 768px) { | ||||
|     .column { | ||||
|         float: left; | ||||
|         width: 30%; | ||||
|     /*     background-color: #1a1a1a; */ | ||||
|     } | ||||
|     .timeline { | ||||
|         float: right; | ||||
|         width: 67%; | ||||
|     /*     background-color: #1a1a1a; */ | ||||
|     } | ||||
| } | ||||
| .postcell { | ||||
|     border: 1px solid gray;  | ||||
| @ -18,8 +21,9 @@ | ||||
|     white-space: pre-wrap; | ||||
|     word-wrap: break-word; | ||||
| } | ||||
| .reply { | ||||
|     margin: 0.5em | ||||
| .buttons { | ||||
|     margin-left: 1em; | ||||
|     margin-bottom:0.5em; | ||||
| } | ||||
| .timestamp { | ||||
|     text-align: right; | ||||
|  | ||||
							
								
								
									
										217
									
								
								microblog.py
									
									
									
									
									
								
							
							
						
						
									
										217
									
								
								microblog.py
									
									
									
									
									
								
							| @ -1,21 +1,31 @@ | ||||
| 
 | ||||
| import sys, os, subprocess | ||||
| 
 | ||||
| # returns html-formatted string | ||||
| def make_buttons(btn_dict, msg_id): | ||||
|     buttons = "<div class=\"buttons\">" | ||||
|     fmt = "<a href=\"%s\">[%s]</a>" | ||||
|     for key in btn_dict: | ||||
|         url = btn_dict[key]  | ||||
|         if url[-1] == '=': | ||||
|         # then interpret it as a query string | ||||
|             url += str(msg_id) | ||||
|         buttons += fmt % (url,key) | ||||
|     buttons += "</div>" | ||||
|     return buttons | ||||
| 
 | ||||
| # apply div classes for use with .css | ||||
| def make_post(num, timestamp, email, msg): | ||||
|     output = "" | ||||
| def make_post(num, timestamp, conf, msg): | ||||
|     fmt  = ''' | ||||
| <div class=\"postcell\" id=\"%i\"> | ||||
|     <div class=\"timestamp\"><a href=#%i>%s</a></div> | ||||
|     <div class=\"timestamp\">%s<a href=#%i>(#%i)</a></div> | ||||
|     <div class=\"message\">%s</div> | ||||
| ''' | ||||
|     if email != None:  | ||||
|         fmt += "<div class=\"reply\"><a href=\"mailto:%s?subject=p%i\">[reply]</a></div></div>" | ||||
|         output = fmt % (num, num, timestamp, msg, email, num) | ||||
|     else: | ||||
|         fmt += "</div>" | ||||
|         output = fmt % (num, num, timestamp, msg) | ||||
|     return output | ||||
|     if "buttons" in conf: | ||||
|         b = make_buttons(conf["buttons"], num) | ||||
|         fmt += b | ||||
|     fmt += "</div>" | ||||
|     return fmt % (num, timestamp, num, num, msg) | ||||
| 
 | ||||
| def make_gallery(i, w): | ||||
|     tag = [] | ||||
| @ -33,49 +43,78 @@ def make_gallery(i, w): | ||||
|     tag.append("</div>") | ||||
|     return tag | ||||
| 
 | ||||
| 
 | ||||
| def markup(message, config): | ||||
|     def is_image(s, image_formats): | ||||
|         l = s.rsplit('.', maxsplit=1) | ||||
|         if len(l) < 2: | ||||
|             return False | ||||
|         # Python 3.10.5 | ||||
|         # example result that had to be filtered: | ||||
|         # string:   started.  | ||||
|         # result:   ['started', ''] | ||||
|         if l[1] == str(''): | ||||
|             return False | ||||
|         #print(s, l, file=sys.stderr) | ||||
|         if l[1] in image_formats: | ||||
|             return True | ||||
|         return False | ||||
| 
 | ||||
|     result = 0 | ||||
|     tagged = "" | ||||
|     # support multiple images (gallery style) | ||||
|     tags   = [] # list of strings | ||||
|     output = [] | ||||
|     gallery = [] | ||||
|     ptags = config["tag_paragraphs"] | ||||
|     sep = "" | ||||
|     if "line_separator" in config: | ||||
|         sep = config["line_separator"] | ||||
|     for line in message: | ||||
|         images = [] # list of integers | ||||
|         words  = line.split() | ||||
|         for i in range(len(words)): | ||||
|             word  = words[i] | ||||
|             # don't help people click http | ||||
|             if word.find("src=") == 0 or word.find("href=") == 0: | ||||
|                 continue | ||||
|             elif word.find("https://") != -1:  | ||||
|                 w = escape(word) | ||||
|                 new_word = ("<a href=\"%s\">%s</a>") % (w, w) | ||||
|                 words[i] = new_word | ||||
|             elif word.find("#") != -1 and len(word) > 1: | ||||
|                 # split by unicode blank character if present | ||||
|                 # allows tagging such as #fanfic|tion | ||||
|                 w = word.split(chr(8206))  | ||||
|                 # w[0] is the portion closest to the # | ||||
|                 tags.append(w[0]) | ||||
|                 new_word = "<span class=\"hashtag\">%s</span>" % (w[0]) | ||||
|                 if len(w) > 1: | ||||
|                     new_word += w[1] | ||||
|                 words[i] = new_word | ||||
|             elif is_image(word, config["accepted_images"]): | ||||
|                 images.append(i) | ||||
|         if len(images) > 0:  | ||||
|             # function invokes pop() which modifies list 'words' | ||||
|             gallery = make_gallery(images, words) | ||||
|         if ptags and len(words) > 0: | ||||
|             words.insert(0,"<p>") | ||||
|             words.append("</p>") | ||||
|         output.append(" ".join(words)) | ||||
|         # avoid paragraph with an image gallery | ||||
|         if len(gallery) > 0: | ||||
|             output.append("".join(gallery)) | ||||
|             gallery = [] | ||||
|     return sep.join(output), tags | ||||
| 
 | ||||
| # 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, email): | ||||
| def get_posts(filename, config): | ||||
|     class Post: | ||||
|         def __init__(self, ts, msg): | ||||
|             self.timestamp = ts # string | ||||
|             self.message = msg  # list | ||||
|         def markup(self): | ||||
|             result = 0 | ||||
|             tagged = "" | ||||
|             # support multiple images (gallery style) | ||||
|             tags   = [] # list of strings | ||||
|             output = [] | ||||
|             for line in self.message: | ||||
|                 images = [] # list of integers | ||||
|                 words  = line.split() | ||||
|                 for i in range(len(words)): | ||||
|                     word  = words[i] | ||||
|                     # don't help people click http | ||||
|                     if word.find("src=") == 0 or word.find("href=") == 0: | ||||
|                         continue | ||||
|                     elif word.find("https://") != -1:  | ||||
|                         w = escape(word) | ||||
|                         new_word = ("<a href=\"%s\">%s</a>") % (w, w) | ||||
|                         words[i] = new_word | ||||
|                     elif word.find("#") != -1 and len(word) > 1: | ||||
|                         # split by unicode blank character if present | ||||
|                         # allows tagging such as #fanfic|tion | ||||
|                         w = word.split(chr(8206))  | ||||
|                         # w[0] is the portion closest to the # | ||||
|                         tags.append(w[0]) | ||||
|                         new_word = "<span class=\"hashtag\">%s</span>" % (w[0]) | ||||
|                         if len(w) > 1: | ||||
|                             new_word += w[1] | ||||
|                         words[i] = new_word | ||||
|                     elif word.find(".jpg") != -1 or word.find(".png") != -1: | ||||
|                         images.append(i) | ||||
|                 if len(images) > 0:  | ||||
|                     gallery = make_gallery(images, words) | ||||
|                     words += gallery | ||||
|                 output.append(" ".join(words)) | ||||
|             return "<br>".join(output), tags | ||||
| 
 | ||||
|     def parse_txt(filename): | ||||
|         content   = [] | ||||
| @ -114,12 +153,13 @@ def get_posts(filename, email): | ||||
|     count = total | ||||
|     index = count # - 1 | ||||
|     timeline = [] | ||||
|     btns = None | ||||
|     for post in posts: | ||||
|         markedup, tags = post.markup() | ||||
|         markedup, tags = markup(post.message, config) | ||||
|         count -= 1 | ||||
|         index -= 1 | ||||
|         timeline.append( | ||||
|             make_post(count, post.timestamp, email, markedup) | ||||
|             make_post(count, post.timestamp, config, markedup) | ||||
|         ) | ||||
|         for tag in tags: | ||||
|             if tagcloud.get(tag) == None: | ||||
| @ -143,21 +183,12 @@ def make_tagcloud(d, rell): | ||||
|     return output | ||||
| 
 | ||||
| class Paginator: | ||||
|     def __init__(self, x, loc="./pages"): | ||||
|     def __init__(self, x, ppp, loc="pages"): | ||||
|         if x <= 0: | ||||
|             print("Error: No posts (x=%i" % x, file=sys.stderr) | ||||
|             raise Exception | ||||
|         self.TOTAL_POSTS = x | ||||
|         try: | ||||
|             setting = "postsperpage.txt" | ||||
|             if os.path.exists(setting): | ||||
|                 with open(setting, 'r') as f: | ||||
|                     self.PPP = int(f.read()) | ||||
|             else: | ||||
|                 with open(setting, 'w') as f: | ||||
|                     f.write(20) | ||||
|         except: | ||||
|             self.PPP = 20 | ||||
|         self.PPP = ppp | ||||
|         self.TOTAL_PAGES = int(x/self.PPP) | ||||
|         self.SUBDIR      = loc | ||||
|         if not os.path.exists(loc): | ||||
| @ -221,43 +252,42 @@ if __name__ == "__main__": | ||||
|     def get_args(): | ||||
|         argc = len(sys.argv) | ||||
|         if argc < 3: | ||||
|             msg = '''This is microblog.py. (%s/3 arguments given; 4 maximum) | ||||
| \tpython microblog.py [template] [content] [optional: email] | ||||
|             msg = '''This is microblog.py. (%s/3 arguments given) | ||||
| \tpython microblog.py [template] [content] | ||||
| ''' | ||||
|             print(msg % argc, file=sys.stderr) | ||||
|             exit() | ||||
|         # script = argv[0] | ||||
|         template = sys.argv[1] | ||||
|         content  = sys.argv[2] | ||||
|         email    = sys.argv[3] if (argc >= 4) else None | ||||
|         return template, content, email | ||||
|         return template, content | ||||
| 
 | ||||
|     # assume relative path | ||||
|     def adjust_css(template, level=1): | ||||
|     def demote_css(template, cssl,  level=1): | ||||
|         prepend = "" | ||||
|         if level == 1: | ||||
|             prepend = '.' | ||||
|         else: | ||||
|             for i in range(level): | ||||
|                 prepend = ("../%s" % prepend) | ||||
|         css1 = "./style.css" | ||||
|         css2 = "./timeline.css" | ||||
|         #sys.stderr.write(("%s%s" % (prepend, css1) )) | ||||
|         tpl = template.replace(css1, ("%s%s" % (prepend, css1) )) | ||||
|         return tpl.replace(css2, ("%s%s" % (prepend, css2) )) | ||||
|         tpl = template | ||||
|         for css in cssl: | ||||
|             tpl = tpl.replace(css, ("%s%s" % (prepend, css) )) | ||||
|         return tpl | ||||
| 
 | ||||
| # needs review / clean-up | ||||
| # ideally relate 'lvl' with sub dir instead of hardcoding | ||||
|     def writepage(template, timeline, tagcloud, subdir = None): | ||||
|     def writepage(template, timeline, tagcloud, config, subdir = None): | ||||
|         html   = "" | ||||
|         with open(template,'r') as f: | ||||
|             html = f.read() | ||||
|         count  = len(timeline) | ||||
|         try: | ||||
|             p = config["postsperpage"] | ||||
|             if subdir == None: | ||||
|                 pagectrl = Paginator(count) | ||||
|                 pagectrl = Paginator(count, p) | ||||
|             else: | ||||
|                 pagectrl = Paginator(count, subdir) | ||||
|                 pagectrl = Paginator(count, p, subdir) | ||||
|                 if not os.path.exists(subdir): | ||||
|                     os.mkdir(subdir) | ||||
|         except: | ||||
| @ -271,35 +301,64 @@ if __name__ == "__main__": | ||||
|             tcloud = make_tagcloud(tagcloud, "./tags/%s/latest.html") | ||||
|             print(pagectrl.singlepage(html, tcloud, latest)) | ||||
|             tcloud = make_tagcloud(tagcloud, "../tags/%s/latest.html") | ||||
|             pagectrl.paginate(adjust_css(html, lvl), tcloud, timeline, ovr) | ||||
|             pagectrl.paginate( | ||||
|                 demote_css(html, config["relative_css"], lvl), tcloud, timeline, ovr) | ||||
|         else: # if timelines per tag | ||||
|             ovr = True | ||||
|             lvl = 2 | ||||
|             newhtml = demote_css(html, config["relative_css"], lvl) | ||||
|             tcloud = make_tagcloud(tagcloud, "../%s/latest.html") | ||||
|             fn = "%s/latest.html" % subdir | ||||
|             with open(fn, 'w') as f: | ||||
|                 pagectrl.written.append(fn) | ||||
|                 f.write( | ||||
|                     pagectrl.singlepage( | ||||
|                         adjust_css(html,lvl), tcloud, latest, p=".")) | ||||
|                         newhtml, tcloud, latest, p=".")) | ||||
|                 pagectrl.paginate( | ||||
|                     adjust_css(html, lvl), tcloud, timeline, ovr) | ||||
|                     newhtml, tcloud, timeline, ovr) | ||||
|         return pagectrl.written | ||||
| 
 | ||||
|     import toml | ||||
|     def load_settings(): | ||||
|         s = dict() | ||||
|         filename = "settings.toml" | ||||
|         if os.path.exists(filename): | ||||
|             with open(filename, 'r') as f: | ||||
|                 s = toml.loads(f.read()) | ||||
|         else: | ||||
|             s = None | ||||
|         return s | ||||
| 
 | ||||
|     def main(): | ||||
|         tpl, content, email = get_args() | ||||
|         tl, tc, tg = get_posts(content, email) | ||||
|         tpl, content = get_args() | ||||
|         # read settings file | ||||
|         cfg = load_settings() | ||||
|         if cfg == None: | ||||
|             print("exit: no settings.toml found.", file=sys.stderr) | ||||
|             return | ||||
|         if "post" not in cfg: | ||||
|             print("exit: table 'post' absent in settings.toml", file=sys.stderr) | ||||
|             return | ||||
|         if "page" not in cfg: | ||||
|             print("exit: table 'page' absent in settings.toml", file=sys.stderr) | ||||
|             return | ||||
|         tl, tc, tg = get_posts(content, cfg["post"]) | ||||
|         # main timeline | ||||
|         updated = [] | ||||
|         updated += writepage(tpl, tl, tc) | ||||
|         updated += writepage(tpl, tl, tc, cfg["page"]) | ||||
|         # timeline per tag | ||||
|         for key in tg.keys(): | ||||
|             tagline = [] | ||||
|             for index in tg[key]: | ||||
|                 tagline.append(tl[index]) | ||||
|             # [1:] means to omit hashtag from dir name | ||||
|             updated += writepage(tpl, tagline, tc, "./tags/%s" % key[1:])  | ||||
|             updated += writepage( | ||||
|                 tpl, tagline, tc, cfg["page"], \ | ||||
|                 subdir="tags/%s" % key[1:]     \ | ||||
|             )  | ||||
|         with open("updatedfiles.txt", 'w') as f: | ||||
|             for filename in updated: | ||||
|                 print(filename, file=f) # sys.stderr) | ||||
|             if "latestpage" in cfg: | ||||
|                 print(cfg["latestpage"], file=f) | ||||
|     main() | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| 
 | ||||
| import sys, subprocess, getpass, pycurl, urllib.parse | ||||
| if __name__ == "__main__": | ||||
|     def api_upload(endpoint, dest_fmt = "/microblog%s%s"): | ||||
|     def api_upload(endpoint, dest_fmt = "/microblog/%s"): | ||||
|         pages = [] | ||||
|         with open("updatedfiles.txt") as f: | ||||
|             pages = f.readlines() | ||||
| @ -10,11 +10,11 @@ if __name__ == "__main__": | ||||
|         c.setopt(c.POST, 1) | ||||
|         for page in pages: | ||||
|             p = page.strip('\n') | ||||
|             i = p.rfind('/') | ||||
|             #i = p.rfind('/') | ||||
|             # folder = p[1:i] | ||||
|             # file   = p[i] | ||||
|             destination = dest_fmt % (p[1:i], p[i:]) | ||||
|             source = p[2:] # omit './' | ||||
|             destination = dest_fmt % p | ||||
|             source = p | ||||
|             print("sending @%s to %s" % (source, destination)) | ||||
|             exists = True | ||||
|             try: | ||||
| @ -40,10 +40,9 @@ if __name__ == "__main__": | ||||
|         except KeyboardInterrupt: | ||||
|             print("Aborted.") | ||||
|             return | ||||
|         finally: | ||||
|             if len(pw) == 0: | ||||
|                 print("Empty input. Exiting.") | ||||
|                 return | ||||
|         if len(pw) == 0: | ||||
|             print("Empty input. Exiting.") | ||||
|             return | ||||
|         p = urllib.parse.quote(pw, safe='') | ||||
|         target = "https://%s:%s@neocities.org/api/upload" % (sys.argv[1], p) | ||||
|         del pw | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user