squash merge toml

This commit is contained in:
likho 2023-02-14 20:36:52 -08:00
parent c787cf3ac8
commit 04117c6479
5 changed files with 184 additions and 103 deletions

View File

@ -5,19 +5,22 @@ Simple and stylish text-to-html microblog generator.
## Requirements ## 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 ### Usage
Send three arguments minimum to `python`. The fourth argument for an e-mail address is optional. 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: 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. 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. 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. * `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. * `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 ### Writing Content
See `example/demo.txt`. See `example/demo.txt`.

13
example/settings.toml Normal file
View 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>"

View File

@ -1,3 +1,5 @@
@media only screen and (min-width: 768px) {
.column { .column {
float: left; float: left;
width: 30%; width: 30%;
@ -8,6 +10,7 @@
width: 67%; width: 67%;
/* background-color: #1a1a1a; */ /* background-color: #1a1a1a; */
} }
}
.postcell { .postcell {
border: 1px solid gray; border: 1px solid gray;
text-align: left; text-align: left;
@ -18,8 +21,9 @@
white-space: pre-wrap; white-space: pre-wrap;
word-wrap: break-word; word-wrap: break-word;
} }
.reply { .buttons {
margin: 0.5em margin-left: 1em;
margin-bottom:0.5em;
} }
.timestamp { .timestamp {
text-align: right; text-align: right;

View File

@ -1,21 +1,31 @@
import sys, os, subprocess 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 # apply div classes for use with .css
def make_post(num, timestamp, email, msg): def make_post(num, timestamp, conf, msg):
output = ""
fmt = ''' fmt = '''
<div class=\"postcell\" id=\"%i\"> <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> <div class=\"message\">%s</div>
''' '''
if email != None: if "buttons" in conf:
fmt += "<div class=\"reply\"><a href=\"mailto:%s?subject=p%i\">[reply]</a></div></div>" b = make_buttons(conf["buttons"], num)
output = fmt % (num, num, timestamp, msg, email, num) fmt += b
else:
fmt += "</div>" fmt += "</div>"
output = fmt % (num, num, timestamp, msg) return fmt % (num, timestamp, num, num, msg)
return output
def make_gallery(i, w): def make_gallery(i, w):
tag = [] tag = []
@ -33,21 +43,34 @@ def make_gallery(i, w):
tag.append("</div>") tag.append("</div>")
return tag return tag
# apply basic HTML formatting - only div class here is gallery
from html import escape def markup(message, config):
# iterate through posts and get information about them def is_image(s, image_formats):
def get_posts(filename, email): l = s.rsplit('.', maxsplit=1)
class Post: if len(l) < 2:
def __init__(self, ts, msg): return False
self.timestamp = ts # string # Python 3.10.5
self.message = msg # list # example result that had to be filtered:
def markup(self): # 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 result = 0
tagged = "" tagged = ""
# support multiple images (gallery style) # support multiple images (gallery style)
tags = [] # list of strings tags = [] # list of strings
output = [] output = []
for line in self.message: gallery = []
ptags = config["tag_paragraphs"]
sep = ""
if "line_separator" in config:
sep = config["line_separator"]
for line in message:
images = [] # list of integers images = [] # list of integers
words = line.split() words = line.split()
for i in range(len(words)): for i in range(len(words)):
@ -69,13 +92,29 @@ def get_posts(filename, email):
if len(w) > 1: if len(w) > 1:
new_word += w[1] new_word += w[1]
words[i] = new_word words[i] = new_word
elif word.find(".jpg") != -1 or word.find(".png") != -1: elif is_image(word, config["accepted_images"]):
images.append(i) images.append(i)
if len(images) > 0: if len(images) > 0:
# function invokes pop() which modifies list 'words'
gallery = make_gallery(images, words) gallery = make_gallery(images, words)
words += gallery if ptags and len(words) > 0:
words.insert(0,"<p>")
words.append("</p>")
output.append(" ".join(words)) output.append(" ".join(words))
return "<br>".join(output), tags # 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, config):
class Post:
def __init__(self, ts, msg):
self.timestamp = ts # string
self.message = msg # list
def parse_txt(filename): def parse_txt(filename):
content = [] content = []
@ -114,12 +153,13 @@ def get_posts(filename, email):
count = total count = total
index = count # - 1 index = count # - 1
timeline = [] timeline = []
btns = None
for post in posts: for post in posts:
markedup, tags = post.markup() markedup, tags = markup(post.message, config)
count -= 1 count -= 1
index -= 1 index -= 1
timeline.append( timeline.append(
make_post(count, post.timestamp, email, markedup) make_post(count, post.timestamp, config, markedup)
) )
for tag in tags: for tag in tags:
if tagcloud.get(tag) == None: if tagcloud.get(tag) == None:
@ -143,21 +183,12 @@ def make_tagcloud(d, rell):
return output return output
class Paginator: class Paginator:
def __init__(self, x, loc="./pages"): def __init__(self, x, ppp, loc="pages"):
if x <= 0: if x <= 0:
print("Error: No posts (x=%i" % x, file=sys.stderr) print("Error: No posts (x=%i" % x, file=sys.stderr)
raise Exception raise Exception
self.TOTAL_POSTS = x self.TOTAL_POSTS = x
try: self.PPP = ppp
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.TOTAL_PAGES = int(x/self.PPP) self.TOTAL_PAGES = int(x/self.PPP)
self.SUBDIR = loc self.SUBDIR = loc
if not os.path.exists(loc): if not os.path.exists(loc):
@ -221,43 +252,42 @@ if __name__ == "__main__":
def get_args(): def get_args():
argc = len(sys.argv) argc = len(sys.argv)
if argc < 3: if argc < 3:
msg = '''This is microblog.py. (%s/3 arguments given; 4 maximum) msg = '''This is microblog.py. (%s/3 arguments given)
\tpython microblog.py [template] [content] [optional: email] \tpython microblog.py [template] [content]
''' '''
print(msg % argc, file=sys.stderr) print(msg % argc, file=sys.stderr)
exit() exit()
# script = argv[0] # script = argv[0]
template = sys.argv[1] template = sys.argv[1]
content = sys.argv[2] content = sys.argv[2]
email = sys.argv[3] if (argc >= 4) else None return template, content
return template, content, email
# assume relative path # assume relative path
def adjust_css(template, level=1): def demote_css(template, cssl, level=1):
prepend = "" prepend = ""
if level == 1: if level == 1:
prepend = '.' prepend = '.'
else: else:
for i in range(level): for i in range(level):
prepend = ("../%s" % prepend) prepend = ("../%s" % prepend)
css1 = "./style.css" tpl = template
css2 = "./timeline.css" for css in cssl:
#sys.stderr.write(("%s%s" % (prepend, css1) )) tpl = tpl.replace(css, ("%s%s" % (prepend, css) ))
tpl = template.replace(css1, ("%s%s" % (prepend, css1) )) return tpl
return tpl.replace(css2, ("%s%s" % (prepend, css2) ))
# needs review / clean-up # needs review / clean-up
# ideally relate 'lvl' with sub dir instead of hardcoding # 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 = "" html = ""
with open(template,'r') as f: with open(template,'r') as f:
html = f.read() html = f.read()
count = len(timeline) count = len(timeline)
try: try:
p = config["postsperpage"]
if subdir == None: if subdir == None:
pagectrl = Paginator(count) pagectrl = Paginator(count, p)
else: else:
pagectrl = Paginator(count, subdir) pagectrl = Paginator(count, p, subdir)
if not os.path.exists(subdir): if not os.path.exists(subdir):
os.mkdir(subdir) os.mkdir(subdir)
except: except:
@ -271,35 +301,64 @@ if __name__ == "__main__":
tcloud = make_tagcloud(tagcloud, "./tags/%s/latest.html") tcloud = make_tagcloud(tagcloud, "./tags/%s/latest.html")
print(pagectrl.singlepage(html, tcloud, latest)) print(pagectrl.singlepage(html, tcloud, latest))
tcloud = make_tagcloud(tagcloud, "../tags/%s/latest.html") 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 else: # if timelines per tag
ovr = True ovr = True
lvl = 2 lvl = 2
newhtml = demote_css(html, config["relative_css"], lvl)
tcloud = make_tagcloud(tagcloud, "../%s/latest.html") tcloud = make_tagcloud(tagcloud, "../%s/latest.html")
fn = "%s/latest.html" % subdir fn = "%s/latest.html" % subdir
with open(fn, 'w') as f: with open(fn, 'w') as f:
pagectrl.written.append(fn) pagectrl.written.append(fn)
f.write( f.write(
pagectrl.singlepage( pagectrl.singlepage(
adjust_css(html,lvl), tcloud, latest, p=".")) newhtml, tcloud, latest, p="."))
pagectrl.paginate( pagectrl.paginate(
adjust_css(html, lvl), tcloud, timeline, ovr) newhtml, tcloud, timeline, ovr)
return pagectrl.written 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(): def main():
tpl, content, email = get_args() tpl, content = get_args()
tl, tc, tg = get_posts(content, email) # 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 # main timeline
updated = [] updated = []
updated += writepage(tpl, tl, tc) updated += writepage(tpl, tl, tc, cfg["page"])
# timeline per tag # timeline per tag
for key in tg.keys(): for key in tg.keys():
tagline = [] tagline = []
for index in tg[key]: for index in tg[key]:
tagline.append(tl[index]) tagline.append(tl[index])
# [1:] means to omit hashtag from dir name # [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: with open("updatedfiles.txt", 'w') as f:
for filename in updated: for filename in updated:
print(filename, file=f) # sys.stderr) print(filename, file=f) # sys.stderr)
if "latestpage" in cfg:
print(cfg["latestpage"], file=f)
main() main()

View File

@ -1,7 +1,7 @@
import sys, subprocess, getpass, pycurl, urllib.parse import sys, subprocess, getpass, pycurl, urllib.parse
if __name__ == "__main__": if __name__ == "__main__":
def api_upload(endpoint, dest_fmt = "/microblog%s%s"): def api_upload(endpoint, dest_fmt = "/microblog/%s"):
pages = [] pages = []
with open("updatedfiles.txt") as f: with open("updatedfiles.txt") as f:
pages = f.readlines() pages = f.readlines()
@ -10,11 +10,11 @@ if __name__ == "__main__":
c.setopt(c.POST, 1) c.setopt(c.POST, 1)
for page in pages: for page in pages:
p = page.strip('\n') p = page.strip('\n')
i = p.rfind('/') #i = p.rfind('/')
# folder = p[1:i] # folder = p[1:i]
# file = p[i] # file = p[i]
destination = dest_fmt % (p[1:i], p[i:]) destination = dest_fmt % p
source = p[2:] # omit './' source = p
print("sending @%s to %s" % (source, destination)) print("sending @%s to %s" % (source, destination))
exists = True exists = True
try: try:
@ -40,7 +40,6 @@ if __name__ == "__main__":
except KeyboardInterrupt: except KeyboardInterrupt:
print("Aborted.") print("Aborted.")
return return
finally:
if len(pw) == 0: if len(pw) == 0:
print("Empty input. Exiting.") print("Empty input. Exiting.")
return return