first commit / upload
This commit is contained in:
commit
989a408325
|
@ -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.
|
||||
|
|
@ -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?
|
|
@ -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
|
||||
}
|
|
@ -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()
|
Loading…
Reference in New Issue