Merge branch 'master' into sebb/build_using_tar_dir
diff --git a/Dockerfile b/Dockerfile
index 3c180c1..23f5e67 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -47,13 +47,9 @@
# Copy the current ASF code
WORKDIR /tmp/pelican-asf
# copy only the GFM build code initially, to reduce rebuilds
-COPY bin bin
+COPY bin/build-cmark.sh bin/build-cmark.sh
# build gfm
RUN ./bin/build-cmark.sh | grep LIBCMARKDIR > LIBCMARKDIR.sh
-# we also need the plugins
-COPY plugins plugins
-# we may need to explain how to create a pelicanconf.yaml
-COPY pelicanconf.md pelicanconf.md
# Standard Pelican stuff
# rebase the image to save up to 230MB of image size
@@ -68,23 +64,29 @@
# we likely do not need the following
# RUN apt install wget unzip fontconfig -y
-ARG PELICAN_VERSION=4.6.0
-ARG MATPLOTLIB_VERSION=3.4.1
+ARG PELICAN_VERSION=4.7.0
RUN pip install pelican==${PELICAN_VERSION}
-RUN pip install matplotlib==${MATPLOTLIB_VERSION}
# Copy the built cmark and ASF
WORKDIR /tmp/pelican-asf
COPY --from=pelican-asf /tmp/pelican-asf .
COPY requirements.txt .
+# Don't automatically load dependencies; please add them to requirements.txt instead
RUN pip install -r requirements.txt --no-deps
+# Now add the local code; do this last to avoid unnecessary rebuilds
+COPY bin bin
+COPY plugins plugins
+
# If the site needs authtokens to build, copy them into the file .authtokens
# and it will be picked up at build time
# N.B. make sure the .authtokens file is not committed to the repo!
RUN ln -s /site/.authtokens /root/.authtokens
+# buildsite.py expects python to be here:
+RUN ln -s /usr/local/bin/python3 /usr/bin/python3
+
# Run Pelican
WORKDIR /site
diff --git a/README.md b/README.md
index 2ad2782..db60a47 100644
--- a/README.md
+++ b/README.md
@@ -1,35 +1,14 @@
-# Tools for using Pelican at the ASF
+Title: README
-_TBD_
+## Tools for using Pelican at the ASF
-## Step One: Build libcmark-gfm
+The infrastructure-pelican repository provides a customized process
+for working with Pelican-based websites at the ASF.
-```
-$ mkdir /tmp/cm
-$ cd /tmp/cm
-$ /path/to/infrastructure-pelican/bin/build-cmark.sh
-... (build output here)
-export LIBCMARKDIR='/tmp/cm/cmark-gfm-0.28.3.gfm.12/lib'
-$
-```
+See the <a href="https://infra.apache.org/asf-pelican-gettingstarted.html" target="_blank">getting started guide</a> for working with the ASF Pelican template.
-Copy/paste/execute that printed `export` line for use in the following steps.
+## Running Local Preview Builds
-(of course, you may use any location of your choice; `/tmp/cm` is
-merely an example)
+See the instructions for <a href="https://infra.apache.org/asf-pelican-local.html" target="_blank">local Pelican builds</a>.
-### Installing libcmark-gfm via packages
-
-_TBD: install a .deb from packages.apache.org_
-
-_TBD: maybe a macOS variant?_
-
-_TBD: maybe Windows?_
-
-## Step Two
-
-_TBD: credentials need to provided in (eg.) bb2.txt_
-
-```
-$ ./kick_build.py --repo=www-site --notify=somewhere@example.com --theme theme/apache --min-pages=200
-```
+Contact `users@infra.apache.org` for any questions or comments.
diff --git a/bin/build-cmark.sh b/bin/build-cmark.sh
index 7917bba..68be098 100755
--- a/bin/build-cmark.sh
+++ b/bin/build-cmark.sh
@@ -30,7 +30,7 @@
# this is checked at the start of the build
OUTPUTDIR=${3:-.}
-ARCHIVES="https://github.com/github/cmark/archive"
+ARCHIVES="https://github.com/github/cmark-gfm/archive/refs/tags"
LOCAL="${TARDIR}/cmark-gfm.$VERSION.orig.tar.gz"
# Follow redirects, and place the result into known name $LOCAL
diff --git a/bin/buildsite.py b/bin/buildsite.py
index b6c1063..b83de51 100755
--- a/bin/buildsite.py
+++ b/bin/buildsite.py
@@ -1,4 +1,12 @@
#!/usr/bin/env python3
+#
+# To run this in dev/test, then LIBCMARKDIR must be defined in the
+# environment.
+#
+# $ export LIBCMARKDIR=/path/to/cmark-gfm.0.28.3.gfm.12/lib
+#
+# ### see build-cmark.sh for building the lib
+#
import sys
@@ -25,16 +33,15 @@
# Command definitions - put into a conf later on?
GIT = '/usr/bin/git'
SVN = '/usr/bin/svn'
+BASH = '/bin/bash'
PELICANFILES = '/home/buildslave/slave/tools'
SCRATCH_DIR = '/tmp'
PLUGINS = '/opt/infrastructure-pelican/plugins'
-
VERSION = '0.28.3.gfm.12'
LIBCMARKDIR = f'/usr/local/asfpackages/cmark-gfm/cmark-gfm-{VERSION}/lib'
if not os.path.exists(LIBCMARKDIR):
# Fail, if a path to the CMARK library is not in ENVIRON.
LIBCMARKDIR = os.environ['LIBCMARKDIR']
-
THIS_DIR = os.path.abspath(os.path.dirname(__file__))
IS_PRODUCTION = os.path.exists(PELICANFILES)
@@ -43,14 +50,20 @@
AUTO_SETTINGS_YAML = 'pelicanconf.yaml'
AUTO_SETTINGS_TEMPLATE = 'pelican.auto.ezt'
AUTO_SETTINGS = 'pelican.auto.py'
-AUTO_SETTINGS_HELP = 'pelicanconf.md'
+AUTO_SETTINGS_HELP = 'https://github.com/apache/infrastructure-pelican/blob/master/pelicanconf.md'
+
+# default config file name
+PELICAN_CONF = 'pelicanconf.py'
+class _helper:
+ def __init__(self, **kw):
+ vars(self).update(kw)
def start_build(args):
""" The actual build steps """
path = os.path.join(SCRATCH_DIR, args.project)
-
+
# Set up virtual environment
print("Setting up virtual python environment in %s" % path)
venv.create(path, clear=True, symlinks=True, with_pip=False)
@@ -61,6 +74,16 @@
subprocess.run((GIT, 'clone', '--branch', args.sourcebranch, '--depth=1', '--no-single-branch', args.source, sourcepath),
check=True)
+ # Check for minimum page count setting in .asf.yaml, which overrides if cli arg is 0 - INFRA-24226.
+ minimum_page_count = args.count
+ asfyaml_path = os.path.join(sourcepath, '.asf.yaml')
+ if os.path.isfile(asfyaml_path):
+ asfyaml = yaml.safe_load(open(asfyaml_path))
+ pelican_asfyaml_section = asfyaml.get("pelican", {})
+ if pelican_asfyaml_section and minimum_page_count <= 0:
+ minimum_page_count = pelican_asfyaml_section.get("minimum_page_count", minimum_page_count)
+
+
# Activate venv and install pips if needed. For dev/test, we will
# assume that all requirements are available at the system level,
# rather than needing to install them into the venv.
@@ -69,7 +92,7 @@
### production buildbot is not difficult to correct.
if IS_PRODUCTION and os.path.exists(os.path.join(sourcepath, 'requirements.txt')):
print("Installing pips")
- subprocess.run(('/bin/bash', '-c',
+ subprocess.run((BASH, '-c',
'source bin/activate; pip3 install -r source/requirements.txt'),
cwd=path, check=True)
else:
@@ -82,6 +105,24 @@
tool_dir = THIS_DIR
print("TOOLS:", tool_dir)
+ ### content_dir isn't quite right either. generate_settings() needs a
+ ### better definition of its sourcepath. And we need a proper definition
+ ### of content_dir to pass to PELICAN.
+ ### gonna brute force for now, to validate some thinking, then refine.
+
+ ### content_dir is where the PAGES are located
+ ### settings_dir is the root of themes and plugins
+
+ # Where is the content located?
+ ### for now, just look for some possibilities. This should come from
+ ### the .yaml or something.
+ content_dir = os.path.join(sourcepath, 'content')
+ settings_dir = sourcepath
+ if not os.path.exists(content_dir):
+ content_dir = os.path.join(sourcepath, 'site')
+ assert os.path.exists(content_dir)
+ settings_dir = content_dir
+
pelconf_yaml = os.path.join(sourcepath, AUTO_SETTINGS_YAML)
if os.path.exists(pelconf_yaml):
settings_path = os.path.join(path, AUTO_SETTINGS)
@@ -89,10 +130,10 @@
builtin_plugins = PLUGINS
else:
builtin_plugins = os.path.join(tool_dir, os.pardir, 'plugins')
- generate_settings(pelconf_yaml, settings_path, [ builtin_plugins ], sourcepath)
+ generate_settings(pelconf_yaml, settings_path, [ builtin_plugins ], settings_dir)
else:
# The default name, but we'll pass it explicitly.
- settings_path = os.path.join(sourcepath, 'pelicanconf.py')
+ settings_path = os.path.join(sourcepath, PELICAN_CONF)
# Set currently supported plugins
### this needs to be removed, as it is too indeterminate.
@@ -107,10 +148,10 @@
# Call pelican
buildpath = os.path.join(path, 'build/output')
os.makedirs(buildpath, exist_ok = True)
- buildcmd = ('/bin/bash', '-c',
+ buildcmd = (BASH, '-c',
'source bin/activate; cd source && '
### note: adding --debug can be handy
- f'(pelican content --settings {settings_path} -o {buildpath})',
+ f'(pelican {content_dir} --settings {settings_path} -o {buildpath})',
)
print("Building web site with:", buildcmd)
env = os.environ.copy()
@@ -119,8 +160,8 @@
count = len(glob.glob(f'{buildpath}/**/*.html', recursive=True))
print(f"{count} html files.")
- if args.count > 0 and args.count > count:
- print("Not enough html pages in the Web Site. Minimum %s > %s found in the Web Site." % (args.count, count))
+ if minimum_page_count > 0 and minimum_page_count > count:
+ print("Not enough html pages in the Web Site. Minimum %s > %s found in the Web Site." % (minimum_page_count, count))
sys.exit(4)
# Done for now
@@ -189,41 +230,48 @@
def build_dir(args):
- path = sourcepath = '.'
+ # Where to place the automatically-generated AUTO_SETTINGS file (pelican.auto.py)
+ auto_dir = '.'
+
+ # Where is the YAML file?
+ yaml_dir = args.yaml_dir
+
+ # Where is the content located?
+ content_dir = args.content_dir
# Where are our tools?
tool_dir = THIS_DIR
print("TOOLS:", tool_dir)
- pelconf_yaml = os.path.join(sourcepath, AUTO_SETTINGS_YAML)
+ pelconf_yaml = os.path.join(yaml_dir, AUTO_SETTINGS_YAML)
if os.path.exists(pelconf_yaml):
- settings_path = os.path.join(path, AUTO_SETTINGS)
+ settings_path = os.path.join(auto_dir, AUTO_SETTINGS)
builtin_plugins = os.path.join(tool_dir, os.pardir, 'plugins')
- generate_settings(pelconf_yaml, settings_path, [ builtin_plugins ], sourcepath)
+ generate_settings(pelconf_yaml, settings_path, [ builtin_plugins ])
+ elif os.path.exists(os.path.join(yaml_dir, PELICAN_CONF)):
+ settings_path = os.path.join(yaml_dir, PELICAN_CONF)
else:
- # The default name, but we'll pass it explicitly.
- settings_path = os.path.join(sourcepath, 'pelicanconf.py')
- print(f'You must convert {settings_path} to {pelconf_yaml}')
- help_path = os.path.join(tool_dir, os.pardir, AUTO_SETTINGS_HELP)
- with open(help_path, encoding='utf-8') as f:
- print(f.read())
+ print(f'ERROR: {pelconf_yaml} is missing')
+ print(f' see: {AUTO_SETTINGS_HELP}')
sys.exit(4)
+
if args.listen:
pel_options = '-r -l -b 0.0.0.0'
else:
pel_options = ''
# Call pelican
- buildcmd = ('/bin/bash', '-c',
+ buildcmd = (BASH, '-c',
### note: adding --debug can be handy
- f'(pelican content --settings {settings_path} --o {args.output} {pel_options})',
+ f'(pelican {content_dir} --settings {settings_path} --o {args.output} {pel_options})',
)
print("Building web site with:", buildcmd)
env = os.environ.copy()
env['LIBCMARKDIR'] = LIBCMARKDIR
try:
- subprocess.run(buildcmd, cwd=path, check=True, env=env)
+ ### is the cwd_necessary?
+ subprocess.run(buildcmd, cwd=auto_dir, check=True, env=env)
except KeyboardInterrupt:
pass
@@ -237,30 +285,58 @@
'theme': os.path.join(sourcepath, ydata.get('theme', 'theme/apache')),
'debug': str(ydata.get('debug', False)),
})
+
+ content = ydata.get('content', { })
+ tdata['pages'] = content.get('pages')
+ tdata['static'] = content.get('static_dirs', [ '.', ])
+
tdata['p_paths'] = builtin_p_paths
tdata['use'] = ['gfm']
+
+ tdata['uses_sitemap'] = None
if 'plugins' in ydata:
if 'paths' in ydata['plugins']:
for p in ydata['plugins']['paths']:
tdata['p_paths'].append(os.path.join(sourcepath, p))
+
if 'use' in ydata['plugins']:
tdata['use'] = ydata['plugins']['use']
- if 'genid' in ydata:
- class GenIdParams:
- def setbool(self, name):
- setattr(self, name, str(ydata['genid'].get(name, False)))
- def setdepth(self, name):
- setattr(self, name, ydata['genid'].get(name))
+ if 'sitemap' in ydata['plugins']:
+ sm = ydata['plugins']['sitemap']
+ sitemap_params =_helper(
+ exclude=str(sm['exclude']),
+ format=sm['format'],
+ priorities=_helper(
+ articles=sm['priorities']['articles'],
+ indexes=sm['priorities']['indexes'],
+ pages=sm['priorities']['pages'],
+ ),
+ changefreqs=_helper(
+ articles=sm['changefreqs']['articles'],
+ indexes=sm['changefreqs']['indexes'],
+ pages=sm['changefreqs']['pages'],
+ ),
+ )
- genid = GenIdParams()
- genid.setbool('unsafe')
- genid.setbool('metadata')
- genid.setbool('elements')
- genid.setbool('permalinks')
- genid.setbool('tables')
- genid.setdepth('headings_depth')
- genid.setdepth('toc_depth')
+ tdata['uses_sitemap'] = 'yes' # ezt.boolean
+ tdata['sitemap'] = sitemap_params
+ tdata['use'].append('sitemap') # add the plugin
+
+ tdata['uses_index'] = None
+ if 'index' in tdata:
+ tdata['uses_index'] = 'yes' # ezt.boolean
+
+ if 'genid' in ydata:
+ genid = _helper(
+ unsafe=str(ydata['genid'].get('unsafe', False)),
+ metadata=str(ydata['genid'].get('metadata', False)),
+ elements=str(ydata['genid'].get('elements', False)),
+ permalinks=str(ydata['genid'].get('permalinks', False)),
+ tables=str(ydata['genid'].get('tables', False)),
+ headings_depth=ydata['genid'].get('headings_depth'),
+ toc_depth=ydata['genid'].get('toc_depth'),
+ )
tdata['uses_genid'] = 'yes' # ezt.boolean()
tdata['genid'] = genid
@@ -275,7 +351,7 @@
tdata['uses_copy'] = None
if 'setup' in ydata:
sdata = ydata['setup']
-
+
# Load data structures into the pelican METADATA.
if 'data' in sdata:
tdata['uses_data'] = 'yes' # ezt.boolean()
@@ -335,7 +411,7 @@
print("ERROR: Could not acquire lock for project directory - is another build taking ages to complete?!")
sys.exit(-1)
-
+
def main():
#os.chdir('/tmp/nowhere') ### DEBUG: make sure we aren't reliant on cwd
@@ -358,6 +434,8 @@
parser_dir = subparsers.add_parser("dir", help = "Build source in current directory and optionally serve the result")
parser_dir.add_argument("--output", help = "Pelican output path (default: %(default)s)", default = "site-generated")
parser_dir.add_argument("--listen", help = "Pelican build in server mode (default: %(default)s)", action = "store_true")
+ parser_dir.add_argument('--yaml-dir', help='Where pelicanconf.yaml is located (default: %(default)s)', default='.')
+ parser_dir.add_argument('--content-dir', help='Where is the content located (default: %{default)s)', default='content')
parser_dir.set_defaults(func=build_dir)
args = parser.parse_args()
diff --git a/bin/kick_build.py b/bin/kick_build.py
index 1f73b70..fcebc7a 100755
--- a/bin/kick_build.py
+++ b/bin/kick_build.py
@@ -1,5 +1,8 @@
#!/usr/bin/env python3
+# Trigger a buildbot run
+# Defaults to the scheduler 'pelican_websites'
+
import argparse
import re
@@ -22,15 +25,15 @@
# The schedule/host we need to kick for a rebuild.
### maybe parameterize?
-SCHEDULER_NAME = 'pelican_websites'
+SCHEDULER_NAME_DEFAULT = 'pelican_websites'
API_HOST = 'ci2.apache.org'
-def main(repo, sourcebranch, outputbranch, theme, notify, min_pages):
+def main(repo, sourcebranch, outputbranch, theme, notify, min_pages, scheduler_name):
# Never build from asf-site.
assert sourcebranch != 'asf-site'
-
+
# Infer project name from the repository name.
### this code and WSMAP should be centralized.
m = re.match(r"(?:incubator-)?([^-.]+)", repo)
@@ -59,7 +62,7 @@
},
}
print('Triggering pelican build...')
- s.post(f'https://{API_HOST}/api/v2/forceschedulers/{SCHEDULER_NAME}', json=payload)
+ s.post(f'https://{API_HOST}/api/v2/forceschedulers/{scheduler_name}', json=payload)
if __name__ == '__main__':
@@ -77,8 +80,10 @@
help='Where to email the build result message.')
parser.add_argument('--min-pages', type=int, default=0,
help='Minimum number of generated pages.')
+ parser.add_argument('--scheduler-name', default=SCHEDULER_NAME_DEFAULT,
+ help='Name of scheduler to trigger')
args = parser.parse_args()
print('ARGS:', args)
main(args.repo, args.sourcebranch, args.outputbranch, args.theme,
- args.notify, args.min_pages)
+ args.notify, args.min_pages, args.scheduler_name)
diff --git a/bin/local-pelican-site.sh b/bin/local-pelican-site.sh
new file mode 100755
index 0000000..a47a1d2
--- /dev/null
+++ b/bin/local-pelican-site.sh
@@ -0,0 +1,143 @@
+#!/bin/bash
+
+# Create a local Pelican build of an infrastructure-pelican-based site
+# and deploy it at http://localhost:8000
+#
+# requires pip3/python3, cmake, and a C compiler
+# known to work on linux/osx. probably works under WSL.
+# will not work under basic Windows.
+
+# github prefix for cloning/updating repos
+GH="https://github.com/apache"
+
+# site_build directory path. use a /tmp dir by default
+SB="$HOME/pelican-local"
+
+# infrastructure-pelican repo
+IP="infrastructure-pelican"
+
+# build target site repo minus the .git suffix
+REPO=`basename $1 .git`
+
+echo "Using GitHub prefix: $GH"
+if [ "$1" = "" ] || [ $# -gt 1 ];
+then
+ echo "Usage: $0 site-repo"
+ echo "Example: $0 infrastructure-website"
+ exit -1
+fi
+
+echo "Starting build for $REPO"
+
+# make sure our tools exist
+echo "Checking dependencies..."
+if ! command -v cmake &> /dev/null
+then
+ echo "cmake not found! you need to install the cmake package"
+ exit -1
+elif ! command -v python3 &> /dev/null
+then
+ echo "python3 not found! you need to install the python3 package"
+ exit -1
+elif ! command -v pip3 &> /dev/null
+then
+ echo "pip3 not found! you need to insatll the pip3 package"
+ exit -1
+elif ! command -v pipenv &> /dev/null
+then
+ echo "pipenv not found! installing it for you..."
+ pip3 install pipenv > /dev/null 2>&1
+ if [ $? -eq 1 ];
+ then
+ echo "pipenv installation failed!"
+ exit -1
+ fi
+fi
+
+# create our build dir to hold our repos and cmark-gfm
+if [ ! -d $SB ];
+then
+ mkdir $SB || 'echo "Creation of $SB failed!" && exit -1'
+ cd $SB
+else
+ cd $SB
+fi
+
+
+# clone or update the pelican and site repos as needed
+echo "Cloning repos..."
+
+if [ -d $IP ];
+then
+ echo "$IP exists - updating..."
+ cd $IP && git pull > /dev/null && cd ..
+else
+ echo "Cloning $IP"
+ # Sometimes useful to add -b <branch> for buildsite testing
+ git clone $GH/$IP 2>&1
+fi
+
+IP="$SB/$IP"
+
+if [ -d $REPO ];
+then
+ echo "$REPO exists - not updating in case there are local changes!"
+ echo "Perform a manual git pull to sync with upstream $REPO"
+ # cd $REPO && git pull > /dev/null && cd ..
+else
+ echo "Cloning $REPO"
+ git clone $GH/$REPO 2>&1
+fi
+REPO="$SB/$REPO"
+# deploy our pipenv if we haven't already
+# TBD: check timestamp on $IP/requirements.txt and auto-update pipenv deps
+# right now that process is manual
+
+if [ ! -f "Pipfile.lock" ];
+then
+ echo "Setting up pipenv..."
+ pipenv --three install -r $IP/requirements.txt > /dev/null 2>&1 || 'echo "pipenv install failed!" && exit -1'
+
+else
+ echo "Pipfile.lock found, assuming pipenv exists."
+ echo "Run pipenv install -r $IP/requirements.txt to update dependencies if needed."
+fi
+
+# figure out what version of cmark-gfm we need to use
+echo "Extracting cmark version..."
+VERSION=`grep ^VERSION ./infrastructure-pelican/bin/build-cmark.sh | cut -d '=' -f 2`
+
+# if we already built this version of cmark, don't build it again
+if [ $VERSION ];
+then
+ echo "Found version $VERSION"
+else
+ echo "cmark-gfm version string not found! this shouldn't happen."
+ exit -1
+fi
+
+if [ -d "cmark-gfm-$VERSION" ];
+then
+ echo "Using existing ${PWD}/cmark-gfm-$VERSION/lib"
+ export LIBCMARKDIR=${PWD}/cmark-gfm-$VERSION/lib
+else
+ echo "Building cmark-gfm..."
+ eval `./infrastructure-pelican/bin/build-cmark.sh 2>&1 | grep export | grep -v echo `
+fi
+
+# run the site build/deploy in our pipenv environment
+
+# Clean
+if [ -d "$(realpath $REPO)/site-generated" ] && [ -f "$(realpath $REPO)/pelican.auto.py" ];
+then
+ echo "Generated local site exists! Removing..."
+ rm -rf $(realpath $REPO)/site-generated $(realpath $REPO)/pelican.auto.py
+fi
+
+# Build
+cd $REPO
+pipenv run python3 $(realpath $IP)/bin/buildsite.py dir --yaml-dir $(realpath $REPO) --content-dir "$(realpath $REPO)/content"
+
+# Serve
+pipenv run python3 -m pelican content --settings $(realpath $REPO)/pelican.auto.py --o $(realpath $REPO)/site-generated -r -l -b 0.0.0.0
+
diff --git a/bin/pelican.auto.ezt b/bin/pelican.auto.ezt
index abc1b1c..9a40475 100644
--- a/bin/pelican.auto.ezt
+++ b/bin/pelican.auto.ezt
@@ -20,13 +20,15 @@
PLUGINS = [ [for use]'[use]', [end] ]
# All content is located at '.' (aka content/ )
-PAGE_PATHS = [ '.' ]
-STATIC_PATHS = [ '.' ]
+PAGE_PATHS = [ '[if-any pages][pages][else].[end]' ]
+STATIC_PATHS = [ [for static]'[static]', [end] ]
# Where to place/link generated pages
+[if-any pages]
+PATH_METADATA = '[pages]/(?P<path_no_ext>.*)\\..*'
+[else]
PATH_METADATA = '(?P<path_no_ext>.*)\\..*'
-### some sites have the pages in a subdir. TBD.
-#PATH_METADATA = 'pages/(?P<path_no_ext>.*)\\..*'
+[end]
PAGE_SAVE_AS = '{path_no_ext}.html'
@@ -42,7 +44,11 @@
ARCHIVES_SAVE_AS = ''
# Disable articles by pointing to a (should-be-absent) subdir
-ARTICLE_PATHS = [ 'articles' ]
+ARTICLE_PATHS = [ 'blog' ]
+
+# needed to create blogs page
+ARTICLE_URL = 'blog/{slug}.html'
+ARTICLE_SAVE_AS = 'blog/{slug}.html'
# Disable all processing of .html files
READERS = { 'html': None, }
@@ -70,6 +76,24 @@
'debug': [debug],
}
[end]
+
+[if-any uses_sitemap]
+SITEMAP = {
+ "exclude": [sitemap.exclude],
+ "format": "[sitemap.format]",
+ "priorities": {
+ "articles": [sitemap.priorities.articles],
+ "indexes": [sitemap.priorities.indexes],
+ "pages": [sitemap.priorities.pages]
+ },
+ "changefreqs": {
+ "articles": "[sitemap.changefreqs.articles]",
+ "indexes": "[sitemap.changefreqs.indexes]",
+ "pages": "[sitemap.changefreqs.pages]"
+ }
+}
+[end]
+
[if-any uses_data]
# Configure the asfdata plugin
ASF_DATA = {
@@ -91,3 +115,9 @@
[if-any uses_copy]
ASF_COPY = [ [for copy]'[copy]', [end] ]
[end]
+[if-any uses_index]
+# Configure the asfindex plugin
+ASF_INDEX = {
+ 'index': '[index]',
+}
+[end]
diff --git a/pelicanconf.md b/pelicanconf.md
index 9c484d4..fc77449 100644
--- a/pelicanconf.md
+++ b/pelicanconf.md
@@ -1,74 +1,3 @@
# Configuring Pelican ASF
-Conversion of pelicanconf.py to pelicanconf.yaml.
-
-See github.com/apache/template-site and inspect a full pelicanconf.yaml
-
-These are the sections:
-
-## Required
-
-```
-site:
- name: Apache Template
- description: Provides a template for projects wishing to use the Pelican ASF static content system
- domain: template.apache.org
- logo: images/logo.png
- repository: https://github.com/apache/template-site/blob/main/content/
- trademarks: Apache, the Apache feather logo, and "Project" are trademarks or registered trademarks
-
-theme: theme/apache
-```
-
-## Options
-
-### Plugins
-
-If you are using the standard plugins included in Pelican ASF then you can leave this section out.
-Your build will automatically include the `gfm` plugin.
-
-```
-plugins:
- paths:
- - theme/plugins
- use:
- - gfm
-```
-
-### Special setup
-
-These configure four different special features.
-
-```
-setup:
- data: asfdata.yaml
- run:
- - /bin/bash shell.sh
- ignore:
- - README.md
- - include
- - docs
- copy:
- - docs
-```
-
-1. data - uses `asfdata` plugin to build a data model to use in `ezmd` files. www-site is the best example.
-2. run - uses `asfshell` plugin to run scripts. httpd-site's security vulnerability processing is the best example.
-3. ignore - sets Pelican's IGNORE_FILES setting.
-4. copy - uses `asfcopy` plugin to copy static files outside of the pelican process. Include these in ignore as well.
- This is useful if you have large files or many static files.
-
-## Generate ID
-
-The `asfgenid` plugin performs a number of fixups and enhancements. See ASF_GENID in your `pelicanconf.py` and convert.
-
-```
-genid:
- unsafe: yes
- metadata: yes
- elements: yes
- headings_depth: 4
- permalinks: yes
- toc_depth: 4
- tables: yes
-```
+See <a href="https://infra.apache.org/asf-pelican-config.html" target="_blank">Configuring ASF Pelican</a> for configuration details.
diff --git a/plugins/age_days_lt/__init__.py b/plugins/age_days_lt/__init__.py
new file mode 100644
index 0000000..dd32091
--- /dev/null
+++ b/plugins/age_days_lt/__init__.py
@@ -0,0 +1,37 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+The age_in_days plugin adds a Jinja test, age_days_lt.
+
+It is intended to be used in Pelican templates like this to select articles newer than 90 days:
+
+ {% for article in (articles | selectattr("date", "age_days_lt", 90) ) %}
+ ...
+ {% endif %}
+"""
+from pelican import signals
+from . import agedayslt
+
+def add_test(pelican):
+ """Add age_days_lt test to Pelican."""
+ pelican.env.tests.update({'age_days_lt': agedayslt.age_days_lt})
+
+
+def register():
+ """Plugin registration."""
+ signals.generator_init.connect(add_test)
diff --git a/plugins/age_days_lt/agedayslt.py b/plugins/age_days_lt/agedayslt.py
new file mode 100644
index 0000000..c191df5
--- /dev/null
+++ b/plugins/age_days_lt/agedayslt.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from datetime import datetime
+
+def age_days_lt(dt, days):
+ """Return true if a number of days since 'dt' < 'days'"""
+ now = datetime.now(dt.tzinfo)
+ delta = now - dt
+ return delta.days < days
diff --git a/plugins/asfdata.py b/plugins/asfdata.py
index 5ecdcef..9ef530a 100644
--- a/plugins/asfdata.py
+++ b/plugins/asfdata.py
@@ -36,6 +36,7 @@
import ezt
import xml.dom.minidom
+import xml.parsers.expat
import pelican.plugins.signals
import pelican.utils
@@ -47,6 +48,8 @@
(re.compile(r'>'), '>'),
]
+# Format of svn ls -v output: Jan 1 1970
+SVN_DATE_FORMAT = "%b %d %Y"
# read the asfdata configuration in order to get data load and transformation instructions.
def read_config(config_yaml, debug):
@@ -321,6 +324,10 @@
else:
print(f'{seq} - split requires an existing sequence to split')
+ if 'truncate' in sequence:
+ multiple = int(sequence["truncate"])
+ reference = int(reference / multiple) * multiple
+
# if this not already a sequence or dictionary then convert to a sequence
if not is_sequence and not is_dictionary:
# convert the dictionary/list to a sequence of objects
@@ -330,12 +337,14 @@
reference = sequence_dict(seq, reference)
elif isinstance(reference, list):
reference = sequence_list(seq, reference)
- else:
- print(f'{seq}: cannot proceed invalid type, must be dict or list')
# save sequence in metadata
if save_metadata:
metadata[seq] = reference
+ try:
+ metadata[f'{seq}_size'] = len(reference)
+ except TypeError: # allow for integer
+ pass
# create metadata sequences and dictionaries from a data load
@@ -394,13 +403,13 @@
# user = listing[1]
if listing[-6] == '':
# dtm in the past year
- dtm1 = datetime.datetime.strptime(" ".join(listing[-4:-2]) + " " + str(gatherYear), "%b %d %Y")
+ dtm1 = datetime.datetime.strptime(" ".join(listing[-4:-2]) + " " + str(gatherYear), SVN_DATE_FORMAT)
if dtm1 > gatherDate:
- dtm1 = datetime.datetime.strptime(" ".join(listing[-4:-2]) + " " + str(gatherYear - 1), "%b %d %Y")
+ dtm1 = datetime.datetime.strptime(" ".join(listing[-4:-2]) + " " + str(gatherYear - 1), SVN_DATE_FORMAT)
fsize = listing[-5]
else:
# dtm older than one year
- dtm1 = datetime.datetime.strptime(" ".join(listing[-5:-1]), "%b %d %Y")
+ dtm1 = datetime.datetime.strptime(" ".join(listing[-5:-1]), SVN_DATE_FORMAT)
fsize = listing[-6]
# date is close enough
dtm = dtm1.strftime("%m/%d/%Y")
@@ -481,7 +490,7 @@
"""http://www.python.org/doc/2.5.2/lib/minidom-example.txt"""
rc = ''
for node in nodelist:
- if node.nodeType == node.TEXT_NODE:
+ if node.nodeType in [node.CDATA_SECTION_NODE, node.TEXT_NODE]:
rc = rc + node.data
return rc
@@ -509,11 +518,15 @@
if debug:
print(f'blog feed: {feed}')
content = requests.get(feed).text
- dom = xml.dom.minidom.parseString(content)
- # dive into the dom to get 'entry' elements
- entries = dom.getElementsByTagName('entry')
- # we only want count many from the beginning
- entries = entries[:count]
+ # See INFRA-23636: cannot check the page status, so just catch parsing errors
+ try:
+ dom = xml.dom.minidom.parseString(content)
+ # dive into the dom to get 'entry' elements
+ entries = dom.getElementsByTagName('entry')
+ # we only want count many from the beginning
+ entries = entries[:count]
+ except xml.parsers.expat.ExpatError:
+ entries = []
v = [ ]
for entry in entries:
if debug:
@@ -568,17 +581,28 @@
print(f'-----\ntwitter feed: {handle}')
bearer_token = twitter_auth()
if not bearer_token:
- return sequence_list('twitter',{
+ print('WARN: no bearer token for Twitter')
+ return sequence_list('twitter',[{
'text': 'To retrieve tweets supply a valid twitter bearer token in ~/.authtokens'
- })
+ }])
# do not print or display bearer_token as it is a secret
query = f'from:{handle}'
tweet_fields = 'tweet.fields=author_id'
url = f'https://api.twitter.com/2/tweets/search/recent?query={query}&{tweet_fields}'
headers = {'Authorization': f'Bearer {bearer_token}'}
load = connect_to_endpoint(url, headers)
+ result_count = load['meta']['result_count']
+ if result_count == 0:
+ print(f'WARN: No recent tweets for {handle}')
+ return sequence_list('twitter',[{ 'text': 'No recent tweets found' }])
+ if 'data' not in load:
+ print('WARN: "data" not in Twitter response')
+ print(load) # DEBUG; should not happen if result_count > 0
+ return sequence_list('twitter',[{
+ 'text': 'Unable to extract Twitter data'
+ }])
reference = sequence_list('twitter', load['data'])
- if load['meta']['result_count'] < count:
+ if result_count < count:
v = reference
else:
v = reference[:count]
@@ -589,7 +613,10 @@
def process_eccn(fname, debug):
if debug:
print('-----\nECCN:', fname)
- j = yaml.safe_load(open(fname))
+ if fname.startswith("https://"):
+ j = yaml.safe_load(requests.get(fname).text)
+ else:
+ j = yaml.safe_load(open(fname))
# versions have zero or more controlled sources
def make_sources(sources):
diff --git a/plugins/asfindex.py b/plugins/asfindex.py
new file mode 100644
index 0000000..2d32873
--- /dev/null
+++ b/plugins/asfindex.py
@@ -0,0 +1,151 @@
+#!/usr/bin/python -B
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+#
+# asfindex.py - Pelican plugin that generates indexes
+#
+
+import sys
+import subprocess
+import shlex
+import io
+import os
+import os.path
+import traceback
+
+import pelican.plugins.signals
+import pelican.settings
+from pelican.contents import Article, Page, Static
+from pelican.generators import (ArticlesGenerator, # noqa: I100
+ PagesGenerator, SourceFileGenerator,
+ StaticGenerator, TemplatePagesGenerator)
+
+
+# get setting
+# Settings are for the whole pelican environment.
+def get_setting(generators, setting):
+ try:
+ for g in generators:
+ if isinstance(g, PagesGenerator):
+ return g.settings[setting]
+ except Exception:
+ return None
+
+
+# set context
+# Context are the processed settings and other environment which is made available to the JINJA template.
+# Changes to the settings have no effect as those are already copied to each generator's context.
+def set_context(generators, setting, value):
+ for g in generators:
+ if isinstance(g, PagesGenerator):
+ g.context[setting] = value
+ return value
+ return None
+
+
+# get pages
+# The PagesGenerator has a list of pages. Retrieve a sorted array of page information
+def get_pages(generators):
+ site_index = []
+ for g in generators:
+ if isinstance(g, PagesGenerator):
+ for p in g.pages:
+ # use an absolute path
+ save_as = '/' + p.save_as
+ if save_as.endswith('/index.html'):
+ # use "/" for the filename of index.html files assuring that they are first in a folder's list
+ save_as = save_as[:-10]
+ # extract the path name
+ path, page = os.path.split(save_as)
+ site_index.append((path, save_as, p.title))
+ site_index.sort()
+ return site_index
+
+
+# get site index
+def get_index(site_index, scope):
+ current_folder = None
+ started = False
+ site_listing = ''
+ if not scope:
+ return
+ scoped = False
+ if scope != '**':
+ scoped = True
+ for p in site_index:
+ path, page = os.path.split(p[0])
+ folder = page.capitalize()
+ if not scoped or (scoped and p[0].startswith(scope)):
+ if folder != current_folder:
+ if started:
+ site_listing += '</ol>\n'
+ started = True
+ site_listing += f'<h3><a href="{p[1]}">{p[2]}</a></h3>\n'
+ site_listing += '<ol>\n'
+ current_folder = folder
+ else:
+ # menu item for page
+ site_listing += f'<li><a href="{p[1]}">{p[2]}</a></li>\n'
+ if started:
+ site_listing += '</ol>\n'
+ return site_listing
+
+
+# get site menu
+# def get_menu(site_index, menus):
+# currrent_menu = None
+# site_menu = ''
+# if menus:
+# for f in menus:
+# path, page = os.path.split(f)
+# folder = page.capitalize()
+# site_menu += '<li class="nav-item active dropdown">\n'
+# site_menu += f'<a class="nav-link dropdown-toggle" href="#" id="dropdown{folder}" '
+# site_menu += f'role="button" data-toggle="dropdown" aria-expanded="false">{folder}</a>\n'
+# site_menu += f'<ul class="dropdown-menu" aria-labelledby="dropdown{folder}">\n'
+# for p in site_index:
+# if p[0] == f:
+# # menu item for page
+# site_menu += f'<li><a class="dropdownitem" href="{p[1]}">{p[2]}</a></li>\n'
+# site_menu += '</ul></li>\n'
+# return site_menu
+#
+#
+# show pages
+def show_pages(generators):
+ site_index = get_pages(generators)
+ asf_index = get_setting(generators, 'ASF_INDEX')
+ print(asf_index)
+ # Not currently interested in menus this way as it is not generalizable
+ # set_context(generators, 'SITE_MENU', get_menu(site_index, asf_index['menus']))
+ set_context(generators, 'SITE_INDEX', get_index(site_index, asf_index['index']))
+
+
+def tb_finalized(generators):
+ """ Print any exception, before Pelican chews it into nothingness."""
+ try:
+ show_pages(generators)
+ except Exception:
+ print('-----', file=sys.stderr)
+ traceback.print_exc()
+ # exceptions here stop the build
+ raise
+
+
+def register():
+ pelican.plugins.signals.all_generators_finalized.connect(tb_finalized)
diff --git a/plugins/asfrun.py b/plugins/asfrun.py
index d7abce5..42ae89d 100644
--- a/plugins/asfrun.py
+++ b/plugins/asfrun.py
@@ -23,8 +23,6 @@
import sys
import subprocess
import shlex
-import io
-import os
import traceback
import pelican.plugins.signals
diff --git a/plugins/extract_toc/__init__.py b/plugins/extract_toc/__init__.py
new file mode 100644
index 0000000..52c5778
--- /dev/null
+++ b/plugins/extract_toc/__init__.py
@@ -0,0 +1 @@
+from .extract_toc import *
diff --git a/plugins/extract_toc/extract_toc.py b/plugins/extract_toc/extract_toc.py
new file mode 100644
index 0000000..38f11eb
--- /dev/null
+++ b/plugins/extract_toc/extract_toc.py
@@ -0,0 +1,65 @@
+# -*- coding: utf-8 -*-
+"""
+Extract Table of Content
+========================
+A Pelican plugin to extract table of contents (ToC) from `article.content` and
+place it in its own `article.toc` variable for use in templates.
+"""
+
+from os import path
+from bs4 import BeautifulSoup
+from pelican import signals, readers, contents
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+def extract_toc(content):
+ if isinstance(content, contents.Static):
+ return
+
+ soup = BeautifulSoup(content._content, 'html.parser')
+ filename = content.source_path
+ extension = path.splitext(filename)[1][1:]
+ toc = None
+
+ # default Markdown reader
+ if not toc and readers.MarkdownReader.enabled and extension in readers.MarkdownReader.file_extensions:
+ toc = soup.find('div', class_='toc')
+ if toc:
+ toc.extract()
+ if len(toc.find_next('ul').find_all('li')) == 0:
+ toc = None
+
+ # default reStructuredText reader
+ if not toc and readers.RstReader.enabled and extension in readers.RstReader.file_extensions:
+ toc = soup.find('div', class_='contents topic')
+ if toc:
+ toc.extract()
+ tag = BeautifulSoup(str(toc), 'html.parser')
+ tag.div['class'] = 'toc'
+ tag.div['id'] = ''
+ p = tag.find('p', class_='topic-title first')
+ if p:
+ p.extract()
+ toc = tag
+
+ # Pandoc reader (markdown and other formats)
+ if 'pandoc_reader' in content.settings['PLUGINS']:
+ try:
+ from pandoc_reader import PandocReader
+ except ImportError:
+ PandocReader = False
+ if not toc and PandocReader and PandocReader.enabled and extension in PandocReader.file_extensions:
+ toc = soup.find('nav', id='TOC')
+
+ if toc:
+ toc.extract()
+ content._content = soup.decode()
+ content.toc = toc.decode()
+ if content.toc.startswith('<html>'):
+ content.toc = content.toc[12:-14]
+
+
+def register():
+ signals.content_object_init.connect(extract_toc)
diff --git a/plugins/jinja2content/__init__.py b/plugins/jinja2content/__init__.py
new file mode 100644
index 0000000..de025c4
--- /dev/null
+++ b/plugins/jinja2content/__init__.py
@@ -0,0 +1 @@
+from .jinja2content import *
diff --git a/plugins/jinja2content/jinja2content.py b/plugins/jinja2content/jinja2content.py
new file mode 100644
index 0000000..44ff18b
--- /dev/null
+++ b/plugins/jinja2content/jinja2content.py
@@ -0,0 +1,67 @@
+"""
+jinja2content.py
+----------------
+Pelican plugin that processes Markdown files as jinja templates.
+"""
+
+from jinja2 import Environment, FileSystemLoader, ChoiceLoader
+import os
+from pelican import signals
+from pelican.readers import MarkdownReader, HTMLReader, RstReader
+from pelican.utils import pelican_open
+from tempfile import NamedTemporaryFile
+
+class JinjaContentMixin:
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # will look first in 'JINJA2CONTENT_TEMPLATES', by default the
+ # content root path, then in the theme's templates
+ local_dirs = self.settings.get('JINJA2CONTENT_TEMPLATES', ['.'])
+ local_dirs = [os.path.join(self.settings['PATH'], folder)
+ for folder in local_dirs]
+ theme_dir = os.path.join(self.settings['THEME'], 'templates')
+
+ loaders = [FileSystemLoader(_dir) for _dir
+ in local_dirs + [theme_dir]]
+ if 'JINJA_ENVIRONMENT' in self.settings: # pelican 3.7
+ jinja_environment = self.settings['JINJA_ENVIRONMENT']
+ else:
+ jinja_environment = {
+ 'trim_blocks': True,
+ 'lstrip_blocks': True,
+ 'extensions': self.settings['JINJA_EXTENSIONS']
+ }
+ self.env = Environment(
+ loader=ChoiceLoader(loaders),
+ **jinja_environment)
+
+
+ def read(self, source_path):
+ with pelican_open(source_path) as text:
+ text = self.env.from_string(text).render()
+
+ with NamedTemporaryFile(delete=False) as f:
+ f.write(text.encode())
+ f.close()
+ content, metadata = super().read(f.name)
+ os.unlink(f.name)
+ return content, metadata
+
+
+class JinjaMarkdownReader(JinjaContentMixin, MarkdownReader):
+ pass
+
+class JinjaRstReader(JinjaContentMixin, RstReader):
+ pass
+
+class JinjaHTMLReader(JinjaContentMixin, HTMLReader):
+ pass
+
+def add_reader(readers):
+ for Reader in [JinjaMarkdownReader, JinjaRstReader, JinjaHTMLReader]:
+ for ext in Reader.file_extensions:
+ readers.reader_classes[ext] = Reader
+
+def register():
+ signals.readers_init.connect(add_reader)
diff --git a/plugins/md_inline_extension/__init__.py b/plugins/md_inline_extension/__init__.py
new file mode 100644
index 0000000..2453fe9
--- /dev/null
+++ b/plugins/md_inline_extension/__init__.py
@@ -0,0 +1 @@
+from .inline import *
diff --git a/plugins/md_inline_extension/inline.py b/plugins/md_inline_extension/inline.py
new file mode 100644
index 0000000..0d73b0b
--- /dev/null
+++ b/plugins/md_inline_extension/inline.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+"""
+Markdown Inline Extension For Pelican
+=====================================
+Extends Pelican's Markdown module
+and allows for customized inline HTML
+"""
+
+import os
+import sys
+
+from pelican import signals
+
+try:
+ from . pelican_inline_markdown_extension import PelicanInlineMarkdownExtension
+except ImportError as e:
+ PelicanInlineMarkdownExtension = None
+ print("\nMarkdown is not installed - inline Markdown extension disabled\n")
+
+def process_settings(pelicanobj):
+ """Sets user specified settings (see README for more details)"""
+
+ # Default settings
+ inline_settings = {}
+ inline_settings['config'] = {'[]':('', 'pelican-inline')}
+
+ # Get the user specified settings
+ try:
+ settings = pelicanobj.settings['MD_INLINE']
+ except:
+ settings = None
+
+ # If settings have been specified, add them to the config
+ if isinstance(settings, dict):
+ inline_settings['config'].update(settings)
+
+ return inline_settings
+
+def inline_markdown_extension(pelicanobj, config):
+ """Instantiates a customized Markdown extension"""
+
+ # Instantiate Markdown extension and append it to the current extensions
+ try:
+ if isinstance(pelicanobj.settings.get('MD_EXTENSIONS'), list): # pelican 3.6.3 and earlier
+ pelicanobj.settings['MD_EXTENSIONS'].append(PelicanInlineMarkdownExtension(config))
+ else:
+ pelicanobj.settings['MARKDOWN'].setdefault('extensions', []).append(PelicanInlineMarkdownExtension(config))
+ except:
+ sys.excepthook(*sys.exc_info())
+ sys.stderr.write("\nError - the pelican Markdown extension failed to configure. Inline Markdown extension is non-functional.\n")
+ sys.stderr.flush()
+
+def pelican_init(pelicanobj):
+ """Loads settings and instantiates the Python Markdown extension"""
+
+ # If there was an error loading Markdown, then do not process any further
+ if not PelicanInlineMarkdownExtension:
+ return
+
+ # Process settings
+ config = process_settings(pelicanobj)
+
+ # Configure Markdown Extension
+ inline_markdown_extension(pelicanobj, config)
+
+def register():
+ """Plugin registration"""
+ signals.initialized.connect(pelican_init)
diff --git a/plugins/md_inline_extension/pelican_inline_markdown_extension.py b/plugins/md_inline_extension/pelican_inline_markdown_extension.py
new file mode 100644
index 0000000..a3a3eb3
--- /dev/null
+++ b/plugins/md_inline_extension/pelican_inline_markdown_extension.py
@@ -0,0 +1,69 @@
+# -*- coding: utf-8 -*-
+"""
+Pelican Inline Markdown Extension
+==================================
+An extension for the Python Markdown module that enables
+the Pelican Python static site generator to add inline patterns.
+"""
+
+import markdown
+import re
+
+from markdown.util import etree
+from markdown.util import AtomicString
+
+class PelicanInlineMarkdownExtensionPattern(markdown.inlinepatterns.Pattern):
+ """Inline Markdown processing"""
+
+ def __init__(self, pelican_markdown_extension, tag, pattern):
+ super(PelicanInlineMarkdownExtensionPattern,self).__init__(pattern)
+ self.tag = tag
+ self.config = pelican_markdown_extension.getConfig('config')
+
+ def handleMatch(self, m):
+ node = markdown.util.etree.Element(self.tag)
+ tag_attributes = self.config.get(m.group('prefix'), ('', 'pelican-inline'))
+ tag_class = 'pelican-inline' # default class
+ tag_style = '' # default is for no styling
+
+ if isinstance(tag_attributes, tuple):
+ tag_style = tag_attributes[0]
+ tag_class = tag_attributes[1] if len(tag_attributes) > 1 else ''
+ elif isinstance(tag_attributes, str):
+ tag_class = tag_attributes
+
+ if tag_class != '':
+ node.set('class', tag_class)
+ if tag_style!= '':
+ node.set('style', tag_style)
+
+ node.text = markdown.util.AtomicString(m.group('text'))
+
+ return node
+
+class PelicanInlineMarkdownExtension(markdown.Extension):
+ """A Markdown extension enabling processing in Markdown for Pelican"""
+ def __init__(self, config):
+
+ try:
+ # Needed for Markdown versions >= 2.5
+ self.config['config'] = ['{}', 'config for markdown extension']
+ super(PelicanInlineMarkdownExtension,self).__init__(**config)
+ except AttributeError:
+ # Markdown versions < 2.5
+ config['config'] = [config['config'], 'config for markdown extension']
+ super(PelicanInlineMarkdownExtension, self).__init__(config)
+
+ def extendMarkdown(self, md, md_globals):
+ # Regex to detect mathjax
+ config = self.getConfig('config')
+ patterns = []
+
+ # The following mathjax settings can be set via the settings dictionary
+ for key in config:
+ patterns.append(re.escape(key))
+
+ inline_regex = r'(?P<prefix>%s)(?P<text>.+?)\2' % ('|'.join(patterns))
+
+ # Process after escapes
+ md.inlinePatterns.add('texthighlight_inlined', PelicanInlineMarkdownExtensionPattern(self, 'span', inline_regex), '>emphasis2')
diff --git a/plugins/regex_replace/__init__.py b/plugins/regex_replace/__init__.py
new file mode 100644
index 0000000..f97c8a3
--- /dev/null
+++ b/plugins/regex_replace/__init__.py
@@ -0,0 +1,17 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from .regex_replace import *
diff --git a/plugins/regex_replace/regex_replace.py b/plugins/regex_replace/regex_replace.py
new file mode 100644
index 0000000..bbe544b
--- /dev/null
+++ b/plugins/regex_replace/regex_replace.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""
+Markdown regex_replace filter for pelican
+"""
+from pelican import signals
+import re
+
+# Custom filter method
+def regex_replace(s, find, replace):
+ return re.sub(find, replace, s)
+
+def add_filter(pelican):
+ """Add filter to Pelican."""
+ pelican.env.filters.update({'regex_replace': regex_replace})
+
+def register():
+ """Plugin registration."""
+ signals.generator_init.connect(add_filter)
diff --git a/plugins/spu.py b/plugins/spu.py
new file mode 100644
index 0000000..248406c
--- /dev/null
+++ b/plugins/spu.py
@@ -0,0 +1,77 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+This is a collection of simple in-page callable tools for pelican.
+To use a function, use the following syntax in your markdown:
+` spu:command_name("arg1", "arg2", "arg3") `
+
+In HTML, you would do:
+<code> spu:command_name("arg1", "arg2") </code>
+
+command_name must match a respective spu_cmd_* command in python.
+"""
+try:
+ from pelican.plugins import signals
+except ImportError:
+ from pelican import signals
+import pelican.contents
+import requests
+import urllib.parse
+import fnmatch
+import re
+
+# List of subdomains deemed safe for spu:fetch()
+SPU_FETCH_SAFE_DOMAINS = ("*.apache.org",)
+
+
+def spu_cmd_fetch(args: list):
+ """Fetches an external URL and put the content where the call was made"""
+ url = args[0]
+ url_parsed = urllib.parse.urlparse(url)
+ is_safe = any(fnmatch.fnmatch(url_parsed.netloc, pattern) for pattern in SPU_FETCH_SAFE_DOMAINS)
+ if is_safe:
+ print("Fetching external resource " + url)
+ return requests.get(url).text
+ else:
+ print("Not fetching unsafe external resource " + url)
+ return ""
+
+
+def spu_sub(call):
+ my_functions = {k: v for k, v in globals().items() if callable(v) and k.startswith("spu_cmd_")}
+ cmd = call.group(1)
+ args = [x[1] for x in re.findall(r"(['\"]?)(.*?)\1(?:,\s*)?", call.group(2)) if x[1]]
+ fnc = "spu_cmd_" + cmd
+ if fnc in my_functions:
+ return my_functions[fnc](args)
+ return ""
+
+
+def spu_parse(instance: pelican.contents.Page):
+ if instance._content is not None:
+ instance._content = re.sub(
+ r"<code>\s*spu:([_a-z]+)\(((?:(['\"]?)(.*?)\3(?:,\s*)?)*)\s*?\)\s*<\/code>",
+ spu_sub,
+ instance._content,
+ flags=re.UNICODE,
+ )
+
+
+def register():
+ print("Simple Pelican Utils registered.")
+ signals.content_object_init.connect(spu_parse)
diff --git a/requirements.txt b/requirements.txt
index 37c8341..e479112 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
pelican
-#pelican-sitemap
+pelican-sitemap # pelican plugin offering
soupsieve # needed by BeautifulSoup4
BeautifulSoup4 # needed by several plugins
ezt # needed by several plugins and buildsite.py