first commit / upload

This commit is contained in:
pine 2021-04-29 12:48:21 -06:00
commit 989a408325
Signed by: pine
GPG Key ID: 66AAFCFCCFBB559E
4 changed files with 626 additions and 0 deletions

122
LICENSE Normal file
View File

@ -0,0 +1,122 @@
Creative Commons Legal Code
CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
HEREUNDER.
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy, or
the present or absence of errors, whether or not discoverable, all to
the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.

165
README.md Normal file
View File

@ -0,0 +1,165 @@
# Vanity Press
An extensible, programmable, static site generator for Gemini first.
## Features
* Extensible - Users can create their own functions and variables to use on their site
* Templates
* Inline bash commands / python commands
* Metadata, which can be used in templates or user-created functions
* Tags
* Atom feeds
## Quick Start
* clone this repository
* edit `settings.py` with information like your site name, url, etc (this will be used to create atom feeds)
* see `Documentation` for basic usage
* run the script `vanity.py` (do `chmod +x vanity.py` on Unix systems to mark it as executable, then run it with `./vanity.py`. Alternatively, you could also run it with `python vanity.py`.)
## Documentation
### Usage
* you modify `settings.py` with configuration options ("`settings.py` variables)
* you apply basic metadata to desired files for use in functions and templates ("Metadata")
* you can create templates for files ("Creating Templates")
### `settings.py` variables
* `SOURCE_DIR`: the folder where you write your content in
* `DEST_DIR`: the folder where vanity creates its output files (the location of the generated site)
* `TEMPLATE_DIR`: where you store template files
* `TEMPLATE_EXTENSIONS`: an array of file extensions. Vanity will only look at files with these extensions when doing things like templating or creating dynamic content. Files with a different extension will be hard-linked instead.
* `IGNORE_PREFIX`: folders starting with this string and all of their contents will not be added to the site. Files, however, WILL be added but won't show up in functions like {{ atom }}, {{ index }}, or {{ recent }}
* `replace_table`: a dictionary where you can define your own variables and functions to use on your site. Furthermore, some optional values are pulled from here to generate atom feeds.
### Metadata
Metadata is data appended to the *start of files* (metadata listed after page content begins will not be used) which provides information about the page. This info can be used by templates and functions. For example, this could be the date published, title, or tags associated with the page. Metadata names are case-insensitive and are preceded by the `&` character. A `:` delimiter is then used to separate it from the value. Metadata is not shown on the destination files.
For example, metadata for a simple page may look like this:
```
&TITLE:Vanity Press: A new static site generator for Gemini
&PUB_DATE:1970-01-01
&TAGS:Programming, Gemini
Lorem ipsum dolor asset. This is the page content
```
Metadata is used in a few functions:
* `&TITLE` is used by `{{ atom }}` for page titles and `{{ index }}` for link titles (defaults to the file name if not provided).
* `&PUB_DATE` is used by `{{ index }}` for specifying the page's publish date in link titles.
* `&TAGS` is used by `{{ tags }}` to organize content based on tag
Users can also use metadata in templates and define their own metadata. For example, consider the file `poem.gmi`:
```
!TEMPLATE:text
&TITLE:Ozymandias
&AUTHOR:Percy Shelly
&POETRY:True
Lorem ipsum. Poem.
```
and the template `text.template`:
```
# {{ &title }}
## by {{ &author }}
{{ python:if {{ &poetry }}:out = '\n```' }}
!CONTENT
{{ python:if {{ &poetry }}:out = '\n```' }}
```
which automatically wraps poems in pre-formatted text blocks and specifies their title and author.
### Current functions / variables
Functions and variables are written in-line in the source directory files with the syntax `{{ foo }}` or `{{ foo:args }}` (`foo` is the name of a function and `args` are a comma separated list of arguments). The spacing between brackets does not matter, so `{{ foo}}`, `{{foo}}`, and `{{foo }}` all work the same.
Below is a list of currently defined functions / variables that you can use. See "Extending Features" for how to create your own.
CURRENT_DIR: outputs the *source* directory where it's being called from
CURRENT_FILE: outputs the *source* file path where it's being called from
atom: outputs an atom feed for *n* most-recent files in the specified directory
* calling `{{ atom }}` returns an atom feed for every file in the current directory ending with an extension listed in `TEMPLATE_EXTENSIONS`
* `{{ atom:n }}` changes it to the *n* most-recent files
* `{{ atom:n,dir }}` changes the directory to *dir* (absolute path), no matter where the function is called from
date: outputs the current date in the specified format
* calling `{{ date }}` returns the current date in the format "YYYY-mm-dd" (ISO 8601)
* `{{ date:format }}` changes the format to *format*. This is a string like `%Y-%m-%d`, that can be used in Python's `strftime` function
index: outputs a list Gemini links for *n* most-recent files in the specified directory
* calling `{{ index }}` returns a list of Gemini links (formatted => `url` `pub_date` `title`) for every file in the current directory. `pub_date` and `title` will be fetched from the file's metadata if provided. If `title` isn't provided, it'll be replaced with the file name.
* `{{ index:n }}` changes it to the *n* most-recent files
* `{{ index:-n }}` changes it to the *n* oldest files. `{{ index:-0 }}` will list all files without limit, oldest to newest
* `{{ index:n,dir }}` changes the directory to *dir* (absolute path) no matter where the function is called from
recent: outputs a list of Gemini links for *n* most-recent files in the specified directory
* `{{ recent }}` behaves exactly like `{{ index }}` does, with the exceptions that the default, unspecified count is 10 (whereas `{{ index }}` is 0, no limit), and that links are formatted as => `url` `date_modified` `file name`
last_updated: returns the modification date of the specified file in the format "YYYY-mm-dd" (ISO 8601)
* `{{ last_updated }}` returns the modification date of the current file
python: computes the specified python command. Outputs whatever the variable `out` is defined as.
* example: `{{ python:import random; out="Hello" if random.random() < 0.5 else "World" }}` would either return "Hello" or "World" with 50/50 odds.
* You *can* use other functions, variables, and metadata in `{{ python }}`. For example, `{{ python:out="{{ &title }}".upper() }}` would output the title defined in metadata in all upper case.
shell: captures the output of the specified shell command
* IMPORTANT: Shell commands are executed in your shell, that's how they work. They are therefore inherently dangerous; calling something like `{{ shell:rm -rf {{ CURRENT_DIR }} }}` *would* execute and delete all of the files in the current working directory. Use with caution!
* example: `{{ shell:ls | sed 's/^.*/=> & &/' }}` could be used to create a quick-and-dirty index file
tags: outputs a list of tags used (tags found via metadata) in the specified directory. In the destination directory, creates the folder `tag` in the relative path
* example: calling `{{ tags }}` in `./gemlog/index.gmi` would output something like:
```
=> tag/cooking.gmi cooking (3)
=> tag/essays.gmi essays (2)
=> tag/programming.gmi programming (5)
```
and create the tag index files `cooking.gmi`, `essays.gmi`, `programming.gmi` in `DEST_DIR/gemlog/tag/`. These files by default look like:
```
# cooking
=> /gemlog/pasta.gmi pasta.gmi
=> /gemlog/banana_bread.gmi Cooking Banana Bread at Home
=> /gemlog/vegan_chili.txt My Vegan Chili Recipe
```
where the titles are pulled from the metadata if available, and then default to the file name.
* You can modify the output/look of tag index files by creating the `tag.template` file in your `TEMPLATE_DIR`. See the `Creating Templates` section further on for more information.
* Calling `{{ tags }}` and creating the template file is usually enough to handle tags on your gemlog without any further configuration
words: outputs the word count for the current file. Takes no arguments
### Creating Templates
Templates are ways for you to reuse the same content/format on multiple pages without having to manually add it every time.
Templates are files with the extension `.template` stored in `TEMPLATE_DIRS`. Templates can be assigned automatically or manually. For example, the template `foobar.template` will automatically be applied to all files in any directories named `foobar`.
Manually specifying a template must be done in the first line of a file. Writing `!TEMPLATE:foobar` in the first line (followed immediately by a newline) will apply the `foobar.template` template (if it exists). You can also use `!TEMPLATE:none`, meaning that no template will be applied even if one would normally be used automatically.
Templates can use file metadata and defined functions / variables. Templates allow you to append prefixes / suffixes to files.
For example, consider the following template:
```
# {{ &title }}
!CONTENT
Last updated {{ last_updated }}
Published on {{ &pub_date }}
=> / home
```
The title is appended before the article, sourced from the original file's metadata (`&title`). At the end, a footer is applied using metadata (`&pub_date`), a function (`last_updated`), and just regular text (the home link). `!CONTENT` is then replaced by the content of the original file.
#### Tag Templates
The file `tag.template`, if created, modifies how tag index pages look. If not created, this default value is used:
```
# {{ tag }}
!CONTENT
```
where `{{ tag }}` is replaced by the name of tag (i.e, "cooking", or "programming", etc)
### Extending Features
Users can create their own variables and functions to use on their site by altering the dictionary `replace_table` in `settings.py`. An example is provided there to guide you further. Custom functions are written in Python in the `settings.py` file. (although you could use something like the `{{ shell }}` function to execute your own files).
## Contributing
If you have a feature you'd like to request (or even better, to implement) I would be *ecstatic* to hear your input. Either make a pull request or let me know at my email, ponderosapinetree [ at ] protonmail.com. Thanks!
If you're interested in taking a peek (I encourage you to do so), the file `vanity.py` is heavily marked up with comments for both your and my understanding. However, followers of any code style guide will weep at the one-liners and inconsistencies that I've used. There is no 80-character line limit, there is no God here, let ye who pass through these parts be warned.
## License
All files in this repository are liscenced under CC0. Feel free (and please) change files / extend this for your use. If you do change something / add a feature that you think others may use as well, why not make a pull request, too?

33
settings.py Normal file
View File

@ -0,0 +1,33 @@
SOURCE_DIR = "./" #replace with whatever folder you write your content in
DEST_DIR = "/var/gemini/" #where the output folder is
TEMPLATE_DIR = "./templates/" #where you store templates
TEMPLATE_EXTENSIONS = ["gmi", "gemini", "xml", "html"]
#^dynamic modifications (including tempalting) will be applied only to files with these extensions
IGNORE_PREFIX = '_'
#^folders starting with this string and all of their contents will not be added to the site
#files, however, WILL be added but won't show up in variables like {{ index }} or {{ atom }}
'''
from datetime import datetime
def my_custom_function(format="%Y-%m-%d"):
return datetime.now().strftime(format)
'''
#^example of creating a function
replace_table = {
'site_name': "Your Gemlog",
'site_url': 'gemini://your_url.com',
'author': 'You',
'email': 'email@email.com',
#^these values are used to genereate atom feeds these are optional. deleting these rows will just cause
#some info in the atom feed to be missing, but everything should still work
#you can also use these variables on your pages like so:
#lorem ipsum dolor asset
#article written by {{ author }}
#email me at {{ email }}
#'my_custom_function': lambda args: my_custom_function(*args),
#^example of creating a new function
}

306
vanity.py Executable file
View File

@ -0,0 +1,306 @@
#!/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.endsiwth('/'): 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
#START HELPER FUNCTIONS
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("&"):
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:
print("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:
print("Error: Could not parse argument as integer")
return None, None
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:
print("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:
print("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'] + '/' + dir[len(SOURCE_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:
print("python did not return anything because you did not define the variable `out` in your statement")
return ""
except:
print("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:
print("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 + dir[len(SOURCE_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' % (f[len(SOURCE_DIR):], 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 + d): os.mkdir(DEST_DIR + d)
for fname in files:
parent = root[len(SOURCE_DIR):]
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 = len(fname.split(".")) > 1 and fname.split(".")[-1] in TEMPLATE_EXTENSIONS
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 isChangable:
#^Only look at files with a suffix in template_extensions, saves time
if rl[0].startswith("!TEMPLATE"):
#TEMPLATE CASE 1: It's explicitely written out
try:
with open(TEMPLATE_DIR + r[0][10:] + ".template", 'r') as tfile:
template = tfile.read().splitlines()
except:
print("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:]
if not isChangable:
link(fpath, dpath)
else:
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()
if __name__ == "__main__":
main()