Compare commits
30 Commits
master
...
syndicatio
Author | SHA1 | Date | |
---|---|---|---|
0ae9d3ddd7 | |||
84e647a390 | |||
9fea166b94 | |||
2ff8027d34 | |||
4c16afebf9 | |||
4e4f2f2a7a | |||
9b2e9ed74d | |||
db14ea6b3d | |||
eec7bc5fff | |||
0e80b8220a | |||
9fea7d8275 | |||
|
57bd52e537 | ||
|
0eb134136f | ||
|
fd96708790 | ||
|
0bccc33730 | ||
|
de78ad42b8 | ||
|
9fbdf2a8a7 | ||
|
26b7d47996 | ||
7d474af899 | |||
3bf37386fc | |||
e4af5bbd6a | |||
972165018c | |||
|
8a723c2bc8 | ||
|
f2afe19bc9 | ||
|
10740cae92 | ||
|
d8c4cb47fa | ||
|
9905a15ace | ||
|
427d4ff972 | ||
d9e998750c | |||
0b8b5ce498 |
41
README.md
41
README.md
@ -5,18 +5,13 @@ Simple and stylish text-to-html microblog generator.
|
||||
|
||||
## Requirements
|
||||
|
||||
The following python modules are used within the repository.
|
||||
python3 dateutil toml make curl pycurl urllib
|
||||
|
||||
toml tomlkit python_dateutil pycurl
|
||||
* `dateutil`, `toml`, `json`, `pycurl`, `hashlib` are Python modules.
|
||||
* `make` (optional), method for invoking the script.
|
||||
* `urllib` (optional), for uploading multiple files to neocities (`neouploader.py`).
|
||||
|
||||
* `tomlkit` (optional), for maintaining the configuration file between updates (`check-settings.py`).
|
||||
|
||||
Some Gnu core utilities are expected to be present but can be substituted for other means.
|
||||
|
||||
* `make` (optional), to invoke the script using Makefiles
|
||||
* `date` (optional), to generate timestamps when writing posts
|
||||
|
||||
## Usage
|
||||
### Usage
|
||||
|
||||
The following generates a sample page `result.html`.
|
||||
|
||||
@ -29,12 +24,16 @@ Using `make` is uptional; it does the following within a new directory:
|
||||
cp example/timeline.css ./timeline.css
|
||||
cp example/default.tpl ./template.tpl
|
||||
cp example/demo.txt ./content.txt
|
||||
python src/microblog.py ./template.tpl ./content.txt > result.html
|
||||
python microblog.py ./template.tpl ./content.txt > result.html
|
||||
|
||||
This script generate a text file after operation.
|
||||
|
||||
* `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`.
|
||||
@ -57,26 +56,6 @@ The content file is a plain text file of posts. Each post has two types of infor
|
||||
* the two last lines of the file must be empty
|
||||
* html can be placed in the message for embedded videos and rich text
|
||||
|
||||
## Configuration
|
||||
|
||||
Settings are read from `settings.toml`. See `example/settings.toml`.
|
||||
|
||||
Configuration options as understood by the script are tentative and may change in the future.
|
||||
|
||||
### A key may be missing from your settings file (KeyError)
|
||||
|
||||
>I'm getting KeyError when I run the program
|
||||
|
||||
>This script is throwing KeyError after I ran git pull
|
||||
|
||||
In most cases, this means I added new configuration options. You can resolve this error by adding missing keys from `example/settings.toml` to `settings.toml`.
|
||||
|
||||
The following command can check for missing keys and update if needed.
|
||||
|
||||
python src/check-settings.py
|
||||
|
||||
Missing keys if any are initialized to default values from `example/settings.toml`.
|
||||
|
||||
## Anything else
|
||||
|
||||
This is a script I wrote for personal use. The output can be seen on [https://likho.neocities.org/microblog/index.html](https://likho.neocities.org/microblog/index.html). I figure someone else may want to use it for their own personal websites, so it is published.
|
||||
|
@ -1,12 +1,8 @@
|
||||
|
||||
|
||||
all: demo tpl css settings
|
||||
python src/microblog.py ./template.tpl ./content.txt > result.html
|
||||
python microblog.py ./template.tpl ./content.txt > result.html
|
||||
|
||||
check:
|
||||
python src/check-settings.py
|
||||
|
||||
# first time run only
|
||||
tpl:
|
||||
cp ./example/default.tpl ./template.tpl
|
||||
|
||||
|
@ -1,44 +1,42 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta content="initial-scale=1.0">
|
||||
<title>Microblog</title>
|
||||
|
||||
<!-- <link href="./style.css" rel="stylesheet" type="text/css" media="all"> -->
|
||||
<link href="./style.css" rel="stylesheet" type="text/css" media="all">
|
||||
<link href="./timeline.css" rel="stylesheet" type="text/css" media="all">
|
||||
</head>
|
||||
<body>
|
||||
<div class="content">
|
||||
|
||||
|
||||
<header>
|
||||
<h1>A Microblog in Plain HTML</h1>
|
||||
</header>
|
||||
|
||||
<aside class="column profile">
|
||||
<figure>
|
||||
<img src="images/avatar.jpg" alt="(Avatar)" class="avatar">
|
||||
<span>Your Name Here</span>
|
||||
</figure>
|
||||
<p>
|
||||
<a href="mailto:user@host.tld">user@host.tld</a>
|
||||
</p>
|
||||
<h2>About Me</h2>
|
||||
<p>Your self-description here.</p>
|
||||
<p>{postcount} total posts</p>
|
||||
<div class = "row"> <div class = "column">
|
||||
<div class="profile">
|
||||
<img src="./images/avatar.jpg" alt="Avatar" class="avatar">
|
||||
<span class="handle">Your Name Here</span>
|
||||
<p><span class="email"><a href="mailto:user@host.tld">user@host.tld</a></span></p>
|
||||
<div class="bio">Description
|
||||
<h4>{postcount} total posts</h4>
|
||||
<h3>Tags</h3>
|
||||
<nav>{tags}</nav>
|
||||
<p>{tags}</p>
|
||||
<h3>Pages</h3>
|
||||
<nav>{pages}</nav>
|
||||
</aside>
|
||||
<p>{pages}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main class="timeline">
|
||||
<div class = "timeline">
|
||||
{timeline}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<center>
|
||||
<a href="https://notabug.org/likho/microblog.py">microblog.py</a>
|
||||
</footer>
|
||||
</center>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
@ -4,36 +4,24 @@ latestpages=["meta.json", "result.html"]
|
||||
[page]
|
||||
postsperpage = 20
|
||||
relative_css=["./style.css", "./timeline.css"]
|
||||
# this would be "latest.html" in earlier versions i.e
|
||||
# user.domain.tld/microblog/tags/tagname/latest.html
|
||||
# naming it as index enables paths like so
|
||||
# user.domain.tld/microblog/tags/tagname
|
||||
landing_page="index.html"
|
||||
|
||||
[post]
|
||||
accepted_images= ["jpg", "JPG", "png", "PNG"]
|
||||
# true = add <p></p> tags to each line.
|
||||
tag_paragraphs=true
|
||||
# apply <p> tags even if a line contains the following
|
||||
inline_tags = ["i", "em", "b", "strong","u", "s", "a", "span"]
|
||||
date_format="%d %B %Y"
|
||||
# adds <br> or user defined string between each line
|
||||
# line_separator="<br>"
|
||||
format="""
|
||||
<article id="{__num__}">
|
||||
<h4>
|
||||
<time>{__timestamp__}</time>
|
||||
<div class="postcell" id="{__num__}">
|
||||
<div class="timestamp">{__timestamp__}
|
||||
<a href=#{__num__}>(#{__num__})</a>
|
||||
</h4>
|
||||
{__msg__}
|
||||
</div>
|
||||
<div class="message">{__msg__}</div>
|
||||
{__btn__}
|
||||
</article>
|
||||
</div>
|
||||
"""
|
||||
|
||||
[post.buttons]
|
||||
format="""
|
||||
<a class="buttons" href="{__url__}">{__label__}</a>
|
||||
"""
|
||||
|
||||
[post.buttons.links]
|
||||
reply = "mailto:user@host.tld"
|
||||
test = "https://toml.io/en/v1.0.0#array-of-tables"
|
||||
interact = "https://yoursite.tld/cgi?postid="
|
||||
@ -50,25 +38,24 @@ file_output="meta.json"
|
||||
username="Your name here"
|
||||
url="https://yourdomain.tld/microblog/"
|
||||
avatar="https://yourdomain.tld/microblog/images/avatar.jpg"
|
||||
short-bio= "Your self-description. Anything longer than 150 characters is truncated."
|
||||
short_bio= "Your self-description. Anything longer than 150 characters is truncated."
|
||||
|
||||
[webring.following]
|
||||
list= ["https://likho.neocities.org/microblog/meta.json"]
|
||||
date_format = "%Y %b %d"
|
||||
format="""
|
||||
<article>
|
||||
<figure>
|
||||
<div class="fill">
|
||||
<div class="postcell">
|
||||
<img src="{__avatar__}" alt="Avatar" class="avatar">
|
||||
<figcaption>
|
||||
<ul>
|
||||
<li><a href="{__url__}" title="microblog of {__handle__}">{__handle__}</a></li>
|
||||
<li><time>Last Update: {__lastupdated__}</time></li>
|
||||
<li>Posts: {__post_count__}</li>
|
||||
</ul>
|
||||
</figcaption>
|
||||
</figure>
|
||||
<span class="wrapper"">
|
||||
<div class="handle">
|
||||
<a href="{__url__}">{__handle__}</a>
|
||||
</div>
|
||||
<div class="last-updated">Last Update: {__lastupdated__}</div>
|
||||
<span class="post-count">Posts: {__post_count__}</span>
|
||||
</span>
|
||||
<p class="short-bio">{__shortbio__}</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
# internally link avatars - avoids hotlinks
|
||||
|
@ -1,76 +1,61 @@
|
||||
|
||||
body {
|
||||
max-width:95%;
|
||||
margin:auto;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 768px) {
|
||||
.column {
|
||||
float: left;
|
||||
width: 30%;
|
||||
width: 32%;
|
||||
}
|
||||
.timeline {
|
||||
float: right;
|
||||
width: 67%;
|
||||
}
|
||||
}
|
||||
|
||||
/* POSTING */
|
||||
|
||||
/* .postcell */
|
||||
.timeline article {
|
||||
.postcell {
|
||||
border: 1px solid red;
|
||||
text-align: left;
|
||||
margin: 0.25em 0
|
||||
}
|
||||
.timeline article h4 {
|
||||
text-align: right;
|
||||
margin: 0.5em
|
||||
}
|
||||
.timeline article h4 ~ * {
|
||||
.message {
|
||||
margin: 1em 1em 1em 3em;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.buttons {
|
||||
margin-left: 1em;
|
||||
margin-bottom:0.5em;
|
||||
}
|
||||
.timestamp {
|
||||
text-align: right;
|
||||
margin: 0.5em
|
||||
}
|
||||
.hashtag {
|
||||
color: green;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* PROFILE */
|
||||
.column figure {
|
||||
margin-left: 3%;
|
||||
}
|
||||
.avatar {
|
||||
vertical-align: middle;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
.column {
|
||||
border:1px solid blue;
|
||||
.column .profile {
|
||||
vertical-align: middle;
|
||||
padding-left: 10px;
|
||||
padding:1%;
|
||||
border:1px solid blue;
|
||||
}
|
||||
.profile .handle{
|
||||
.column .profile .handle{
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
}
|
||||
.profile .email{
|
||||
.column .profile .email{
|
||||
font-size: 0.8em;
|
||||
text-align:left;
|
||||
text-decoration:none;
|
||||
}
|
||||
.profile .bio {
|
||||
.column .profile .bio {
|
||||
font-size: 0.9em;
|
||||
vertical-align: middle;
|
||||
margin: 1em
|
||||
}
|
||||
|
||||
/* IMAGES */
|
||||
|
||||
.gallery {
|
||||
margin:auto;
|
||||
display: flex;
|
||||
@ -89,41 +74,31 @@ body {
|
||||
border: 1px solid #777;
|
||||
filter: invert(100%);
|
||||
}
|
||||
|
||||
/* WEBRING */
|
||||
|
||||
.timeline article figure img {
|
||||
.postcell .avatar {
|
||||
margin-left:3%;
|
||||
margin-top:2%;
|
||||
height: 4em;
|
||||
width:auto;
|
||||
vertical-align:top;
|
||||
}
|
||||
.timeline article figure {
|
||||
display:flex;
|
||||
margin-left:0;
|
||||
}
|
||||
.timeline article figcaption {
|
||||
margin-left: 3%;
|
||||
.postcell .wrapper {
|
||||
margin-top:2%;
|
||||
display: inline-block;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
.timeline article figcaption ul {
|
||||
list-style-type:none;
|
||||
padding-left:0;
|
||||
.postcell .wrapper .last-updated,
|
||||
.postcell .wrapper .post-count {
|
||||
font-size: 1em;
|
||||
color:grey;
|
||||
}
|
||||
.timeline article figcaption p {
|
||||
margin-top:0;
|
||||
margin-bottom:0;
|
||||
}
|
||||
.timeline article .short-bio{
|
||||
.postcell .short-bio{
|
||||
padding-left: 3%;
|
||||
padding-right: 2%;
|
||||
font-style: italic;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align:center;
|
||||
/* Clear floats after the columns */
|
||||
.row:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
|
@ -3,15 +3,17 @@ import sys, os, traceback
|
||||
import dateutil.parser
|
||||
from time import strftime, localtime
|
||||
|
||||
def make_buttons(btn_conf, msg_id):
|
||||
fmt = btn_conf["format"]
|
||||
buttons = str()
|
||||
for key in btn_conf["links"]:
|
||||
url = btn_conf["links"][key]
|
||||
# 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.format(
|
||||
__url__=url, __label__ = key)
|
||||
buttons += fmt % (url,key)
|
||||
buttons += "</div>"
|
||||
return buttons
|
||||
|
||||
# apply div classes for use with .css
|
||||
@ -52,32 +54,6 @@ def make_gallery(indices, w, conf=None):
|
||||
return tag
|
||||
|
||||
# apply basic HTML formatting - only div class here is gallery
|
||||
from html.parser import HTMLParser
|
||||
class My_Html_Parser(HTMLParser):
|
||||
def __init__(self, ignore_list):
|
||||
super().__init__()
|
||||
self.stack = []
|
||||
self.completed_by = ""
|
||||
# ignore common inline tags
|
||||
self.ignore = ignore_list
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
self.stack.append(tag)
|
||||
self.is_completed_by = ""
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
# remove an item == tag from the end of the list
|
||||
i = len(self.stack) - 1
|
||||
last = self.stack[i]
|
||||
while i > -1:
|
||||
if tag == last:
|
||||
self.stack.pop(i)
|
||||
break
|
||||
i -= 1
|
||||
last = self.stack[i]
|
||||
if self.stack == [] and tag not in self.ignore:
|
||||
self.completed_by = "</%s>" % tag
|
||||
|
||||
from html import escape
|
||||
def markup(message, config):
|
||||
def is_image(s, image_formats):
|
||||
@ -95,18 +71,28 @@ def markup(message, config):
|
||||
return True
|
||||
return False
|
||||
|
||||
def automarkup(list_of_words):
|
||||
images = []
|
||||
tags = []
|
||||
for i in range(len(list_of_words)):
|
||||
word = list_of_words[i]
|
||||
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)
|
||||
list_of_words[i] = new_word
|
||||
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
|
||||
@ -116,41 +102,17 @@ def markup(message, config):
|
||||
new_word = "<span class=\"hashtag\">%s</span>" % (w[0])
|
||||
if len(w) > 1:
|
||||
new_word += w[1]
|
||||
list_of_words[i] = new_word
|
||||
words[i] = new_word
|
||||
elif is_image(word, config["accepted_images"]):
|
||||
images.append(i)
|
||||
return list_of_words, images, tags
|
||||
|
||||
tags = [] # list of strings
|
||||
output = []
|
||||
gallery = []
|
||||
ptags = config["tag_paragraphs"]
|
||||
ignore = []
|
||||
if "inline_tags" in config:
|
||||
ignore = config["inline_tags"]
|
||||
parser = My_Html_Parser(ignore)
|
||||
sep = ""
|
||||
for line in message:
|
||||
images = [] # list of integers
|
||||
parser.feed(line)
|
||||
if parser.stack == [] \
|
||||
and (parser.completed_by == "" or parser.completed_by not in line):
|
||||
words, images, t = automarkup(line.split())
|
||||
tags += t
|
||||
if len(images) > 0:
|
||||
# function invokes pop() which modifies list 'words'
|
||||
gc = config["gallery"] if "gallery" in config else None
|
||||
gallery = make_gallery(images, words, gc)
|
||||
elif ptags and len(words) > 0:
|
||||
if ptags and len(words) > 0:
|
||||
words.insert(0,"<p>")
|
||||
words.append("</p>")
|
||||
output.append(" ".join(words))
|
||||
elif "pre" in parser.stack \
|
||||
and ("<pre>" not in line \
|
||||
and "<code>" not in line and "</code>" not in line):
|
||||
output.append(escape(line))
|
||||
else: # <pre> is in the parser.stack
|
||||
output.append(line.strip())
|
||||
# avoid paragraph with an image gallery
|
||||
if len(gallery) > 0:
|
||||
output.append("".join(gallery))
|
||||
@ -168,11 +130,9 @@ class Post:
|
||||
return int(t.timestamp())
|
||||
|
||||
# format used for display
|
||||
def get_short_time(self, form):
|
||||
if form == "":
|
||||
form = "%y %b %d"
|
||||
def get_short_time(self):
|
||||
t = dateutil.parser.parse(self.timestamp)
|
||||
return t.strftime(form)
|
||||
return t.strftime("%y %b %d")
|
||||
|
||||
def parse_txt(filename):
|
||||
content = []
|
||||
@ -201,7 +161,7 @@ def parse_txt(filename):
|
||||
state = 0
|
||||
return posts
|
||||
|
||||
def get_posts(posts, config, newest = None):
|
||||
def get_posts(posts, config):
|
||||
taginfos = []
|
||||
tagcloud = dict() # (tag, count)
|
||||
tagged = dict() # (tag, index of message)
|
||||
@ -209,29 +169,21 @@ def get_posts(posts, config, newest = None):
|
||||
count = total
|
||||
index = count # - 1
|
||||
timeline = []
|
||||
df = ""
|
||||
subset = []
|
||||
if "date_format" in config:
|
||||
df = config["date_format"]
|
||||
btns = None
|
||||
for post in posts:
|
||||
markedup, tags = markup(post.message, config)
|
||||
count -= 1
|
||||
index -= 1
|
||||
timeline.append(
|
||||
make_post(count, post.get_short_time(df), config, markedup)
|
||||
make_post(count, post.get_short_time(), config, markedup)
|
||||
)
|
||||
for tag in tags:
|
||||
if tagcloud.get(tag) == None:
|
||||
tagcloud[tag] = 0
|
||||
tagcloud[tag] += 1
|
||||
if newest is not None and (total - (1 + count)) < newest:
|
||||
subset.append(tag)
|
||||
if newest is None \
|
||||
or newest is not None and tag in subset:
|
||||
if tagged.get(tag) == None:
|
||||
tagged[tag] = []
|
||||
tagged[tag].append(index)
|
||||
# print(tagged, file=sys.stderr)
|
||||
return timeline, tagcloud, tagged
|
||||
|
||||
def make_tagcloud(d, rell):
|
||||
@ -288,14 +240,16 @@ class Paginator:
|
||||
|
||||
def paginate(self, template, tagcloud, timeline, is_tagline=False):
|
||||
outfile = "%s/%s" % (self.SUBDIR, self.FILENAME)
|
||||
l = len(timeline)
|
||||
for i in range(0, self.TOTAL_PAGES):
|
||||
timeline.reverse() # reorder from oldest to newest
|
||||
start = 0
|
||||
for i in range(start, self.TOTAL_PAGES):
|
||||
fn = outfile % i
|
||||
with open(fn, 'w') as f:
|
||||
self.written.append(fn)
|
||||
prev = l - (self.PPP * i)
|
||||
curr = l - self.PPP * (i+1)
|
||||
sliced = timeline[curr:prev]
|
||||
prev = self.PPP * i
|
||||
curr = self.PPP * (i+1)
|
||||
sliced = timeline[prev:curr]
|
||||
sliced.reverse()
|
||||
f.write(self.singlepage(template, tagcloud, sliced, i, "."))
|
||||
return
|
||||
|
||||
@ -319,20 +273,19 @@ if __name__ == "__main__":
|
||||
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", action="store_true", \
|
||||
p.add_argument("--sort", \
|
||||
help="sorts content from oldest to newest"
|
||||
" (this is a separate operation from page generation)")
|
||||
p.add_argument("--skip-fetch", action="store_true", \
|
||||
" (this is a separate operation from page generation)", \
|
||||
action="store_true")
|
||||
p.add_argument("--skip-fetch", \
|
||||
help="skips fetching profile data from remote sources;"
|
||||
" has no effect if webring is not enabled")
|
||||
p.add_argument("--new-posts", type=int, nargs='?',
|
||||
help="generate pages based only on new entries; " \
|
||||
"if I wrote 5 new posts then --new-posts=5'")
|
||||
" has no effect if webring is not enabled",\
|
||||
action="store_true")
|
||||
args = p.parse_args()
|
||||
if args.sort:
|
||||
sort(args.content)
|
||||
exit()
|
||||
return args.template, args.content, args.skip_fetch, args.new_posts
|
||||
return args.template, args.content, args.skip_fetch
|
||||
|
||||
# assume relative path
|
||||
def demote_css(template, css_list, level=1):
|
||||
@ -347,7 +300,7 @@ if __name__ == "__main__":
|
||||
tpl = tpl.replace(css, ("%s%s" % (prepend, css) ))
|
||||
return tpl
|
||||
|
||||
def writepage(template, timeline, tagcloud, config, subdir = None, paginate = True):
|
||||
def writepage(template, timeline, tagcloud, config, subdir = None):
|
||||
count = len(timeline)
|
||||
html = ""
|
||||
with open(template,'r') as f:
|
||||
@ -361,11 +314,10 @@ if __name__ == "__main__":
|
||||
except Exception as e:
|
||||
print("error: ",e, ("(number of posts = %i)" % count), file=sys.stderr)
|
||||
exit()
|
||||
index = config["landing_page"]
|
||||
latest = timeline[:pagectrl.PPP]
|
||||
link_from_top = "./tags/%s/" + index
|
||||
link_from_subdir = "../tags/%s/" + index
|
||||
link_from_tagdir = "../%s/" + index
|
||||
link_from_top = "./tags/%s/latest.html"
|
||||
link_from_subdir = "../tags/%s/latest.html"
|
||||
link_from_tagdir = "../%s/latest.html"
|
||||
cloud = ""
|
||||
level = 1
|
||||
is_tagline = False
|
||||
@ -381,20 +333,20 @@ if __name__ == "__main__":
|
||||
else:
|
||||
cloud = make_tagcloud(tagcloud, link_from_subdir)
|
||||
demoted = demote_css(html, config["relative_css"], level)
|
||||
filename = "%s/%s" % (subdir, index)
|
||||
filename = "%s/latest.html" % subdir
|
||||
with open(filename, 'w') as f: # landing page for tag
|
||||
pagectrl.written.append(filename)
|
||||
page = pagectrl.singlepage(demoted, cloud, latest, p=".")
|
||||
f.write(page)
|
||||
if paginate:
|
||||
pagectrl.paginate(
|
||||
demote_css(html, config["relative_css"], level),
|
||||
cloud, timeline, is_tagline)
|
||||
return pagectrl.written
|
||||
|
||||
import toml
|
||||
def load_settings(filename = "settings.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())
|
||||
@ -463,7 +415,7 @@ if __name__ == "__main__":
|
||||
print(e)
|
||||
return json_objs
|
||||
|
||||
def render(profiles, template, date_format):
|
||||
def render(profiles, template):
|
||||
rendered = []
|
||||
SHORT_BIO_LIMIT = 150
|
||||
for profile in profiles:
|
||||
@ -484,7 +436,7 @@ if __name__ == "__main__":
|
||||
__post_count__ = post_count,
|
||||
__shortbio__= escape(self_desc),
|
||||
__lastupdated__= strftime(
|
||||
date_format, localtime(epoch_timestamp)) )
|
||||
"%Y %b %d", localtime(epoch_timestamp)) )
|
||||
rendered.append(foo)
|
||||
except KeyError as e:
|
||||
print("remote profile is missing key: ", e, file=sys.stderr)
|
||||
@ -525,9 +477,10 @@ if __name__ == "__main__":
|
||||
try:
|
||||
list_of_json_objs.sort(key=lambda e: e["last-updated"], reverse=True)
|
||||
except KeyError: pass
|
||||
return render(list_of_json_objs, f_cfg["format"], f_cfg["date_format"])
|
||||
return render(list_of_json_objs, f_cfg["format"])
|
||||
|
||||
def main(tpl, content, skip_fetch, new_posts):
|
||||
def main():
|
||||
tpl, content, skip_fetch = get_args()
|
||||
cfg = load_settings()
|
||||
if cfg == None:
|
||||
print("exit: no settings.toml found.", file=sys.stderr)
|
||||
@ -539,31 +492,25 @@ if __name__ == "__main__":
|
||||
print("exit: table 'page' absent in settings.toml", file=sys.stderr)
|
||||
return
|
||||
p = parse_txt(content)
|
||||
tl, tc, tg = get_posts(p, cfg["post"], new_posts)
|
||||
tl, tc, tg = get_posts(p, cfg["post"])
|
||||
if tl == []:
|
||||
return
|
||||
# main timeline
|
||||
updated = []
|
||||
updated += writepage(tpl, tl, tc, cfg["page"],
|
||||
paginate=True if new_posts is None else False)
|
||||
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")
|
||||
tl.reverse()
|
||||
for key in tg.keys():
|
||||
tagline = []
|
||||
for index in tg[key]:
|
||||
tagline.append(tl[index])
|
||||
# [1:] means to omit hashtag from dir name
|
||||
wp = True # will paginate
|
||||
if new_posts is not None \
|
||||
and len(tagline) > cfg["page"]["postsperpage"]:
|
||||
wp = False
|
||||
updated += writepage(
|
||||
tpl, tagline, tc, cfg["page"], \
|
||||
subdir="tags/%s" % key[1:], \
|
||||
paginate=wp)
|
||||
subdir="tags/%s" % key[1:] \
|
||||
)
|
||||
if "webring" in cfg:
|
||||
if cfg["webring"]["enabled"] == True:
|
||||
export_profile(
|
||||
@ -576,11 +523,13 @@ if __name__ == "__main__":
|
||||
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)
|
||||
if "latestpages" in cfg:
|
||||
for page in cfg["latestpages"]:
|
||||
print(page, file=f)
|
||||
try:
|
||||
main(*get_args())
|
||||
main()
|
||||
except KeyError as e:
|
||||
traceback.print_exc()
|
||||
print("\n\tA key may be missing from your settings file.", file=sys.stderr)
|
@ -1,13 +0,0 @@
|
||||
pycurl
|
||||
# ==7.45.3
|
||||
# pycurl==7.45.2
|
||||
|
||||
python_dateutil
|
||||
# ==2.9.0.post0
|
||||
# python_dateutil==2.8.2
|
||||
|
||||
toml
|
||||
# ==0.10.2
|
||||
|
||||
tomlkit
|
||||
# ==0.12.5
|
@ -1,136 +0,0 @@
|
||||
|
||||
import os, argparse
|
||||
from tomlkit import loads
|
||||
from tomlkit import dump
|
||||
|
||||
def nest_dictionary(d, keys, val):
|
||||
for key in keys:
|
||||
d = d.setdefault(key, val)
|
||||
return d
|
||||
|
||||
class MicroblogConfig:
|
||||
def __init__(self, given_config):
|
||||
self.is_outdated = False
|
||||
self.updated = given_config
|
||||
|
||||
def compare(self, sref, suser, keylist=[]):
|
||||
# subtable of ref, subtable of user
|
||||
updated = self.updated
|
||||
# nnavigate to table
|
||||
if keylist != []:
|
||||
for key in keylist:
|
||||
sref = sref[key]
|
||||
for key in keylist:
|
||||
suser = suser[key]
|
||||
for key in keylist:
|
||||
updated = updated[key]
|
||||
for key in sref:
|
||||
if key not in suser:
|
||||
self.is_outdated = True
|
||||
updated[key] =sref[key]
|
||||
print("noticed '", key, "' missing from ", keylist)
|
||||
nest_dictionary(self.updated, keylist, updated)
|
||||
return
|
||||
|
||||
def check(self, r, u): # (reference, user)
|
||||
for key in r:
|
||||
if key == "latestpages": continue;
|
||||
# post and webring have subtables
|
||||
# webring.profile
|
||||
# webring.following
|
||||
# webring.following.internal-avatars
|
||||
# post.gallery
|
||||
# post.buttons
|
||||
try:
|
||||
self.compare(r, u, [key])
|
||||
except KeyError:
|
||||
u[key] = dict()
|
||||
print("missing top-level table '", key, '\'')
|
||||
self.compare(r, u, [key])
|
||||
if key == "webring":
|
||||
self.compare(r, u, ["webring", "profile"])
|
||||
self.compare(r, u, ["webring", "following"])
|
||||
self.compare(r, u, ["webring", "following", "internal-avatars"])
|
||||
if key == "post":
|
||||
self.compare(r, u, ["post", "gallery"])
|
||||
self.compare(r, u, ["post", "buttons"])
|
||||
pass
|
||||
|
||||
def load_files(user_conf_file):
|
||||
script_dir = os.path.dirname(
|
||||
os.path.abspath(__file__))
|
||||
parent_dir = os.path.abspath(
|
||||
os.path.join(script_dir, os.pardir))
|
||||
target_folder = "example"
|
||||
example = os.path.abspath(
|
||||
os.path.join(parent_dir, target_folder))
|
||||
ref_file = "%s/%s" % (example, "/settings.toml")
|
||||
if not os.path.exists(ref_file):
|
||||
return
|
||||
ref_conf = dict()
|
||||
with open(ref_file, 'r') as f:
|
||||
ref_conf = loads(f.read())
|
||||
user_conf = dict()
|
||||
with open(user_conf_file, 'r') as f:
|
||||
user_conf = loads(f.read())
|
||||
return ref_conf, user_conf
|
||||
|
||||
def multi_prompt(message):
|
||||
try:
|
||||
while True:
|
||||
user_input = int(input(f"{message}").lower())
|
||||
if user_input < 3:
|
||||
return user_input
|
||||
else:
|
||||
return 0
|
||||
except KeyboardInterrupt:
|
||||
print()
|
||||
except ValueError:
|
||||
pass
|
||||
return 0
|
||||
|
||||
def get_args():
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--no-prompt", action="store_true", \
|
||||
help="does not ask what to do if missing keys are detected")
|
||||
p.add_argument("-c", "--check", type=str,\
|
||||
help="sets/changes the file to be checked (default: settings.toml)")
|
||||
args = p.parse_args()
|
||||
if args.no_prompt:
|
||||
print("'--no-prompt' set")
|
||||
if args.check:
|
||||
print("--check set", args.check)
|
||||
else:
|
||||
args.check = "settings.toml"
|
||||
return args.no_prompt, args.check
|
||||
|
||||
def main(is_no_prompt, user_conf_file="settings.toml"):
|
||||
print("checking ", user_conf_file)
|
||||
reference, user_edited = load_files(user_conf_file)
|
||||
mcfg = MicroblogConfig(user_edited)
|
||||
mcfg.check(reference, user_edited)
|
||||
if mcfg.is_outdated == False:
|
||||
print("Your settings file is OK!")
|
||||
return
|
||||
message = """
|
||||
Your settings file is outdated.
|
||||
Do you want to...
|
||||
\t 1. save new settings to new file
|
||||
\t 2. update/overwrite existing settings
|
||||
\t *. do nothing
|
||||
"""
|
||||
response = 0 if is_no_prompt else multi_prompt(message)
|
||||
out_file = str()
|
||||
if response == 0:
|
||||
return
|
||||
elif response == 1:
|
||||
out_file = "new.toml"
|
||||
elif response == 2:
|
||||
out_file = user_conf_file
|
||||
with open(out_file, 'w') as f:
|
||||
dump(mcfg.updated, f)
|
||||
print("Wrote updated config to ", out_file)
|
||||
pass
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(*get_args())
|
Loading…
x
Reference in New Issue
Block a user