diff --git a/README.md b/README.md index ac6513b..4017cc4 100644 --- a/README.md +++ b/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`. diff --git a/example/settings.toml b/example/settings.toml new file mode 100644 index 0000000..151a206 --- /dev/null +++ b/example/settings.toml @@ -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

tags to each line. +tag_paragraphs=true +# adds
or user defined string between each line +# line_separator="
" diff --git a/example/timeline.css b/example/timeline.css index b503684..2db28a2 100644 --- a/example/timeline.css +++ b/example/timeline.css @@ -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; diff --git a/microblog.py b/microblog.py index 47d800c..7f8d0b0 100644 --- a/microblog.py +++ b/microblog.py @@ -1,21 +1,31 @@ import sys, os, subprocess +# returns html-formatted string +def make_buttons(btn_dict, msg_id): + buttons = "
" + fmt = "[%s]" + 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 += "
" + 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 = '''
-
%s
+
%s(#%i)
%s
''' - if email != None: - fmt += "
[reply]
" - output = fmt % (num, num, timestamp, msg, email, num) - else: - fmt += "" - output = fmt % (num, num, timestamp, msg) - return output + if "buttons" in conf: + b = make_buttons(conf["buttons"], num) + fmt += b + fmt += "" + return fmt % (num, timestamp, num, num, msg) def make_gallery(i, w): tag = [] @@ -33,49 +43,78 @@ def make_gallery(i, w): tag.append("") 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 = ("%s") % (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 = "%s" % (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,"

") + words.append("

") + 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 = ("%s") % (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 = "%s" % (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 "
".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() diff --git a/neouploader.py b/neouploader.py index 1922a84..c4350c1 100644 --- a/neouploader.py +++ b/neouploader.py @@ -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