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
|
## 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
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 {
|
|
||||||
|
@media only screen and (min-width: 768px) {
|
||||||
|
.column {
|
||||||
float: left;
|
float: left;
|
||||||
width: 30%;
|
width: 30%;
|
||||||
/* background-color: #1a1a1a; */
|
/* background-color: #1a1a1a; */
|
||||||
}
|
}
|
||||||
.timeline {
|
.timeline {
|
||||||
float: right;
|
float: right;
|
||||||
width: 67%;
|
width: 67%;
|
||||||
/* background-color: #1a1a1a; */
|
/* background-color: #1a1a1a; */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.postcell {
|
.postcell {
|
||||||
border: 1px solid gray;
|
border: 1px solid gray;
|
||||||
@ -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;
|
||||||
|
169
microblog.py
169
microblog.py
@ -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()
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user