320 lines
15 KiB
Python
Executable File
320 lines
15 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
import os, re, subprocess
|
|
from glob import glob
|
|
from shutil import copyfile
|
|
from datetime import datetime as dt
|
|
|
|
from settings import *
|
|
|
|
def default(var, default, dir = False):
|
|
#if the user didn't specify something in settings, then just return the default val that I'll specify
|
|
try:
|
|
if type(var) == type(default):
|
|
return var if not dir else parse_directory(var)
|
|
return default
|
|
except NameError: return default
|
|
|
|
def parse_directory(dir):
|
|
if not dir.endswith('/'): dir += '/'
|
|
if not os.path.exists(dir): os.mkdir(dir)
|
|
return dir
|
|
|
|
SOURCE_DIR = default(SOURCE_DIR, "./", True)
|
|
DEST_DIR = default(DEST_DIR, "/var/gemini/", True)
|
|
TEMPLATE_DIR = default(TEMPLATE_DIR, "./templates/", True)
|
|
TEMPLATE_EXTENSIONS = default(TEMPLATE_EXTENSIONS, ["gmi", "xml", "html", "gemini"])
|
|
IGNORE_PREFIX = default(IGNORE_PREFIX, '_')
|
|
replace_table = default(replace_table, {})
|
|
|
|
VAR_REGEX = re.compile("({{ ?(&?[\d\w]+):?(.*[^ ])? ?}})")
|
|
|
|
dynamic_dict = {
|
|
"atom": lambda args: atom(*args),
|
|
"date": lambda args: dt.now().strftime(*args) if args else dt.now().strftime('%Y-%m-%d'),
|
|
"index": lambda args: index(*args),
|
|
"recent": lambda args: recent(*args),
|
|
"last_updated": lambda args: dt.fromtimestamp(os.path.getmtime(args[0])).strftime('%Y-%m-%d'),
|
|
"python": lambda args: python(*args),
|
|
"shell": lambda args: shell(*args),
|
|
"tags": lambda args: tags(*args),
|
|
"words": lambda args: len(args[0].split()) if args else "",
|
|
}
|
|
dynamic_dict.update(replace_table) #add whatever's specified in settings.py
|
|
|
|
messages = {}
|
|
|
|
#START HELPER FUNCTIONS
|
|
def add_message(m):
|
|
if not messages.get(m) is None:
|
|
messages[m] += 1
|
|
else:
|
|
messages[m] = 1
|
|
|
|
def files_to_gem(dir, files, date=None, name=lambda f, dir:f[len(dir):]):
|
|
s = []
|
|
for f in files: #I apoligize for this one-liner, but it works?
|
|
fname = name(f, dir)
|
|
fdate = date(f)
|
|
if fdate: s.append("=> %s %s %s" % (f[len(dir):], fdate, fname))
|
|
else: s.append("=> %s %s" % (f[len(dir):], fname))
|
|
return "\n".join(s)
|
|
|
|
def get_meta(file, target):
|
|
found = False
|
|
with open(file, 'r') as f:
|
|
line = '&'
|
|
while line.startswith("&") or line.startswith("!"):
|
|
line = f.readline()
|
|
if line[:len(target)+1].lower() == '&' + target.lower():
|
|
found = True
|
|
break
|
|
if found:
|
|
return line[len(target)+2:].strip() #+2 because there's a colon and an ampersand
|
|
return None
|
|
|
|
def link(src, dest):
|
|
try:
|
|
os.link(src, dest)
|
|
except FileExistsError:
|
|
if not os.path.islink(dest):
|
|
os.remove(dest)
|
|
link(src, dest)
|
|
except OSError:
|
|
add_message("Can't create hard-link, destination folder is on a different partition. Copying instead.")
|
|
copyfile(src, dest)
|
|
perms_s = oct(os.stat(src).st_mode)[-3:]
|
|
perms_d = oct(os.stat(dest).st_mode)[-3:]
|
|
if perms_s != perms_d: #the destination file should have the same perms as the source
|
|
os.chmod(dest, int(perms_s, 8))
|
|
|
|
def parse_str_int(s):
|
|
if not s: return None, None
|
|
if isinstance(s, int): return s, s<0
|
|
negative = s[0] == '-' #I want users to be able to write "-0" and use that as a value
|
|
try:
|
|
i = int(s[1:]) if negative else int(s)
|
|
return i, negative
|
|
except:
|
|
add_message("Error: Could not parse argument as integer")
|
|
return None, None
|
|
|
|
rel_path = lambda path: path[len(SOURCE_DIR):]
|
|
|
|
def replace_dynamic(body, fpath, d_list=None, dynamic_dict=dynamic_dict, VAR_REGEX=VAR_REGEX):
|
|
if d_list == None:
|
|
#^don't recompile regex if we don't have to
|
|
d_list = VAR_REGEX.findall(body)
|
|
d_list = [i for i in d_list if i[1] != 'words'] + [i for i in d_list if i[1] == 'words']
|
|
#^sort so that {{ words }} gets calculated last, that way other tags' additions get added to the word count
|
|
for d in d_list:
|
|
replace, key, args = d
|
|
args = args.split(",")
|
|
if args == ['']: args = []
|
|
for i in range(len(args)):
|
|
args[i] = dynamic_dict.get(args[i]) if not dynamic_dict.get(args[i]) is None else args[i]
|
|
#^allows us to use variables as an argument
|
|
|
|
if key in ["index", "recent", "atom", "tags"]:
|
|
if not args and key != "tags":args.append('')
|
|
if len(args) == 1 or not args and key == "tags":args.append(dynamic_dict["CURRENT_DIR"])
|
|
#^if index, or recent don't specify a dir, assume it's the current one
|
|
elif key == 'last_updated' and not args or key in key in ['shell', 'python'] and len(args) == 1:
|
|
args.append(dynamic_dict["CURRENT_FILE"])
|
|
#^similarly, assume these functions reference the current file if not specified
|
|
elif key == 'words' and not args:
|
|
args.append(body)
|
|
|
|
if key.startswith('&'):
|
|
replace_val = get_meta(fpath, key[1:])
|
|
elif isinstance(dynamic_dict.get(key), type(lambda: 1)):
|
|
try:
|
|
if not args: replace_val = dynamic_dict[key]([])
|
|
else: replace_val = dynamic_dict[key](args)
|
|
except TypeError:
|
|
add_message("Too many args supplied to key %s: %s" % (key, args))
|
|
replace_val = dynamic_dict[key]([])
|
|
#^If the function takes arguments, pass arguments
|
|
else:
|
|
replace_val = dynamic_dict.get(key)
|
|
if replace_val is None:
|
|
add_message("Variable used but not defined in dynamic_dict or metadata: %s" % key)
|
|
replace_val = ""
|
|
body = body.replace(replace, str(replace_val))
|
|
if body[0:10] == "!TEMPLATE":
|
|
body = "\n".join(body.splitlines()[1:])
|
|
return body
|
|
#END HELPER FUNCTIONS
|
|
|
|
#START FUNCTIONS FOR DYNAMIC CONTENT
|
|
def atom(count=0, dir=SOURCE_DIR, d=dynamic_dict):
|
|
if not d.get('site_url'): return "Must specify `site_url` in settings.py for atom feeds to work"
|
|
|
|
fname = lambda fpath: get_meta(f, "TITLE") if get_meta(fpath, "TITLE") else fpath[len(dir):]
|
|
url = lambda fpath: d['site_url'] + '/' + rel_path(dir) + os.path.basename(fpath)
|
|
date = lambda fpath: dt.fromtimestamp(os.path.getmtime(fpath)).strftime('%Y-%m-%dT%H:%M:%S+00:00')
|
|
|
|
files = [i for i in glob(dir + '/*') if os.path.isfile(i) and len(i.split('.')) > 1 and i.split('.')[-1] in TEMPLATE_EXTENSIONS and not os.path.basename(i).startswith(IGNORE_PREFIX)]
|
|
[files.remove(f) for f in files if os.path.basename(f) in ['index.gmi', 'atom.xml']]
|
|
count, _ = parse_str_int(count)
|
|
if count is None or 0: count = len(files) #parse the input
|
|
files.sort(key=lambda x: -os.path.getmtime(x)) #sort new to old (recent)
|
|
files = files[:count] #make it only the most count recent if specified
|
|
|
|
feed_s = "<?xml version='1.0' encoding='UTF-8'?>\n<feed xmlns='http://www.w3.org/2005/Atom'>" #some basic header stuff that I fill in the details of
|
|
feed_s += '\n <id>%s</id>\n <title>%s</title>\n <updated>%s</updated>' % (url(''), d.get('site_name'), date(files[0]))
|
|
feed_s += '\n <link href="%s" rel="self"/>' % url('atom.xml')
|
|
feed_s += '\n <link href="%s" rel="alternate"/>' % url('')
|
|
if d.get('author') or d.get('email'): #add author information if provided
|
|
feed_s += "\n <author>"
|
|
if d.get('author'): feed_s += "\n <name>%s</name>" % d.get('author')
|
|
if d.get('email'): feed_s += "\n <email>%s</email>" % d.get('email')
|
|
feed_s += "\n <author>"
|
|
for f in files:
|
|
feed_s += '\n <entry>\n <id>%s</id>\n <title>%s</title>\n <updated>%s</updated>\n <link href="%s" rel="alternate"/>\n </entry>' % (url(f), fname(f), date(f), url(f))
|
|
#^I should apologize for the one liner, but It takes up less space for me
|
|
feed_s += "\n</feed>"
|
|
return feed_s
|
|
|
|
def index(count=0, dir=SOURCE_DIR):
|
|
files = [i for i in glob(dir + '/*') if not i[len(dir):].startswith('_')]
|
|
[files.remove(f) for f in files if os.path.basename(f) in ['index.gmi', 'atom.xml']]
|
|
count, negative = parse_str_int(count)
|
|
if count is None or negative is None: count = 0; negative = False
|
|
|
|
if negative: files.sort(key=lambda x: os.path.getmtime(x))
|
|
else: files.sort(key=lambda x: -os.path.getmtime(x))
|
|
if count != 0: files = files[:count]
|
|
fdate = lambda f: get_meta(f, "PUB_DATE") if os.path.isfile(f) else None
|
|
fname = lambda f, dir: get_meta(f, "TITLE") if os.path.isfile(f) and get_meta(f, "TITLE") else f[len(dir):]
|
|
return files_to_gem(dir, files, date=fdate, name=fname)
|
|
|
|
def recent(count=10, dir=SOURCE_DIR):
|
|
files = [i for i in glob(dir + '/**/*', recursive=True) if os.path.isfile(i) and not os.path.basename(os.path.abspath(os.path.join(i, os.pardir))).startswith(IGNORE_PREFIX) and not os.path.basename(i).startswith(IGNORE_PREFIX)]
|
|
count, negative = parse_str_int(count) #parse the input
|
|
if count is None or negative is None: count = 10; negative = False
|
|
|
|
if negative: files.sort(key=lambda x: os.path.getmtime(x)) #the "reverse" of recent is sort latest to present
|
|
else: files.sort(key=lambda x: -os.path.getmtime(x)) #sort new to old (recent)
|
|
if count != 0: files = files[:count] #limit list if specified
|
|
fdate = lambda f: dt.fromtimestamp(os.path.getmtime(f)).strftime("%Y-%m-%d")
|
|
return files_to_gem(dir, files, date=fdate)
|
|
|
|
def python(command, fpath):
|
|
try:
|
|
namespace = {}
|
|
command = replace_dynamic(command, fpath)
|
|
exec(command, namespace)
|
|
return namespace["out"]
|
|
except KeyError:
|
|
add_message("python did not return anything because you did not define the variable `out` in your statement")
|
|
return ""
|
|
except:
|
|
add_message("python encountered a problem")
|
|
return ""
|
|
|
|
def shell(command, fpath, encoding="UTF-8"):
|
|
try:
|
|
command = replace_dynamic(command, fpath)
|
|
parent_dir = "/".join(fpath.split('/')[:-1]) + '/'
|
|
return subprocess.check_output(command, shell=True, cwd=parent_dir).decode(encoding).strip()
|
|
except:
|
|
add_message("shell encountered a problem")
|
|
return ""
|
|
|
|
def tags(dir=SOURCE_DIR):
|
|
files = [i for i in glob(dir + '/*') if os.path.isfile(i) and len(i.split('.')) > 1 and i.split('.')[-1] in TEMPLATE_EXTENSIONS and not i.startswith(IGNORE_PREFIX)]
|
|
tags = {} #dictionary of tags, and a list of all of the files with those tags
|
|
for f in files:
|
|
f_tags = get_meta(f, 'TAGS')
|
|
if f_tags:
|
|
f_tags = f_tags.replace(", ", ",").split(',')
|
|
for tag in f_tags:
|
|
tags[tag.lower()] = [f] if tags.get(tag.lower()) is None else tags[tag.lower()] + [f]
|
|
if not tags: return "No tags created yet"
|
|
|
|
tag_dir = DEST_DIR + rel_path(dir) + 'tag'
|
|
tag_template_exists = os.path.isfile(TEMPLATE_DIR + "tag.template") #defined here so the function isn't called multiple times in the for loop
|
|
if not os.path.isdir(tag_dir): os.mkdir(tag_dir)
|
|
for tag, files in tags.items():
|
|
fname = lambda f, dir: get_meta(f, "TITLE") if os.path.isfile(f) and get_meta(f, "TITLE") else f[len(dir):]
|
|
file_s = '\n'.join(['=> /%s %s' % (rel_path(f), fname(f, dir)) for f in files])
|
|
with open("%s/%s.gmi" % (tag_dir, tag), 'w') as f:
|
|
if tag_template_exists:
|
|
lines = [file_s]
|
|
with open(TEMPLATE_DIR + "tag.template", 'r') as tfile: #users can template their tag pages
|
|
template = tfile.read().splitlines()
|
|
lines = template[:template.index("!CONTENT")] + [file_s] + template[template.index("!CONTENT")+1:]
|
|
dynamic_dict['tag'] = tag #for the purposes of templating, add this variable to the dynamic_dict
|
|
f.write(replace_dynamic("\n".join(lines), "%s/%s.gmi" %(tag_dir, tag)))
|
|
else:
|
|
f.write("# %s\n\n%s" % (tag, file_s))
|
|
dynamic_dict.pop('tag', None) #a little bit of cleanup
|
|
return "\n".join(["=> tag/%s.gmi %s (%s)" % (tag, tag, len(files)) for tag, files in tags.items()])
|
|
#END FUNCTIONS FOR DYNAMIC CONTENT
|
|
|
|
def make(SOURCE_DIR=SOURCE_DIR,
|
|
dir = '',
|
|
DEST_DIR=DEST_DIR,
|
|
TEMPLATE_DIR=TEMPLATE_DIR,
|
|
TEMPLATE_EXTENSIONS=TEMPLATE_EXTENSIONS,
|
|
IGNORE_PREFIX=IGNORE_PREFIX,
|
|
VAR_REGEX=VAR_REGEX,
|
|
dynamic_dict=dynamic_dict):
|
|
if not os.path.isdir(DEST_DIR): os.mkdir(DEST_DIR)
|
|
for root, dirs, files in os.walk(SOURCE_DIR + dir):
|
|
dynamic_dict["CURRENT_DIR"] = root if root.endswith("/") else root + '/' #I use this in the source files
|
|
[dirs.remove(d) for d in list(dirs) if d.startswith('_')]
|
|
for d in dirs: #create the directories if they haven't been created
|
|
if not os.path.isdir(DEST_DIR + os.path.join(rel_path(root), d)):
|
|
os.mkdir(DEST_DIR + os.path.join(rel_path(root), d))
|
|
for fname in files:
|
|
parent = rel_path(root)
|
|
fpath = os.path.join(root, fname)
|
|
dynamic_dict["CURRENT_FILE"] = fpath #Use this in the source files, too
|
|
dpath = "%s%s" % (DEST_DIR, os.path.join(parent, fname))
|
|
template = None
|
|
isChangable = os.path.splitext(fname)[1][1:] in TEMPLATE_EXTENSIONS
|
|
if not isChangable:
|
|
link(fpath, dpath)
|
|
else:
|
|
#^Only look at files with a suffix in template_extensions, saves time + sometimes you can't read weird files like executables
|
|
with open(fpath, 'r') as f:
|
|
rl = f.read().splitlines()
|
|
|
|
#Get the template
|
|
if rl and rl[0].strip() == "!TEMPLATE:none": #don't do a template if it says !TEMPLATE:none
|
|
rl.pop(0)
|
|
elif rl[0].startswith("!TEMPLATE"):
|
|
#TEMPLATE CASE 1: It's explicitely written out
|
|
try:
|
|
with open(TEMPLATE_DIR + rl[0][10:] + ".template", 'r') as tfile:
|
|
template = tfile.read().splitlines()
|
|
except:
|
|
add_message("Template '%s' specified but does not exist" % r[0][10:])
|
|
elif os.path.isfile(TEMPLATE_DIR + parent + ".template"):
|
|
#TEMPALTE CASE 2: It's automatically applied to the dynamic_dict
|
|
with open(TEMPLATE_DIR + parent + ".template", 'r') as tfile:
|
|
template = tfile.read().splitlines()
|
|
if not template is None:
|
|
rl = template[:template.index("!CONTENT")] + rl + template[template.index("!CONTENT")+1:]
|
|
|
|
body = "\n".join([i for i in rl if not i.startswith("&")])
|
|
d_list = VAR_REGEX.findall(body)
|
|
if body == "\n".join(rl) and not d_list: #the files aren't dynamic, link them
|
|
link(fpath, dpath)
|
|
else:
|
|
#replace the static variables with the dynamic content
|
|
body = replace_dynamic(body, fpath, d_list)
|
|
with open(dpath, "w") as dfile:
|
|
dfile.write(body)
|
|
|
|
def main():
|
|
make()
|
|
for message, count in messages.items():
|
|
if count > 1: print("%s (x%s)" % (message, count))
|
|
else: print(message)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|