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: "";
|
||||
|
183
microblog.py
183
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 = '''
|
||||
<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,14 +118,22 @@ 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
|
||||
def get_posts(filename, config):
|
||||
class Post:
|
||||
class Post:
|
||||
def __init__(self, ts, msg):
|
||||
self.timestamp = ts # string
|
||||
self.timestamp = ts.strip() # string
|
||||
self.message = msg # list
|
||||
|
||||
def parse_txt(filename):
|
||||
# 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()
|
||||
@ -130,9 +147,7 @@ def get_posts(filename, config):
|
||||
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')
|
||||
timestamp = line
|
||||
state = 1
|
||||
elif state == 1:
|
||||
if len(line) > 1:
|
||||
@ -145,6 +160,7 @@ def get_posts(filename, config):
|
||||
state = 0
|
||||
return posts
|
||||
|
||||
def get_posts(filename, config):
|
||||
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:
|
||||
count = len(timeline)
|
||||
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)
|
||||
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)
|
||||
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