1
0
Fork 0
forked from github/pelican

feat: this is going live now

This commit is contained in:
Oliver Ladner 2024-12-19 09:56:25 +01:00
commit 06cac4589c
410 changed files with 4684 additions and 44715 deletions

View file

@ -3,7 +3,7 @@ root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

30
.gitignore vendored
View file

@ -1,29 +1 @@
*.egg-info
.*.swp
.*.swo
*.pyc
.DS_Store
docs/_build
docs/*/_build
build
dist
tags
.tox
.coverage
htmlcov
*.orig
venv
samples/output
*.pem
*.lock
.pdm-python
.vale
.venv
**/LC_MESSAGES/*.mo
# direnv
.envrc
# IDE cruft
.idea
.vscode
node_modules/

View file

@ -1,687 +0,0 @@
import argparse
import importlib.metadata
import json
import logging
import multiprocessing
import os
import pprint
import sys
import time
import traceback
from collections.abc import Iterable
# Combines all paths to `pelican` package accessible from `sys.path`
# Makes it possible to install `pelican` and namespace plugins into different
# locations in the file system (e.g. pip with `-e` or `--user`)
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)
# pelican.log has to be the first pelican module to be loaded
# because logging.setLoggerClass has to be called before logging.getLogger
from pelican.log import console, DEFAULT_LOG_HANDLER # noqa: I001
from pelican.log import init as init_logging
from pelican.generators import (
ArticlesGenerator,
PagesGenerator,
SourceFileGenerator,
StaticGenerator,
TemplatePagesGenerator,
)
from pelican.plugins import signals
from pelican.plugins._utils import get_plugin_name, load_plugins
from pelican.server import ComplexHTTPRequestHandler, RootedHTTPServer
from pelican.settings import read_settings
from pelican.utils import clean_output_dir, maybe_pluralize, wait_for_changes
from pelican.writers import Writer
try:
__version__ = importlib.metadata.version("pelican")
except Exception:
__version__ = "unknown"
DEFAULT_CONFIG_NAME = "pelicanconf.py"
logger = logging.getLogger(__name__)
class Pelican:
def __init__(self, settings):
"""Pelican initialization
Performs some checks on the environment before doing anything else.
"""
# define the default settings
self.settings = settings
self.path = settings["PATH"]
self.theme = settings["THEME"]
self.output_path = settings["OUTPUT_PATH"]
self.ignore_files = settings["IGNORE_FILES"]
self.delete_outputdir = settings["DELETE_OUTPUT_DIRECTORY"]
self.output_retention = settings["OUTPUT_RETENTION"]
self.init_path()
self.init_plugins()
signals.initialized.send(self)
def init_path(self):
if not any(p in sys.path for p in ["", os.curdir]):
logger.debug("Adding current directory to system path")
sys.path.insert(0, "")
def init_plugins(self):
self.plugins = []
for plugin in load_plugins(self.settings):
name = get_plugin_name(plugin)
logger.debug("Registering plugin `%s`", name)
try:
plugin.register()
self.plugins.append(plugin)
except Exception as e:
logger.error(
"Cannot register plugin `%s`\n%s",
name,
e,
stacklevel=2,
)
if self.settings.get("DEBUG", False):
console.print_exception()
self.settings["PLUGINS"] = [get_plugin_name(p) for p in self.plugins]
def run(self):
"""Run the generators and return"""
start_time = time.time()
context = self.settings.copy()
# Share these among all the generators and content objects
# They map source paths to Content objects or None
context["generated_content"] = {}
context["static_links"] = set()
context["static_content"] = {}
context["localsiteurl"] = self.settings["SITEURL"]
generators = [
cls(
context=context,
settings=self.settings,
path=self.path,
theme=self.theme,
output_path=self.output_path,
)
for cls in self._get_generator_classes()
]
# Delete the output directory if (1) the appropriate setting is True
# and (2) that directory is not the parent of the source directory
if self.delete_outputdir and os.path.commonpath(
[os.path.realpath(self.output_path)]
) != os.path.commonpath(
[os.path.realpath(self.output_path), os.path.realpath(self.path)]
):
clean_output_dir(self.output_path, self.output_retention)
for p in generators:
if hasattr(p, "generate_context"):
p.generate_context()
if hasattr(p, "check_disabled_readers"):
p.check_disabled_readers()
# for plugins that create/edit the summary
logger.debug("Signal all_generators_finalized.send(<generators>)")
signals.all_generators_finalized.send(generators)
# update links in the summary, etc
for p in generators:
if hasattr(p, "refresh_metadata_intersite_links"):
p.refresh_metadata_intersite_links()
writer = self._get_writer()
for p in generators:
if hasattr(p, "generate_output"):
p.generate_output(writer)
signals.finalized.send(self)
articles_generator = next(
g for g in generators if isinstance(g, ArticlesGenerator)
)
pages_generator = next(g for g in generators if isinstance(g, PagesGenerator))
pluralized_articles = maybe_pluralize(
(len(articles_generator.articles) + len(articles_generator.translations)),
"article",
"articles",
)
pluralized_drafts = maybe_pluralize(
(
len(articles_generator.drafts)
+ len(articles_generator.drafts_translations)
),
"draft",
"drafts",
)
pluralized_hidden_articles = maybe_pluralize(
(
len(articles_generator.hidden_articles)
+ len(articles_generator.hidden_translations)
),
"hidden article",
"hidden articles",
)
pluralized_pages = maybe_pluralize(
(len(pages_generator.pages) + len(pages_generator.translations)),
"page",
"pages",
)
pluralized_hidden_pages = maybe_pluralize(
(
len(pages_generator.hidden_pages)
+ len(pages_generator.hidden_translations)
),
"hidden page",
"hidden pages",
)
pluralized_draft_pages = maybe_pluralize(
(
len(pages_generator.draft_pages)
+ len(pages_generator.draft_translations)
),
"draft page",
"draft pages",
)
console.print(
f"Done: Processed {pluralized_articles}, {pluralized_drafts}, {pluralized_hidden_articles}, {pluralized_pages}, {pluralized_hidden_pages} and {pluralized_draft_pages} in {time.time() - start_time:.2f} seconds."
)
def _get_generator_classes(self):
discovered_generators = [
(ArticlesGenerator, "internal"),
(PagesGenerator, "internal"),
]
if self.settings["TEMPLATE_PAGES"]:
discovered_generators.append((TemplatePagesGenerator, "internal"))
if self.settings["OUTPUT_SOURCES"]:
discovered_generators.append((SourceFileGenerator, "internal"))
for receiver, values in signals.get_generators.send(self):
if not isinstance(values, Iterable):
values = (values,)
for generator in values:
if generator is None:
continue # plugin did not return a generator
discovered_generators.append((generator, receiver.__module__))
# StaticGenerator must run last, so it can identify files that
# were skipped by the other generators, and so static files can
# have their output paths overridden by the {attach} link syntax.
discovered_generators.append((StaticGenerator, "internal"))
generators = []
for generator, origin in discovered_generators:
if not isinstance(generator, type):
logger.error("Generator %s (%s) cannot be loaded", generator, origin)
continue
logger.debug("Found generator: %s (%s)", generator.__name__, origin)
generators.append(generator)
return generators
def _get_writer(self):
writers = [w for _, w in signals.get_writer.send(self) if isinstance(w, type)]
num_writers = len(writers)
if num_writers == 0:
return Writer(self.output_path, settings=self.settings)
if num_writers > 1:
logger.warning("%s writers found, using only first one", num_writers)
writer = writers[0]
logger.debug("Found writer: %s (%s)", writer.__name__, writer.__module__)
return writer(self.output_path, settings=self.settings)
class PrintSettings(argparse.Action):
def __call__(self, parser, namespace, values, option_string):
init_logging(name=__name__)
try:
instance, settings = get_instance(namespace)
except Exception as e:
logger.critical("%s: %s", e.__class__.__name__, e)
console.print_exception()
sys.exit(getattr(e, "exitcode", 1))
if values:
# One or more arguments provided, so only print those settings
for setting in values:
if setting in settings:
# Only add newline between setting name and value if dict
if isinstance(settings[setting], (dict, tuple, list)):
setting_format = "\n{}:\n{}"
else:
setting_format = "\n{}: {}"
console.print(
setting_format.format(
setting, pprint.pformat(settings[setting])
)
)
else:
console.print(f"\n{setting} is not a recognized setting.")
break
else:
# No argument was given to --print-settings, so print all settings
console.print(settings)
parser.exit()
class ParseOverrides(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
overrides = {}
for item in values:
try:
k, v = item.split("=", 1)
except ValueError:
raise ValueError(
"Extra settings must be specified as KEY=VALUE pairs "
f"but you specified {item}"
) from None
try:
overrides[k] = json.loads(v)
except json.decoder.JSONDecodeError:
raise ValueError(
f"Invalid JSON value: {v}. "
"Values specified via -e / --extra-settings flags "
"must be in JSON notation. "
"Use -e KEY='\"string\"' to specify a string value; "
"-e KEY=null to specify None; "
"-e KEY=false (or true) to specify False (or True)."
) from None
setattr(namespace, self.dest, overrides)
def parse_arguments(argv=None):
parser = argparse.ArgumentParser(
description="A tool to generate a static blog, "
" with restructured text input files.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
dest="path",
nargs="?",
help="Path where to find the content files.",
default=None,
)
parser.add_argument(
"-t",
"--theme-path",
dest="theme",
help="Path where to find the theme templates. If not "
"specified, it will use the default one included with "
"pelican.",
)
parser.add_argument(
"-o",
"--output",
dest="output",
help="Where to output the generated files. If not "
"specified, a directory will be created, named "
'"output" in the current path.',
)
parser.add_argument(
"-s",
"--settings",
dest="settings",
help="The settings of the application, this is "
f"automatically set to {DEFAULT_CONFIG_NAME} if a file exists with this "
"name.",
)
parser.add_argument(
"-d",
"--delete-output-directory",
dest="delete_outputdir",
action="store_true",
default=None,
help="Delete the output directory.",
)
parser.add_argument(
"-v",
"--verbose",
action="store_const",
const=logging.INFO,
dest="verbosity",
help="Show all messages.",
)
parser.add_argument(
"-q",
"--quiet",
action="store_const",
const=logging.CRITICAL,
dest="verbosity",
help="Show only critical errors.",
)
parser.add_argument(
"-D",
"--debug",
action="store_const",
const=logging.DEBUG,
dest="verbosity",
help="Show all messages, including debug messages.",
)
parser.add_argument(
"--version",
action="version",
version=__version__,
help="Print the pelican version and exit.",
)
parser.add_argument(
"-r",
"--autoreload",
dest="autoreload",
action="store_true",
help="Relaunch pelican each time a modification occurs"
" on the content files.",
)
parser.add_argument(
"--print-settings",
dest="print_settings",
nargs="*",
action=PrintSettings,
metavar="SETTING_NAME",
help="Print current configuration settings and exit. "
"Append one or more setting name arguments to see the "
"values for specific settings only.",
)
parser.add_argument(
"--relative-urls",
dest="relative_paths",
action="store_true",
help="Use relative urls in output, useful for site development",
)
parser.add_argument(
"--cache-path",
dest="cache_path",
help=(
"Directory in which to store cache files. "
'If not specified, defaults to "cache".'
),
)
parser.add_argument(
"--ignore-cache",
action="store_true",
dest="ignore_cache",
help="Ignore content cache from previous runs by not loading cache files.",
)
parser.add_argument(
"--fatal",
metavar="errors|warnings",
choices=("errors", "warnings"),
default="",
help=(
"Exit the program with non-zero status if any "
"errors/warnings encountered."
),
)
LOG_HANDLERS = {"plain": None, "rich": DEFAULT_LOG_HANDLER}
parser.add_argument(
"--log-handler",
default="rich",
choices=LOG_HANDLERS,
help=(
"Which handler to use to format log messages. "
"The `rich` handler prints output in columns."
),
)
parser.add_argument(
"--logs-dedup-min-level",
default="WARNING",
choices=("DEBUG", "INFO", "WARNING", "ERROR"),
help=(
"Only enable log de-duplication for levels equal"
" to or above the specified value"
),
)
parser.add_argument(
"-l",
"--listen",
dest="listen",
action="store_true",
help="Serve content files via HTTP and port 8000.",
)
parser.add_argument(
"-p",
"--port",
dest="port",
type=int,
help="Port to serve HTTP files at. (default: 8000)",
)
parser.add_argument(
"-b",
"--bind",
dest="bind",
help="IP to bind to when serving files via HTTP (default: 127.0.0.1)",
)
parser.add_argument(
"-e",
"--extra-settings",
dest="overrides",
help="Specify one or more SETTING=VALUE pairs to "
"override settings. VALUE must be in JSON notation: "
"specify string values as SETTING='\"some string\"'; "
"booleans as SETTING=true or SETTING=false; "
"None as SETTING=null.",
nargs="*",
action=ParseOverrides,
default={},
)
args = parser.parse_args(argv)
if args.port is not None and not args.listen:
logger.warning("--port without --listen has no effect")
if args.bind is not None and not args.listen:
logger.warning("--bind without --listen has no effect")
args.log_handler = LOG_HANDLERS[args.log_handler]
return args
def get_config(args):
"""Builds a config dictionary based on supplied `args`."""
config = {}
if args.path:
config["PATH"] = os.path.abspath(os.path.expanduser(args.path))
if args.output:
config["OUTPUT_PATH"] = os.path.abspath(os.path.expanduser(args.output))
if args.theme:
abstheme = os.path.abspath(os.path.expanduser(args.theme))
config["THEME"] = abstheme if os.path.exists(abstheme) else args.theme
if args.delete_outputdir is not None:
config["DELETE_OUTPUT_DIRECTORY"] = args.delete_outputdir
if args.ignore_cache:
config["LOAD_CONTENT_CACHE"] = False
if args.cache_path:
config["CACHE_PATH"] = args.cache_path
if args.relative_paths:
config["RELATIVE_URLS"] = args.relative_paths
if args.port is not None:
config["PORT"] = args.port
if args.bind is not None:
config["BIND"] = args.bind
config["DEBUG"] = args.verbosity == logging.DEBUG
config.update(args.overrides)
return config
def get_instance(args):
config_file = args.settings
if config_file is None and os.path.isfile(DEFAULT_CONFIG_NAME):
config_file = DEFAULT_CONFIG_NAME
args.settings = DEFAULT_CONFIG_NAME
settings = read_settings(config_file, override=get_config(args))
cls = settings["PELICAN_CLASS"]
if isinstance(cls, str):
module, cls_name = cls.rsplit(".", 1)
module = __import__(module)
cls = getattr(module, cls_name)
return cls(settings), settings
def autoreload(args, excqueue=None):
console.print(
" --- AutoReload Mode: Monitoring `content`, `theme` and"
" `settings` for changes. ---"
)
pelican, settings = get_instance(args)
settings_file = os.path.abspath(args.settings)
while True:
try:
pelican.run()
changed_files = wait_for_changes(args.settings, settings)
changed_files = {c[1] for c in changed_files}
if settings_file in changed_files:
pelican, settings = get_instance(args)
console.print(
"\n-> Modified: {}. re-generating...".format(", ".join(changed_files))
)
except KeyboardInterrupt:
if excqueue is not None:
excqueue.put(None)
return
raise
except Exception as e:
if args.verbosity == logging.DEBUG:
if excqueue is not None:
excqueue.put(traceback.format_exception_only(type(e), e)[-1])
else:
raise
logger.warning(
'Caught exception:\n"%s".', e, exc_info=settings.get("DEBUG", False)
)
def listen(server, port, output, excqueue=None):
# set logging level to at least "INFO" (so we can see the server requests)
if logger.level < logging.INFO:
logger.setLevel(logging.INFO)
RootedHTTPServer.allow_reuse_address = True
try:
httpd = RootedHTTPServer(output, (server, port), ComplexHTTPRequestHandler)
except OSError as e:
logging.error("Could not listen on port %s, server %s.", port, server)
if excqueue is not None:
excqueue.put(traceback.format_exception_only(type(e), e)[-1])
return
try:
console.print(f"Serving site at: http://{server}:{port} - Tap CTRL-C to stop")
httpd.serve_forever()
except Exception as e:
if excqueue is not None:
excqueue.put(traceback.format_exception_only(type(e), e)[-1])
return
except KeyboardInterrupt:
httpd.socket.close()
if excqueue is not None:
return
raise
def main(argv=None):
args = parse_arguments(argv)
logs_dedup_min_level = getattr(logging, args.logs_dedup_min_level)
init_logging(
level=args.verbosity,
fatal=args.fatal,
name=__name__,
handler=args.log_handler,
logs_dedup_min_level=logs_dedup_min_level,
)
logger.debug("Pelican version: %s", __version__)
logger.debug("Python version: %s", sys.version.split()[0])
try:
pelican, settings = get_instance(args)
if args.autoreload and args.listen:
excqueue = multiprocessing.Queue()
p1 = multiprocessing.Process(target=autoreload, args=(args, excqueue))
p2 = multiprocessing.Process(
target=listen,
args=(
settings.get("BIND"),
settings.get("PORT"),
settings.get("OUTPUT_PATH"),
excqueue,
),
)
try:
p1.start()
p2.start()
exc = excqueue.get()
if exc is not None:
logger.critical(exc)
finally:
p1.terminate()
p2.terminate()
elif args.autoreload:
autoreload(args)
elif args.listen:
listen(
settings.get("BIND"), settings.get("PORT"), settings.get("OUTPUT_PATH")
)
else:
with console.status("Generating..."):
pelican.run()
except KeyboardInterrupt:
logger.warning("Keyboard interrupt received. Exiting.")
except Exception as e:
logger.critical("%s: %s", e.__class__.__name__, e)
if args.verbosity == logging.DEBUG:
console.print_exception()
sys.exit(getattr(e, "exitcode", 1))

View file

@ -1,8 +0,0 @@
"""
python -m pelican module entry point to run via python -m
"""
from . import main
if __name__ == "__main__":
main()

View file

@ -1,140 +0,0 @@
import hashlib
import logging
import os
import pickle
from pelican.utils import mkdir_p
logger = logging.getLogger(__name__)
class FileDataCacher:
"""Class that can cache data contained in files"""
def __init__(self, settings, cache_name, caching_policy, load_policy):
"""Load the specified cache within CACHE_PATH in settings
only if *load_policy* is True,
May use gzip if GZIP_CACHE ins settings is True.
Sets caching policy according to *caching_policy*.
"""
self.settings = settings
self._cache_path = os.path.join(self.settings["CACHE_PATH"], cache_name)
self._cache_data_policy = caching_policy
if self.settings["GZIP_CACHE"]:
import gzip
self._cache_open = gzip.open
else:
self._cache_open = open
if load_policy:
try:
with self._cache_open(self._cache_path, "rb") as fhandle:
self._cache = pickle.load(fhandle)
except (OSError, UnicodeDecodeError) as err:
logger.debug(
"Cannot load cache %s (this is normal on first "
"run). Proceeding with empty cache.\n%s",
self._cache_path,
err,
)
self._cache = {}
except pickle.PickleError as err:
logger.warning(
"Cannot unpickle cache %s, cache may be using "
"an incompatible protocol (see pelican "
"caching docs). "
"Proceeding with empty cache.\n%s",
self._cache_path,
err,
)
self._cache = {}
else:
self._cache = {}
def cache_data(self, filename, data):
"""Cache data for given file"""
if self._cache_data_policy:
self._cache[filename] = data
def get_cached_data(self, filename, default=None):
"""Get cached data for the given file
if no data is cached, return the default object
"""
return self._cache.get(filename, default)
def save_cache(self):
"""Save the updated cache"""
if self._cache_data_policy:
try:
mkdir_p(self.settings["CACHE_PATH"])
with self._cache_open(self._cache_path, "wb") as fhandle:
pickle.dump(self._cache, fhandle)
except (OSError, pickle.PicklingError, TypeError) as err:
logger.warning(
"Could not save cache %s\n ... %s", self._cache_path, err
)
class FileStampDataCacher(FileDataCacher):
"""Subclass that also caches the stamp of the file"""
def __init__(self, settings, cache_name, caching_policy, load_policy):
"""This subclass additionally sets filestamp function
and base path for filestamping operations
"""
super().__init__(settings, cache_name, caching_policy, load_policy)
method = self.settings["CHECK_MODIFIED_METHOD"]
if method == "mtime":
self._filestamp_func = os.path.getmtime
else:
try:
hash_func = getattr(hashlib, method)
def filestamp_func(filename):
"""return hash of file contents"""
with open(filename, "rb") as fhandle:
return hash_func(fhandle.read()).digest()
self._filestamp_func = filestamp_func
except AttributeError as err:
logger.warning("Could not get hashing function\n\t%s", err)
self._filestamp_func = None
def cache_data(self, filename, data):
"""Cache stamp and data for the given file"""
stamp = self._get_file_stamp(filename)
super().cache_data(filename, (stamp, data))
def _get_file_stamp(self, filename):
"""Check if the given file has been modified
since the previous build.
depending on CHECK_MODIFIED_METHOD
a float may be returned for 'mtime',
a hash for a function name in the hashlib module
or an empty bytes string otherwise
"""
try:
return self._filestamp_func(filename)
except (OSError, TypeError) as err:
logger.warning("Cannot get modification stamp for %s\n\t%s", filename, err)
return ""
def get_cached_data(self, filename, default=None):
"""Get the cached data for the given filename
if the file has not been modified.
If no record exists or file has been modified, return default.
Modification is checked by comparing the cached
and current file stamp.
"""
stamp, data = super().get_cached_data(filename, (None, default))
if stamp != self._get_file_stamp(filename):
return default
return data

View file

@ -1,692 +0,0 @@
import copy
import datetime
import locale
import logging
import os
import re
from datetime import timezone
from html import unescape
from typing import Any, Dict, Optional, Set, Tuple
from urllib.parse import ParseResult, unquote, urljoin, urlparse, urlunparse
try:
from zoneinfo import ZoneInfo
except ModuleNotFoundError:
from backports.zoneinfo import ZoneInfo
from pelican.plugins import signals
from pelican.settings import DEFAULT_CONFIG, Settings
# Import these so that they're available when you import from pelican.contents.
from pelican.urlwrappers import Author, Category, Tag, URLWrapper # NOQA
from pelican.utils import (
deprecated_attribute,
memoized,
path_to_url,
posixize_path,
sanitised_join,
set_date_tzinfo,
slugify,
truncate_html_paragraphs,
truncate_html_words,
)
logger = logging.getLogger(__name__)
class Content:
"""Represents a content.
:param content: the string to parse, containing the original content.
:param metadata: the metadata associated to this page (optional).
:param settings: the settings dictionary (optional).
:param source_path: The location of the source of this content (if any).
:param context: The shared context between generators.
"""
default_template: Optional[str] = None
mandatory_properties: Tuple[str, ...] = ()
@deprecated_attribute(old="filename", new="source_path", since=(3, 2, 0))
def filename():
return None
def __init__(
self,
content: str,
metadata: Optional[Dict[str, Any]] = None,
settings: Optional[Settings] = None,
source_path: Optional[str] = None,
context: Optional[Dict[Any, Any]] = None,
):
if metadata is None:
metadata = {}
if settings is None:
settings = copy.deepcopy(DEFAULT_CONFIG)
self.settings = settings
self._content = content
if context is None:
context = {}
self._context = context
self.translations = []
local_metadata = {}
local_metadata.update(metadata)
# set metadata as attributes
for key, value in local_metadata.items():
if key in ("save_as", "url"):
key = "override_" + key
setattr(self, key.lower(), value)
# also keep track of the metadata attributes available
self.metadata = local_metadata
# default template if it's not defined in page
self.template = self._get_template()
# First, read the authors from "authors", if not, fallback to "author"
# and if not use the settings defined one, if any.
if not hasattr(self, "author"):
if hasattr(self, "authors"):
self.author = self.authors[0]
elif "AUTHOR" in settings:
self.author = Author(settings["AUTHOR"], settings)
if not hasattr(self, "authors") and hasattr(self, "author"):
self.authors = [self.author]
# XXX Split all the following code into pieces, there is too much here.
# manage languages
self.in_default_lang = True
if "DEFAULT_LANG" in settings:
default_lang = settings["DEFAULT_LANG"].lower()
if not hasattr(self, "lang"):
self.lang = default_lang
self.in_default_lang = self.lang == default_lang
# create the slug if not existing, generate slug according to
# setting of SLUG_ATTRIBUTE
if not hasattr(self, "slug"):
if settings["SLUGIFY_SOURCE"] == "title" and hasattr(self, "title"):
value = self.title
elif settings["SLUGIFY_SOURCE"] == "basename" and source_path is not None:
value = os.path.basename(os.path.splitext(source_path)[0])
else:
value = None
if value is not None:
self.slug = slugify(
value,
regex_subs=settings.get("SLUG_REGEX_SUBSTITUTIONS", []),
preserve_case=settings.get("SLUGIFY_PRESERVE_CASE", False),
use_unicode=settings.get("SLUGIFY_USE_UNICODE", False),
)
self.source_path = source_path
self.relative_source_path = self.get_relative_source_path()
# manage the date format
if not hasattr(self, "date_format"):
if hasattr(self, "lang") and self.lang in settings["DATE_FORMATS"]:
self.date_format = settings["DATE_FORMATS"][self.lang]
else:
self.date_format = settings["DEFAULT_DATE_FORMAT"]
if isinstance(self.date_format, tuple):
locale_string = self.date_format[0]
locale.setlocale(locale.LC_ALL, locale_string)
self.date_format = self.date_format[1]
# manage timezone
default_timezone = settings.get("TIMEZONE", "UTC")
timezone = getattr(self, "timezone", default_timezone)
self.timezone = ZoneInfo(timezone)
if hasattr(self, "date"):
self.date = set_date_tzinfo(self.date, timezone)
self.locale_date = self.date.strftime(self.date_format)
if hasattr(self, "modified"):
self.modified = set_date_tzinfo(self.modified, timezone)
self.locale_modified = self.modified.strftime(self.date_format)
# manage status
if not hasattr(self, "status"):
# Previous default of None broke comment plugins and perhaps others
self.status = getattr(self, "default_status", "")
# store the summary metadata if it is set
if "summary" in metadata:
self._summary = metadata["summary"]
signals.content_object_init.send(self)
def __str__(self) -> str:
return self.source_path or repr(self)
def _has_valid_mandatory_properties(self) -> bool:
"""Test mandatory properties are set."""
for prop in self.mandatory_properties:
if not hasattr(self, prop):
logger.error(
"Skipping %s: could not find information about '%s'", self, prop
)
return False
return True
def _has_valid_save_as(self) -> bool:
"""Return true if save_as doesn't write outside output path, false
otherwise."""
try:
output_path = self.settings["OUTPUT_PATH"]
except KeyError:
# we cannot check
return True
try:
sanitised_join(output_path, self.save_as)
except RuntimeError: # outside output_dir
logger.error(
"Skipping %s: file %r would be written outside output path",
self,
self.save_as,
)
return False
return True
def _has_valid_status(self) -> bool:
if hasattr(self, "allowed_statuses"):
if self.status not in self.allowed_statuses:
logger.error(
"Unknown status '%s' for file %s, skipping it. (Not in %s)",
self.status,
self,
self.allowed_statuses,
)
return False
# if undefined we allow all
return True
def is_valid(self) -> bool:
"""Validate Content"""
# Use all() to not short circuit and get results of all validations
return all(
[
self._has_valid_mandatory_properties(),
self._has_valid_save_as(),
self._has_valid_status(),
]
)
@property
def url_format(self) -> Dict[str, Any]:
"""Returns the URL, formatted with the proper values"""
metadata = copy.copy(self.metadata)
path = self.metadata.get("path", self.get_relative_source_path())
metadata.update(
{
"path": path_to_url(path),
"slug": getattr(self, "slug", ""),
"lang": getattr(self, "lang", "en"),
"date": getattr(self, "date", datetime.datetime.now()),
"author": self.author.slug if hasattr(self, "author") else "",
"category": self.category.slug if hasattr(self, "category") else "",
}
)
return metadata
def _expand_settings(self, key: str, klass: Optional[str] = None) -> str:
if not klass:
klass = self.__class__.__name__
fq_key = (f"{klass}_{key}").upper()
return str(self.settings[fq_key]).format(**self.url_format)
def get_url_setting(self, key: str) -> str:
if hasattr(self, "override_" + key):
return getattr(self, "override_" + key)
key = key if self.in_default_lang else f"lang_{key}"
return self._expand_settings(key)
def _link_replacer(self, siteurl: str, m: re.Match) -> str:
what = m.group("what")
value = urlparse(m.group("value"))
path = value.path
origin = m.group("path")
# urllib.parse.urljoin() produces `a.html` for urljoin("..", "a.html")
# so if RELATIVE_URLS are enabled, we fall back to os.path.join() to
# properly get `../a.html`. However, os.path.join() produces
# `baz/http://foo/bar.html` for join("baz", "http://foo/bar.html")
# instead of correct "http://foo/bar.html", so one has to pick a side
# as there is no silver bullet.
if self.settings["RELATIVE_URLS"]:
joiner = os.path.join
else:
joiner = urljoin
# However, it's not *that* simple: urljoin("blog", "index.html")
# produces just `index.html` instead of `blog/index.html` (unlike
# os.path.join()), so in order to get a correct answer one needs to
# append a trailing slash to siteurl in that case. This also makes
# the new behavior fully compatible with Pelican 3.7.1.
if not siteurl.endswith("/"):
siteurl += "/"
# XXX Put this in a different location.
if what in {"filename", "static", "attach"}:
def _get_linked_content(key: str, url: ParseResult) -> Optional[Content]:
nonlocal value
def _find_path(path: str) -> Optional[Content]:
if path.startswith("/"):
path = path[1:]
else:
# relative to the source path of this content
path = self.get_relative_source_path( # type: ignore
os.path.join(self.relative_dir, path)
)
return self._context[key].get(path, None)
# try path
result = _find_path(url.path)
if result is not None:
return result
# try unquoted path
result = _find_path(unquote(url.path))
if result is not None:
return result
# try html unescaped url
unescaped_url = urlparse(unescape(url.geturl()))
result = _find_path(unescaped_url.path)
if result is not None:
value = unescaped_url
return result
# check if a static file is linked with {filename}
if what == "filename" and key == "generated_content":
linked_content = _get_linked_content("static_content", value)
if linked_content:
logger.warning(
"{filename} used for linking to static"
" content %s in %s. Use {static} instead",
value.path,
self.get_relative_source_path(),
)
return linked_content
return None
if what == "filename":
key = "generated_content"
else:
key = "static_content"
linked_content = _get_linked_content(key, value)
if linked_content:
if what == "attach":
linked_content.attach_to(self) # type: ignore
origin = joiner(siteurl, linked_content.url)
origin = origin.replace("\\", "/") # for Windows paths.
else:
logger.warning(
"Unable to find '%s', skipping url replacement.",
value.geturl(),
extra={
"limit_msg": (
"Other resources were not found "
"and their urls not replaced"
)
},
)
elif what == "category":
origin = joiner(siteurl, Category(path, self.settings).url)
elif what == "tag":
origin = joiner(siteurl, Tag(path, self.settings).url)
elif what == "index":
origin = joiner(siteurl, self.settings["INDEX_SAVE_AS"])
elif what == "author":
origin = joiner(siteurl, Author(path, self.settings).url)
else:
logger.warning(
"Replacement Indicator '%s' not recognized, skipping replacement",
what,
)
# keep all other parts, such as query, fragment, etc.
parts = list(value)
parts[2] = origin
origin = urlunparse(parts)
return "".join((m.group("markup"), m.group("quote"), origin, m.group("quote")))
def _get_intrasite_link_regex(self) -> re.Pattern:
intrasite_link_regex = self.settings["INTRASITE_LINK_REGEX"]
regex = rf"""
(?P<markup><[^\>]+ # match tag with all url-value attributes
(?:href|src|poster|data|cite|formaction|action|content)\s*=\s*)
(?P<quote>["\']) # require value to be quoted
(?P<path>{intrasite_link_regex}(?P<value>.*?)) # the url value
(?P=quote)"""
return re.compile(regex, re.X)
def _update_content(self, content: str, siteurl: str) -> str:
"""Update the content attribute.
Change all the relative paths of the content to relative paths
suitable for the output content.
:param content: content resource that will be passed to the templates.
:param siteurl: siteurl which is locally generated by the writer in
case of RELATIVE_URLS.
"""
if not content:
return content
hrefs = self._get_intrasite_link_regex()
return hrefs.sub(lambda m: self._link_replacer(siteurl, m), content)
def get_static_links(self) -> Set[str]:
static_links = set()
hrefs = self._get_intrasite_link_regex()
for m in hrefs.finditer(self._content):
what = m.group("what")
value = urlparse(m.group("value"))
path = value.path
if what not in {"static", "attach"}:
continue
if path.startswith("/"):
path = path[1:]
else:
# relative to the source path of this content
path = self.get_relative_source_path(
os.path.join(self.relative_dir, path)
)
path = path.replace("%20", " ") # type: ignore
static_links.add(path)
return static_links
def get_siteurl(self) -> str:
return self._context.get("localsiteurl", "")
@memoized
def get_content(self, siteurl: str) -> str:
if hasattr(self, "_get_content"):
content = self._get_content()
else:
content = self._content
return self._update_content(content, siteurl)
@property
def content(self) -> str:
return self.get_content(self.get_siteurl())
@memoized
def get_summary(self, siteurl: str) -> str:
"""Returns the summary of an article.
This is based on the summary metadata if set, otherwise truncate the
content.
"""
if "summary" in self.metadata:
return self.metadata["summary"]
content = self.content
max_paragraphs = self.settings.get("SUMMARY_MAX_PARAGRAPHS")
if max_paragraphs is not None:
content = truncate_html_paragraphs(self.content, max_paragraphs)
if self.settings["SUMMARY_MAX_LENGTH"] is None:
return content
return truncate_html_words(
self.content,
self.settings["SUMMARY_MAX_LENGTH"],
self.settings["SUMMARY_END_SUFFIX"],
)
@property
def summary(self) -> str:
return self.get_summary(self.get_siteurl())
def _get_summary(self) -> str:
"""deprecated function to access summary"""
logger.warning(
"_get_summary() has been deprecated since 3.6.4. "
"Use the summary decorator instead"
)
return self.summary
@summary.setter
def summary(self, value: str):
"""Dummy function"""
@property
def status(self) -> str:
return self._status
@status.setter
def status(self, value: str) -> None:
# TODO maybe typecheck
self._status = value.lower()
@property
def url(self) -> str:
return self.get_url_setting("url")
@property
def save_as(self) -> str:
return self.get_url_setting("save_as")
def _get_template(self) -> str:
if hasattr(self, "template") and self.template is not None:
return self.template
else:
return self.default_template
def get_relative_source_path(
self, source_path: Optional[str] = None
) -> Optional[str]:
"""Return the relative path (from the content path) to the given
source_path.
If no source path is specified, use the source path of this
content object.
"""
if not source_path:
source_path = self.source_path
if source_path is None:
return None
return posixize_path(
os.path.relpath(
os.path.abspath(os.path.join(self.settings["PATH"], source_path)),
os.path.abspath(self.settings["PATH"]),
)
)
@property
def relative_dir(self) -> str:
return posixize_path(
os.path.dirname(
os.path.relpath(
os.path.abspath(self.source_path),
os.path.abspath(self.settings["PATH"]),
)
)
)
def refresh_metadata_intersite_links(self) -> None:
for key in self.settings["FORMATTED_FIELDS"]:
if key in self.metadata and key != "summary":
value = self._update_content(self.metadata[key], self.get_siteurl())
self.metadata[key] = value
setattr(self, key.lower(), value)
# _summary is an internal variable that some plugins may be writing to,
# so ensure changes to it are picked up, and write summary back to it
if "summary" in self.settings["FORMATTED_FIELDS"]:
if hasattr(self, "_summary"):
self.metadata["summary"] = self._summary
if "summary" in self.metadata:
self.metadata["summary"] = self._update_content(
self.metadata["summary"], self.get_siteurl()
)
self._summary = self.metadata["summary"]
class SkipStub(Content):
"""Stub class representing content that should not be processed in any way."""
def __init__(
self, content, metadata=None, settings=None, source_path=None, context=None
):
self.source_path = source_path
def is_valid(self):
return False
@property
def content(self):
raise NotImplementedError("Stub content should not be read")
@property
def save_as(self):
raise NotImplementedError("Stub content cannot be saved")
class Page(Content):
mandatory_properties = ("title",)
allowed_statuses = ("published", "hidden", "draft", "skip")
default_status = "published"
default_template = "page"
def _expand_settings(self, key: str) -> str:
klass = "draft_page" if self.status == "draft" else None
return super()._expand_settings(key, klass)
class Article(Content):
mandatory_properties = ("title", "date", "category")
allowed_statuses = ("published", "hidden", "draft", "skip")
default_status = "published"
default_template = "article"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# handle WITH_FUTURE_DATES (designate article to draft based on date)
if not self.settings["WITH_FUTURE_DATES"] and hasattr(self, "date"):
if self.date.tzinfo is None:
now = datetime.datetime.now()
else:
now = datetime.datetime.utcnow().replace(tzinfo=timezone.utc)
if self.date > now:
self.status = "draft"
# if we are a draft and there is no date provided, set max datetime
if not hasattr(self, "date") and self.status == "draft":
self.date = datetime.datetime.max.replace(tzinfo=self.timezone)
def _expand_settings(self, key: str) -> str:
klass = "draft" if self.status == "draft" else "article"
return super()._expand_settings(key, klass)
class Static(Content):
mandatory_properties = ("title",)
default_status = "published"
default_template = None
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self._output_location_referenced = False
@deprecated_attribute(old="filepath", new="source_path", since=(3, 2, 0))
def filepath():
return None
@deprecated_attribute(old="src", new="source_path", since=(3, 2, 0))
def src():
return None
@deprecated_attribute(old="dst", new="save_as", since=(3, 2, 0))
def dst():
return None
@property
def url(self) -> str:
# Note when url has been referenced, so we can avoid overriding it.
self._output_location_referenced = True
return super().url
@property
def save_as(self) -> str:
# Note when save_as has been referenced, so we can avoid overriding it.
self._output_location_referenced = True
return super().save_as
def attach_to(self, content: Content) -> None:
"""Override our output directory with that of the given content object."""
# Determine our file's new output path relative to the linking
# document. If it currently lives beneath the linking
# document's source directory, preserve that relationship on output.
# Otherwise, make it a sibling.
linking_source_dir = os.path.dirname(content.source_path)
tail_path = os.path.relpath(self.source_path, linking_source_dir)
if tail_path.startswith(os.pardir + os.sep):
tail_path = os.path.basename(tail_path)
new_save_as = os.path.join(os.path.dirname(content.save_as), tail_path)
# We do not build our new url by joining tail_path with the linking
# document's url, because we cannot know just by looking at the latter
# whether it points to the document itself or to its parent directory.
# (An url like 'some/content' might mean a directory named 'some'
# with a file named 'content', or it might mean a directory named
# 'some/content' with a file named 'index.html'.) Rather than trying
# to figure it out by comparing the linking document's url and save_as
# path, we simply build our new url from our new save_as path.
new_url = path_to_url(new_save_as)
def _log_reason(reason: str) -> None:
logger.warning(
"The {attach} link in %s cannot relocate "
"%s because %s. Falling back to "
"{filename} link behavior instead.",
content.get_relative_source_path(),
self.get_relative_source_path(),
reason,
extra={"limit_msg": "More {attach} warnings silenced."},
)
# We never override an override, because we don't want to interfere
# with user-defined overrides that might be in EXTRA_PATH_METADATA.
if hasattr(self, "override_save_as") or hasattr(self, "override_url"):
if new_save_as != self.save_as or new_url != self.url:
_log_reason("its output location was already overridden")
return
# We never change an output path that has already been referenced,
# because we don't want to break links that depend on that path.
if self._output_location_referenced:
if new_save_as != self.save_as or new_url != self.url:
_log_reason("another link already referenced its location")
return
self.override_save_as = new_save_as
self.override_url = new_url

File diff suppressed because it is too large Load diff

View file

@ -1,174 +0,0 @@
import logging
from collections import defaultdict
from rich.console import Console
from rich.logging import RichHandler
__all__ = ["init"]
console = Console()
class LimitFilter(logging.Filter):
"""
Remove duplicates records, and limit the number of records in the same
group.
Groups are specified by the message to use when the number of records in
the same group hit the limit.
E.g.: log.warning(('43 is not the answer', 'More erroneous answers'))
"""
LOGS_DEDUP_MIN_LEVEL = logging.WARNING
_ignore = set()
_raised_messages = set()
_threshold = 5
_group_count = defaultdict(int)
def filter(self, record):
# don't limit log messages for anything above "warning"
if record.levelno > self.LOGS_DEDUP_MIN_LEVEL:
return True
# extract group
group = record.__dict__.get("limit_msg", None)
group_args = record.__dict__.get("limit_args", ())
# ignore record if it was already raised
message_key = (record.levelno, record.getMessage())
if message_key in self._raised_messages:
return False
else:
self._raised_messages.add(message_key)
# ignore LOG_FILTER records by templates or messages
# when "debug" isn't enabled
logger_level = logging.getLogger().getEffectiveLevel()
if logger_level > logging.DEBUG:
template_key = (record.levelno, record.msg)
message_key = (record.levelno, record.getMessage())
if template_key in self._ignore or message_key in self._ignore:
return False
# check if we went over threshold
if group:
key = (record.levelno, group)
self._group_count[key] += 1
if self._group_count[key] == self._threshold:
record.msg = group
record.args = group_args
elif self._group_count[key] > self._threshold:
return False
return True
class LimitLogger(logging.Logger):
"""
A logger which adds LimitFilter automatically
"""
limit_filter = LimitFilter()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.enable_filter()
def disable_filter(self):
self.removeFilter(LimitLogger.limit_filter)
def enable_filter(self):
self.addFilter(LimitLogger.limit_filter)
class FatalLogger(LimitLogger):
warnings_fatal = False
errors_fatal = False
def warning(self, *args, stacklevel=1, **kwargs):
"""
Displays a logging warning.
Wrapping it here allows Pelican to filter warnings, and conditionally
make warnings fatal.
Args:
stacklevel (int): the stacklevel that would be used to display the
calling location, except for this function. Adjusting the
stacklevel allows you to see the "true" calling location of the
warning, rather than this wrapper location.
"""
stacklevel += 1
super().warning(*args, stacklevel=stacklevel, **kwargs)
if FatalLogger.warnings_fatal:
raise RuntimeError("Warning encountered")
def error(self, *args, stacklevel=1, **kwargs):
"""
Displays a logging error.
Wrapping it here allows Pelican to filter errors, and conditionally
make errors non-fatal.
Args:
stacklevel (int): the stacklevel that would be used to display the
calling location, except for this function. Adjusting the
stacklevel allows you to see the "true" calling location of the
error, rather than this wrapper location.
"""
stacklevel += 1
super().error(*args, stacklevel=stacklevel, **kwargs)
if FatalLogger.errors_fatal:
raise RuntimeError("Error encountered")
logging.setLoggerClass(FatalLogger)
# force root logger to be of our preferred class
logging.getLogger().__class__ = FatalLogger
DEFAULT_LOG_HANDLER = RichHandler(console=console)
def init(
level=None,
fatal="",
handler=DEFAULT_LOG_HANDLER,
name=None,
logs_dedup_min_level=None,
):
FatalLogger.warnings_fatal = fatal.startswith("warning")
FatalLogger.errors_fatal = bool(fatal)
LOG_FORMAT = "%(message)s"
logging.basicConfig(
level=level,
format=LOG_FORMAT,
datefmt="[%H:%M:%S]",
handlers=[handler] if handler else [],
)
logger = logging.getLogger(name)
if level:
logger.setLevel(level)
if logs_dedup_min_level:
LimitFilter.LOGS_DEDUP_MIN_LEVEL = logs_dedup_min_level
def log_warnings():
import warnings
logging.captureWarnings(True)
warnings.simplefilter("default", DeprecationWarning)
init(logging.DEBUG, name="py.warnings")
if __name__ == "__main__":
init(level=logging.DEBUG, name=__name__)
root_logger = logging.getLogger(__name__)
root_logger.debug("debug")
root_logger.info("info")
root_logger.warning("warning")
root_logger.error("error")
root_logger.critical("critical")

View file

@ -1,171 +0,0 @@
import functools
import logging
import os
from collections import namedtuple
from math import ceil
logger = logging.getLogger(__name__)
PaginationRule = namedtuple( # noqa: PYI024
"PaginationRule",
"min_page URL SAVE_AS",
)
class Paginator:
def __init__(self, name, url, object_list, settings, per_page=None):
self.name = name
self.url = url
self.object_list = object_list
self.settings = settings
if per_page:
self.per_page = per_page
self.orphans = settings["DEFAULT_ORPHANS"]
else:
self.per_page = len(object_list)
self.orphans = 0
self._num_pages = self._count = None
def page(self, number):
"Returns a Page object for the given 1-based page number."
bottom = (number - 1) * self.per_page
top = bottom + self.per_page
if top + self.orphans >= self.count:
top = self.count
return Page(
self.name,
self.url,
self.object_list[bottom:top],
number,
self,
self.settings,
)
def _get_count(self):
"Returns the total number of objects, across all pages."
if self._count is None:
self._count = len(self.object_list)
return self._count
count = property(_get_count)
def _get_num_pages(self):
"Returns the total number of pages."
if self._num_pages is None:
hits = max(1, self.count - self.orphans)
self._num_pages = int(ceil(hits / (float(self.per_page) or 1)))
return self._num_pages
num_pages = property(_get_num_pages)
def _get_page_range(self):
"""
Returns a 1-based range of pages for iterating through within
a template for loop.
"""
return list(range(1, self.num_pages + 1))
page_range = property(_get_page_range)
class Page:
def __init__(self, name, url, object_list, number, paginator, settings):
self.full_name = name
self.name, self.extension = os.path.splitext(name)
dn, fn = os.path.split(name)
self.base_name = dn if fn in ("index.htm", "index.html") else self.name
self.base_url = url
self.object_list = object_list
self.number = number
self.paginator = paginator
self.settings = settings
def __repr__(self):
return f"<Page {self.number} of {self.paginator.num_pages}>"
def has_next(self):
return self.number < self.paginator.num_pages
def has_previous(self):
return self.number > 1
def has_other_pages(self):
return self.has_previous() or self.has_next()
def next_page_number(self):
return self.number + 1
def previous_page_number(self):
return self.number - 1
def start_index(self):
"""
Returns the 1-based index of the first object on this page,
relative to total objects in the paginator.
"""
# Special case, return zero if no items.
if self.paginator.count == 0:
return 0
return (self.paginator.per_page * (self.number - 1)) + 1
def end_index(self):
"""
Returns the 1-based index of the last object on this page,
relative to total objects found (hits).
"""
# Special case for the last page because there can be orphans.
if self.number == self.paginator.num_pages:
return self.paginator.count
return self.number * self.paginator.per_page
def _from_settings(self, key):
"""Returns URL information as defined in settings. Similar to
URLWrapper._from_settings, but specialized to deal with pagination
logic."""
rule = None
# find the last matching pagination rule
for p in self.settings["PAGINATION_PATTERNS"]:
if p.min_page == -1:
if not self.has_next():
rule = p
break
elif p.min_page <= self.number:
rule = p
if not rule:
return ""
prop_value = getattr(rule, key)
if not isinstance(prop_value, str):
logger.warning("%s is set to %s", key, prop_value)
return prop_value
# URL or SAVE_AS is a string, format it with a controlled context
context = {
"save_as": self.full_name,
"url": self.base_url,
"name": self.name,
"base_name": self.base_name,
"extension": self.extension,
"number": self.number,
}
ret = prop_value.format(**context)
# Remove a single leading slash, if any. This is done for backwards
# compatibility reasons. If a leading slash is needed (for URLs
# relative to server root or absolute URLs without the scheme such as
# //blog.my.site/), it can be worked around by prefixing the pagination
# pattern by an additional slash (which then gets removed, preserving
# the other slashes). This also means the following code *can't* be
# changed to lstrip() because that would remove all leading slashes and
# thus make the workaround impossible. See
# test_custom_pagination_pattern() for a verification of this.
if ret.startswith("/"):
ret = ret[1:]
return ret
url = property(functools.partial(_from_settings, key="URL"))
save_as = property(functools.partial(_from_settings, key="SAVE_AS"))

View file

@ -1,138 +0,0 @@
import importlib
import importlib.machinery
import importlib.util
import inspect
import logging
import pkgutil
import sys
logger = logging.getLogger(__name__)
def iter_namespace(ns_pkg):
# Specifying the second argument (prefix) to iter_modules makes the
# returned name an absolute name instead of a relative one. This allows
# import_module to work without having to do additional modification to
# the name.
return pkgutil.iter_modules(ns_pkg.__path__, ns_pkg.__name__ + ".")
def get_namespace_plugins(ns_pkg=None):
if ns_pkg is None:
import pelican.plugins as ns_pkg
return {
name: importlib.import_module(name)
for finder, name, ispkg in iter_namespace(ns_pkg)
if ispkg
}
def list_plugins(ns_pkg=None):
from pelican.log import init as init_logging
init_logging(logging.INFO)
ns_plugins = get_namespace_plugins(ns_pkg)
if ns_plugins:
logger.info("Plugins found:\n" + "\n".join(ns_plugins))
else:
logger.info("No plugins are installed")
def plugin_enabled(name, plugin_list=None):
if plugin_list is None or not plugin_list:
# no plugins are loaded
return False
if name in plugin_list:
# search name as is
return True
if f"pelican.plugins.{name}" in plugin_list:
# check if short name is a namespace plugin
return True
return False
def load_legacy_plugin(plugin, plugin_paths):
if "." in plugin:
# it is in a package, try to resolve package first
package, _, _ = plugin.rpartition(".")
load_legacy_plugin(package, plugin_paths)
# Try to find plugin in PLUGIN_PATHS
spec = importlib.machinery.PathFinder.find_spec(plugin, plugin_paths)
if spec is None:
# If failed, try to find it in normal importable locations
spec = importlib.util.find_spec(plugin)
if spec is None:
raise ImportError(f"Cannot import plugin `{plugin}`")
else:
# Avoid loading the same plugin twice
if spec.name in sys.modules:
return sys.modules[spec.name]
# create module object from spec
mod = importlib.util.module_from_spec(spec)
# place it into sys.modules cache
# necessary if module imports itself at some point (e.g. packages)
sys.modules[spec.name] = mod
try:
# try to execute it inside module object
spec.loader.exec_module(mod)
except Exception: # problem with import
try:
# remove module from sys.modules since it can't be loaded
del sys.modules[spec.name]
except KeyError:
pass
raise
# if all went well, we have the plugin module
return mod
def load_plugins(settings):
logger.debug("Finding namespace plugins")
namespace_plugins = get_namespace_plugins()
if namespace_plugins:
logger.debug("Namespace plugins found:\n" + "\n".join(namespace_plugins))
plugins = []
if settings.get("PLUGINS") is not None:
for plugin in settings["PLUGINS"]:
if isinstance(plugin, str):
logger.debug("Loading plugin `%s`", plugin)
# try to find in namespace plugins
if plugin in namespace_plugins:
plugin = namespace_plugins[plugin]
elif f"pelican.plugins.{plugin}" in namespace_plugins:
plugin = namespace_plugins[f"pelican.plugins.{plugin}"]
# try to import it
else:
try:
plugin = load_legacy_plugin(
plugin, settings.get("PLUGIN_PATHS", [])
)
except ImportError as e:
logger.error("Cannot load plugin `%s`\n%s", plugin, e)
continue
plugins.append(plugin)
else:
plugins = list(namespace_plugins.values())
return plugins
def get_plugin_name(plugin):
"""
Plugins can be passed as module objects, however this breaks caching as
module objects cannot be pickled. To work around this, all plugins are
stringified post-initialization.
"""
if inspect.isclass(plugin):
return plugin.__qualname__
if inspect.ismodule(plugin):
return plugin.__name__
return type(plugin).__qualname__

View file

@ -1,53 +0,0 @@
from blinker import Signal, signal
from ordered_set import OrderedSet
# Signals will call functions in the order of connection, i.e. plugin order
Signal.set_class = OrderedSet
# Run-level signals:
initialized = signal("pelican_initialized")
get_generators = signal("get_generators")
all_generators_finalized = signal("all_generators_finalized")
get_writer = signal("get_writer")
finalized = signal("pelican_finalized")
# Reader-level signals
readers_init = signal("readers_init")
# Generator-level signals
generator_init = signal("generator_init")
article_generator_init = signal("article_generator_init")
article_generator_pretaxonomy = signal("article_generator_pretaxonomy")
article_generator_finalized = signal("article_generator_finalized")
article_generator_write_article = signal("article_generator_write_article")
article_writer_finalized = signal("article_writer_finalized")
page_generator_init = signal("page_generator_init")
page_generator_finalized = signal("page_generator_finalized")
page_generator_write_page = signal("page_generator_write_page")
page_writer_finalized = signal("page_writer_finalized")
static_generator_init = signal("static_generator_init")
static_generator_finalized = signal("static_generator_finalized")
# Page-level signals
article_generator_preread = signal("article_generator_preread")
article_generator_context = signal("article_generator_context")
page_generator_preread = signal("page_generator_preread")
page_generator_context = signal("page_generator_context")
static_generator_preread = signal("static_generator_preread")
static_generator_context = signal("static_generator_context")
content_object_init = signal("content_object_init")
# Writers signals
content_written = signal("content_written")
feed_generated = signal("feed_generated")
feed_written = signal("feed_written")

View file

@ -1,811 +0,0 @@
import datetime
import logging
import os
import re
from collections import OrderedDict
from html import escape
from html.parser import HTMLParser
from io import StringIO
import docutils
import docutils.core
import docutils.io
from docutils.parsers.rst.languages import get_language as get_docutils_lang
from docutils.writers.html4css1 import HTMLTranslator, Writer
from pelican import rstdirectives # NOQA
from pelican.cache import FileStampDataCacher
from pelican.contents import Author, Category, Page, SkipStub, Tag
from pelican.plugins import signals
from pelican.utils import file_suffix, get_date, pelican_open, posixize_path
try:
from markdown import Markdown
except ImportError:
Markdown = False
# Metadata processors have no way to discard an unwanted value, so we have
# them return this value instead to signal that it should be discarded later.
# This means that _filter_discardable_metadata() must be called on processed
# metadata dicts before use, to remove the items with the special value.
_DISCARD = object()
DUPLICATES_DEFINITIONS_ALLOWED = {
"tags": False,
"date": False,
"modified": False,
"status": False,
"category": False,
"author": False,
"save_as": False,
"url": False,
"authors": False,
"slug": False,
}
METADATA_PROCESSORS = {
"tags": lambda x, y: ([Tag(tag, y) for tag in ensure_metadata_list(x)] or _DISCARD),
"date": lambda x, y: get_date(x.replace("_", " ")),
"modified": lambda x, y: get_date(x),
"status": lambda x, y: x.strip() or _DISCARD,
"category": lambda x, y: _process_if_nonempty(Category, x, y),
"author": lambda x, y: _process_if_nonempty(Author, x, y),
"authors": lambda x, y: (
[Author(author, y) for author in ensure_metadata_list(x)] or _DISCARD
),
"slug": lambda x, y: x.strip() or _DISCARD,
}
logger = logging.getLogger(__name__)
def ensure_metadata_list(text):
"""Canonicalize the format of a list of authors or tags. This works
the same way as Docutils' "authors" field: if it's already a list,
those boundaries are preserved; otherwise, it must be a string;
if the string contains semicolons, it is split on semicolons;
otherwise, it is split on commas. This allows you to write
author lists in either "Jane Doe, John Doe" or "Doe, Jane; Doe, John"
format.
Regardless, all list items undergo .strip() before returning, and
empty items are discarded.
"""
if isinstance(text, str):
if ";" in text:
text = text.split(";")
else:
text = text.split(",")
return list(OrderedDict.fromkeys([v for v in (w.strip() for w in text) if v]))
def _process_if_nonempty(processor, name, settings):
"""Removes extra whitespace from name and applies a metadata processor.
If name is empty or all whitespace, returns _DISCARD instead.
"""
name = name.strip()
return processor(name, settings) if name else _DISCARD
def _filter_discardable_metadata(metadata):
"""Return a copy of a dict, minus any items marked as discardable."""
return {name: val for name, val in metadata.items() if val is not _DISCARD}
class BaseReader:
"""Base class to read files.
This class is used to process static files, and it can be inherited for
other types of file. A Reader class must have the following attributes:
- enabled: (boolean) tell if the Reader class is enabled. It
generally depends on the import of some dependency.
- file_extensions: a list of file extensions that the Reader will process.
- extensions: a list of extensions to use in the reader (typical use is
Markdown).
"""
enabled = True
file_extensions = ["static"]
extensions = None
def __init__(self, settings):
self.settings = settings
def process_metadata(self, name, value):
if name in METADATA_PROCESSORS:
return METADATA_PROCESSORS[name](value, self.settings)
return value
def read(self, source_path):
"No-op parser"
content = None
metadata = {}
return content, metadata
def disabled_message(self) -> str:
"""Message about why this plugin was disabled."""
return ""
class _FieldBodyTranslator(HTMLTranslator):
def __init__(self, document):
super().__init__(document)
self.compact_p = None
def astext(self):
return "".join(self.body)
def visit_field_body(self, node):
pass
def depart_field_body(self, node):
pass
def render_node_to_html(document, node, field_body_translator_class):
visitor = field_body_translator_class(document)
node.walkabout(visitor)
return visitor.astext()
class PelicanHTMLWriter(Writer):
def __init__(self):
super().__init__()
self.translator_class = PelicanHTMLTranslator
class PelicanHTMLTranslator(HTMLTranslator):
def visit_abbreviation(self, node):
attrs = {}
if node.hasattr("explanation"):
attrs["title"] = node["explanation"]
self.body.append(self.starttag(node, "abbr", "", **attrs))
def depart_abbreviation(self, node):
self.body.append("</abbr>")
def visit_image(self, node):
# set an empty alt if alt is not specified
# avoids that alt is taken from src
node["alt"] = node.get("alt", "")
return HTMLTranslator.visit_image(self, node)
class RstReader(BaseReader):
"""Reader for reStructuredText files
By default the output HTML is written using
docutils.writers.html4css1.Writer and translated using a subclass of
docutils.writers.html4css1.HTMLTranslator. If you want to override it with
your own writer/translator (e.g. a HTML5-based one), pass your classes to
these two attributes. Look in the source code for details.
writer_class Used for writing contents
field_body_translator_class Used for translating metadata such
as article summary
"""
enabled = bool(docutils)
file_extensions = ["rst"]
writer_class = PelicanHTMLWriter
field_body_translator_class = _FieldBodyTranslator
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
lang_code = self.settings.get("DEFAULT_LANG", "en")
if get_docutils_lang(lang_code):
self._language_code = lang_code
else:
logger.warning(
"Docutils has no localization for '%s'. Using 'en' instead.",
lang_code,
)
self._language_code = "en"
def _parse_metadata(self, document, source_path):
"""Return the dict containing document metadata"""
formatted_fields = self.settings["FORMATTED_FIELDS"]
output = {}
if document.first_child_matching_class(docutils.nodes.title) is None:
logger.warning(
"Document title missing in file %s: "
"Ensure exactly one top level section",
source_path,
)
try:
# docutils 0.18.1+
nodes = document.findall(docutils.nodes.docinfo)
except AttributeError:
# docutils 0.18.0 or before
nodes = document.traverse(docutils.nodes.docinfo)
for docinfo in nodes:
for element in docinfo.children:
if element.tagname == "field": # custom fields (e.g. summary)
name_elem, body_elem = element.children
name = name_elem.astext()
if name.lower() in formatted_fields:
value = render_node_to_html(
document, body_elem, self.field_body_translator_class
)
else:
value = body_elem.astext()
elif element.tagname == "authors": # author list
name = element.tagname
value = [element.astext() for element in element.children]
else: # standard fields (e.g. address)
name = element.tagname
value = element.astext()
name = name.lower()
output[name] = self.process_metadata(name, value)
return output
def _get_publisher(self, source_path):
extra_params = {
"initial_header_level": "2",
"syntax_highlight": "short",
"input_encoding": "utf-8",
"language_code": self._language_code,
"halt_level": 2,
"traceback": True,
"warning_stream": StringIO(),
"embed_stylesheet": False,
}
user_params = self.settings.get("DOCUTILS_SETTINGS")
if user_params:
extra_params.update(user_params)
pub = docutils.core.Publisher(
writer=self.writer_class(), destination_class=docutils.io.StringOutput
)
pub.set_components("standalone", "restructuredtext", "html")
pub.process_programmatic_settings(None, extra_params, None)
pub.set_source(source_path=source_path)
pub.publish()
return pub
def read(self, source_path):
"""Parses restructured text"""
pub = self._get_publisher(source_path)
parts = pub.writer.parts
content = parts.get("body")
metadata = self._parse_metadata(pub.document, source_path)
metadata.setdefault("title", parts.get("title"))
return content, metadata
class MarkdownReader(BaseReader):
"""Reader for Markdown files"""
enabled = bool(Markdown)
file_extensions = ["md", "markdown", "mkd", "mdown"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
settings = self.settings["MARKDOWN"]
settings.setdefault("extension_configs", {})
settings.setdefault("extensions", [])
for extension in settings["extension_configs"].keys():
if extension not in settings["extensions"]:
settings["extensions"].append(extension)
if "markdown.extensions.meta" not in settings["extensions"]:
settings["extensions"].append("markdown.extensions.meta")
self._source_path = None
def _parse_metadata(self, meta):
"""Return the dict containing document metadata"""
formatted_fields = self.settings["FORMATTED_FIELDS"]
# prevent metadata extraction in fields
self._md.preprocessors.deregister("meta")
output = {}
for name, value in meta.items():
name = name.lower()
if name in formatted_fields:
# formatted metadata is special case and join all list values
formatted_values = "\n".join(value)
# reset the markdown instance to clear any state
self._md.reset()
formatted = self._md.convert(formatted_values)
output[name] = self.process_metadata(name, formatted)
elif not DUPLICATES_DEFINITIONS_ALLOWED.get(name, True):
if len(value) > 1:
logger.warning(
"Duplicate definition of `%s` for %s. Using first one.",
name,
self._source_path,
)
output[name] = self.process_metadata(name, value[0])
elif len(value) > 1:
# handle list metadata as list of string
output[name] = self.process_metadata(name, value)
else:
# otherwise, handle metadata as single string
output[name] = self.process_metadata(name, value[0])
return output
def read(self, source_path):
"""Parse content and metadata of markdown files"""
self._source_path = source_path
self._md = Markdown(**self.settings["MARKDOWN"])
with pelican_open(source_path) as text:
content = self._md.convert(text)
if hasattr(self._md, "Meta"):
metadata = self._parse_metadata(self._md.Meta)
else:
metadata = {}
return content, metadata
def disabled_message(self) -> str:
return (
"Could not import 'markdown.Markdown'. "
"Have you installed the 'markdown' package?"
)
class HTMLReader(BaseReader):
"""Parses HTML files as input, looking for meta, title, and body tags"""
file_extensions = ["htm", "html"]
enabled = True
class _HTMLParser(HTMLParser):
def __init__(self, settings, filename):
super().__init__(convert_charrefs=False)
self.body = ""
self.metadata = {}
self.settings = settings
self._data_buffer = ""
self._filename = filename
self._in_top_level = True
self._in_head = False
self._in_title = False
self._in_body = False
self._in_tags = False
def handle_starttag(self, tag, attrs):
if tag == "head" and self._in_top_level:
self._in_top_level = False
self._in_head = True
elif tag == "title" and self._in_head:
self._in_title = True
self._data_buffer = ""
elif tag == "body" and self._in_top_level:
self._in_top_level = False
self._in_body = True
self._data_buffer = ""
elif tag == "meta" and self._in_head:
self._handle_meta_tag(attrs)
elif self._in_body:
self._data_buffer += self.build_tag(tag, attrs, False)
def handle_endtag(self, tag):
if tag == "head":
if self._in_head:
self._in_head = False
self._in_top_level = True
elif self._in_head and tag == "title":
self._in_title = False
self.metadata["title"] = self._data_buffer
elif tag == "body":
self.body = self._data_buffer
self._in_body = False
self._in_top_level = True
elif self._in_body:
self._data_buffer += f"</{escape(tag)}>"
def handle_startendtag(self, tag, attrs):
if tag == "meta" and self._in_head:
self._handle_meta_tag(attrs)
if self._in_body:
self._data_buffer += self.build_tag(tag, attrs, True)
def handle_comment(self, data):
self._data_buffer += f"<!--{data}-->"
def handle_data(self, data):
self._data_buffer += data
def handle_entityref(self, data):
self._data_buffer += f"&{data};"
def handle_charref(self, data):
self._data_buffer += f"&#{data};"
def build_tag(self, tag, attrs, close_tag):
result = f"<{escape(tag)}"
for k, v in attrs:
result += " " + escape(k)
if v is not None:
# If the attribute value contains a double quote, surround
# with single quotes, otherwise use double quotes.
if '"' in v:
result += f"='{escape(v, quote=False)}'"
else:
result += f'="{escape(v, quote=False)}"'
if close_tag:
return result + " />"
return result + ">"
def _handle_meta_tag(self, attrs):
name = self._attr_value(attrs, "name")
if name is None:
attr_list = [f'{k}="{v}"' for k, v in attrs]
attr_serialized = ", ".join(attr_list)
logger.warning(
"Meta tag in file %s does not have a 'name' "
"attribute, skipping. Attributes: %s",
self._filename,
attr_serialized,
)
return
name = name.lower()
contents = self._attr_value(attrs, "content", "")
if not contents:
contents = self._attr_value(attrs, "contents", "")
if contents:
logger.warning(
"Meta tag attribute 'contents' used in file %s, should"
" be changed to 'content'",
self._filename,
extra={
"limit_msg": "Other files have meta tag "
"attribute 'contents' that should "
"be changed to 'content'"
},
)
if name == "keywords":
name = "tags"
if name in self.metadata:
# if this metadata already exists (i.e. a previous tag with the
# same name has already been specified then either convert to
# list or append to list
if isinstance(self.metadata[name], list):
self.metadata[name].append(contents)
else:
self.metadata[name] = [self.metadata[name], contents]
else:
self.metadata[name] = contents
@classmethod
def _attr_value(cls, attrs, name, default=None):
return next((x[1] for x in attrs if x[0] == name), default)
def read(self, filename):
"""Parse content and metadata of HTML files"""
with pelican_open(filename) as content:
parser = self._HTMLParser(self.settings, filename)
parser.feed(content)
parser.close()
metadata = {}
for k in parser.metadata:
metadata[k] = self.process_metadata(k, parser.metadata[k])
return parser.body, metadata
class Readers(FileStampDataCacher):
"""Interface for all readers.
This class contains a mapping of file extensions / Reader classes, to know
which Reader class must be used to read a file (based on its extension).
This is customizable both with the 'READERS' setting, and with the
'readers_init' signall for plugins.
"""
def __init__(self, settings=None, cache_name=""):
self.settings = settings or {}
self.readers = {}
self.disabled_readers = {}
# extension => reader for readers that are enabled
self.reader_classes = {}
# extension => reader for readers that are not enabled
disabled_reader_classes = {}
for cls in [BaseReader] + BaseReader.__subclasses__():
if not cls.enabled:
logger.debug(
"Missing dependencies for %s", ", ".join(cls.file_extensions)
)
for ext in cls.file_extensions:
if cls.enabled:
self.reader_classes[ext] = cls
else:
disabled_reader_classes[ext] = cls
if self.settings["READERS"]:
self.reader_classes.update(self.settings["READERS"])
signals.readers_init.send(self)
for fmt, reader_class in self.reader_classes.items():
if not reader_class:
continue
self.readers[fmt] = reader_class(self.settings)
for fmt, reader_class in disabled_reader_classes.items():
self.disabled_readers[fmt] = reader_class(self.settings)
# set up caching
cache_this_level = (
cache_name != "" and self.settings["CONTENT_CACHING_LAYER"] == "reader"
)
caching_policy = cache_this_level and self.settings["CACHE_CONTENT"]
load_policy = cache_this_level and self.settings["LOAD_CONTENT_CACHE"]
super().__init__(settings, cache_name, caching_policy, load_policy)
@property
def extensions(self):
"""File extensions that will be processed by a reader."""
return self.readers.keys()
@property
def disabled_extensions(self):
return self.disabled_readers.keys()
def read_file(
self,
base_path,
path,
content_class=Page,
fmt=None,
context=None,
preread_signal=None,
preread_sender=None,
context_signal=None,
context_sender=None,
):
"""Return a content object parsed with the given format."""
path = os.path.abspath(os.path.join(base_path, path))
source_path = posixize_path(os.path.relpath(path, base_path))
logger.debug("Read file %s -> %s", source_path, content_class.__name__)
if not fmt:
fmt = file_suffix(path)
if fmt not in self.readers:
raise TypeError("Pelican does not know how to parse %s", path)
if preread_signal:
logger.debug("Signal %s.send(%s)", preread_signal.name, preread_sender)
preread_signal.send(preread_sender)
reader = self.readers[fmt]
metadata = _filter_discardable_metadata(
default_metadata(settings=self.settings, process=reader.process_metadata)
)
metadata.update(
path_metadata(
full_path=path, source_path=source_path, settings=self.settings
)
)
metadata.update(
_filter_discardable_metadata(
parse_path_metadata(
source_path=source_path,
settings=self.settings,
process=reader.process_metadata,
)
)
)
reader_name = reader.__class__.__name__
metadata["reader"] = reader_name.replace("Reader", "").lower()
content, reader_metadata = self.get_cached_data(path, (None, None))
if content is None:
content, reader_metadata = reader.read(path)
reader_metadata = _filter_discardable_metadata(reader_metadata)
self.cache_data(path, (content, reader_metadata))
metadata.update(reader_metadata)
if content:
# find images with empty alt
find_empty_alt(content, path)
# eventually filter the content with typogrify if asked so
if self.settings["TYPOGRIFY"]:
import smartypants
from typogrify.filters import typogrify
typogrify_dashes = self.settings["TYPOGRIFY_DASHES"]
if typogrify_dashes == "oldschool":
smartypants.Attr.default = smartypants.Attr.set2
elif typogrify_dashes == "oldschool_inverted":
smartypants.Attr.default = smartypants.Attr.set3
else:
smartypants.Attr.default = smartypants.Attr.set1
# Tell `smartypants` to also replace &quot; HTML entities with
# smart quotes. This is necessary because Docutils has already
# replaced double quotes with said entities by the time we run
# this filter.
smartypants.Attr.default |= smartypants.Attr.w
def typogrify_wrapper(text):
"""Ensures ignore_tags feature is backward compatible"""
try:
return typogrify(text, self.settings["TYPOGRIFY_IGNORE_TAGS"])
except TypeError:
return typogrify(text)
if content:
content = typogrify_wrapper(content)
if "title" in metadata:
metadata["title"] = typogrify_wrapper(metadata["title"])
if "summary" in metadata:
metadata["summary"] = typogrify_wrapper(metadata["summary"])
if context_signal:
logger.debug(
"Signal %s.send(%s, <metadata>)", context_signal.name, context_sender
)
context_signal.send(context_sender, metadata=metadata)
if metadata.get("status") == "skip":
content_class = SkipStub
return content_class(
content=content,
metadata=metadata,
settings=self.settings,
source_path=path,
context=context,
)
def check_file(self, source_path: str) -> None:
"""Log a warning if a file is processed by a disabled reader."""
reader = self.disabled_readers.get(file_suffix(source_path), None)
if reader:
logger.warning(f"{source_path}: {reader.disabled_message()}")
def find_empty_alt(content, path):
"""Find images with empty alt
Create warnings for all images with empty alt (up to a certain number),
as they are really likely to be accessibility flaws.
"""
imgs = re.compile(
r"""
(?:
# src before alt
<img
[^\>]*
src=(['"])(.*?)\1
[^\>]*
alt=(['"])\3
)|(?:
# alt before src
<img
[^\>]*
alt=(['"])\4
[^\>]*
src=(['"])(.*?)\5
)
""",
re.X,
)
for match in re.findall(imgs, content):
logger.warning(
"Empty alt attribute for image %s in %s",
os.path.basename(match[1] + match[5]),
path,
extra={"limit_msg": "Other images have empty alt attributes"},
)
def default_metadata(settings=None, process=None):
metadata = {}
if settings:
for name, value in dict(settings.get("DEFAULT_METADATA", {})).items():
if process:
value = process(name, value)
metadata[name] = value
if "DEFAULT_CATEGORY" in settings:
value = settings["DEFAULT_CATEGORY"]
if process:
value = process("category", value)
metadata["category"] = value
if settings.get("DEFAULT_DATE", None) and settings["DEFAULT_DATE"] != "fs":
if isinstance(settings["DEFAULT_DATE"], str):
metadata["date"] = get_date(settings["DEFAULT_DATE"])
else:
metadata["date"] = datetime.datetime(*settings["DEFAULT_DATE"])
return metadata
def path_metadata(full_path, source_path, settings=None):
metadata = {}
if settings:
if settings.get("DEFAULT_DATE", None) == "fs":
metadata["date"] = datetime.datetime.fromtimestamp(
os.stat(full_path).st_mtime
)
metadata["modified"] = metadata["date"]
# Apply EXTRA_PATH_METADATA for the source path and the paths of any
# parent directories. Sorting EPM first ensures that the most specific
# path wins conflicts.
epm = settings.get("EXTRA_PATH_METADATA", {})
for path, meta in sorted(epm.items()):
# Enforce a trailing slash when checking for parent directories.
# This prevents false positives when one file or directory's name
# is a prefix of another's.
dirpath = posixize_path(os.path.join(path, ""))
if source_path == path or source_path.startswith(dirpath):
metadata.update(meta)
return metadata
def parse_path_metadata(source_path, settings=None, process=None):
r"""Extract a metadata dictionary from a file's path
>>> import pprint
>>> settings = {
... 'FILENAME_METADATA': r'(?P<slug>[^.]*).*',
... 'PATH_METADATA':
... r'(?P<category>[^/]*)/(?P<date>\d{4}-\d{2}-\d{2})/.*',
... }
>>> reader = BaseReader(settings=settings)
>>> metadata = parse_path_metadata(
... source_path='my-cat/2013-01-01/my-slug.html',
... settings=settings,
... process=reader.process_metadata)
>>> pprint.pprint(metadata) # doctest: +ELLIPSIS
{'category': <pelican.urlwrappers.Category object at ...>,
'date': datetime.datetime(2013, 1, 1, 0, 0),
'slug': 'my-slug'}
"""
metadata = {}
dirname, basename = os.path.split(source_path)
base, ext = os.path.splitext(basename)
subdir = os.path.basename(dirname)
if settings:
checks = []
for key, data in [("FILENAME_METADATA", base), ("PATH_METADATA", source_path)]:
checks.append((settings.get(key, None), data))
if settings.get("USE_FOLDER_AS_CATEGORY", None):
checks.append(("(?P<category>.*)", subdir))
for regexp, data in checks:
if regexp and data:
match = re.match(regexp, data)
if match:
# .items() for py3k compat.
for k, v in match.groupdict().items():
k = k.lower() # metadata must be lowercase
if v is not None and k not in metadata:
if process:
v = process(k, v)
metadata[k] = v
return metadata

View file

@ -1,91 +0,0 @@
import re
from docutils import nodes, utils
from docutils.parsers.rst import Directive, directives, roles
from pygments import highlight
from pygments.formatters import HtmlFormatter
from pygments.lexers import TextLexer, get_lexer_by_name
import pelican.settings as pys
class Pygments(Directive):
"""Source code syntax highlighting."""
required_arguments = 1
optional_arguments = 0
final_argument_whitespace = True
option_spec = {
"anchorlinenos": directives.flag,
"classprefix": directives.unchanged,
"hl_lines": directives.unchanged,
"lineanchors": directives.unchanged,
"linenos": directives.unchanged,
"linenospecial": directives.nonnegative_int,
"linenostart": directives.nonnegative_int,
"linenostep": directives.nonnegative_int,
"lineseparator": directives.unchanged,
"linespans": directives.unchanged,
"nobackground": directives.flag,
"nowrap": directives.flag,
"tagsfile": directives.unchanged,
"tagurlformat": directives.unchanged,
}
has_content = True
def run(self):
self.assert_has_content()
try:
lexer = get_lexer_by_name(self.arguments[0])
except ValueError:
# no lexer found - use the text one instead of an exception
lexer = TextLexer()
# Fetch the defaults
if pys.PYGMENTS_RST_OPTIONS is not None:
for k, v in pys.PYGMENTS_RST_OPTIONS.items():
# Locally set options overrides the defaults
if k not in self.options:
self.options[k] = v
if "linenos" in self.options and self.options["linenos"] not in (
"table",
"inline",
):
if self.options["linenos"] == "none":
self.options.pop("linenos")
else:
self.options["linenos"] = "table"
for flag in ("nowrap", "nobackground", "anchorlinenos"):
if flag in self.options:
self.options[flag] = True
# noclasses should already default to False, but just in case...
formatter = HtmlFormatter(noclasses=False, **self.options)
parsed = highlight("\n".join(self.content), lexer, formatter)
return [nodes.raw("", parsed, format="html")]
directives.register_directive("code-block", Pygments)
directives.register_directive("sourcecode", Pygments)
_abbr_re = re.compile(r"\((.*)\)$", re.DOTALL)
class abbreviation(nodes.Inline, nodes.TextElement):
pass
def abbr_role(typ, rawtext, text, lineno, inliner, options=None, content=None):
text = utils.unescape(text)
m = _abbr_re.search(text)
if m is None:
return [abbreviation(text, text)], []
abbr = text[: m.start()].strip()
expl = m.group(1)
return [abbreviation(abbr, abbr, explanation=expl)], []
roles.register_local_role("abbr", abbr_role)

View file

@ -1,166 +0,0 @@
import argparse
import logging
import os
import posixpath
import ssl
import sys
import urllib
from http import server
try:
from magic import from_file as magic_from_file
except ImportError:
magic_from_file = None
from pelican.log import console # noqa: F401
from pelican.log import init as init_logging
logger = logging.getLogger(__name__)
def parse_arguments():
parser = argparse.ArgumentParser(
description="Pelican Development Server",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"port", default=8000, type=int, nargs="?", help="Port to Listen On"
)
parser.add_argument("server", default="", nargs="?", help="Interface to Listen On")
parser.add_argument("--ssl", action="store_true", help="Activate SSL listener")
parser.add_argument(
"--cert",
default="./cert.pem",
nargs="?",
help="Path to certificate file. Relative to current directory",
)
parser.add_argument(
"--key",
default="./key.pem",
nargs="?",
help="Path to certificate key file. Relative to current directory",
)
parser.add_argument(
"--path",
default=".",
help="Path to pelican source directory to serve. Relative to current directory",
)
return parser.parse_args()
class ComplexHTTPRequestHandler(server.SimpleHTTPRequestHandler):
SUFFIXES = [".html", "/index.html", "/", ""]
extensions_map = {
**server.SimpleHTTPRequestHandler.extensions_map,
# web fonts
".oft": "font/oft",
".sfnt": "font/sfnt",
".ttf": "font/ttf",
".woff": "font/woff",
".woff2": "font/woff2",
}
def translate_path(self, path):
# abandon query parameters
path = path.split("?", 1)[0]
path = path.split("#", 1)[0]
# Don't forget explicit trailing slash when normalizing. Issue17324
trailing_slash = path.rstrip().endswith("/")
path = urllib.parse.unquote(path)
path = posixpath.normpath(path)
words = path.split("/")
words = filter(None, words)
path = self.base_path
for word in words:
if os.path.dirname(word) or word in (os.curdir, os.pardir):
# Ignore components that are not a simple file/directory name
continue
path = os.path.join(path, word)
if trailing_slash:
path += "/"
return path
def do_GET(self):
# cut off a query string
original_path = self.path.split("?", 1)[0]
# try to find file
self.path = self.get_path_that_exists(original_path)
if not self.path:
return
server.SimpleHTTPRequestHandler.do_GET(self)
def get_path_that_exists(self, original_path):
# Try to strip trailing slash
trailing_slash = original_path.endswith("/")
original_path = original_path.rstrip("/")
# Try to detect file by applying various suffixes
tries = []
for suffix in self.SUFFIXES:
if not trailing_slash and suffix == "/":
# if original request does not have trailing slash, skip the '/' suffix
# so that base class can redirect if needed
continue
path = original_path + suffix
if os.path.exists(self.translate_path(path)):
return path
tries.append(path)
logger.warning(
"Unable to find `%s` or variations:\n%s", original_path, "\n".join(tries)
)
return None
def guess_type(self, path):
"""Guess at the mime type for the specified file."""
mimetype = server.SimpleHTTPRequestHandler.guess_type(self, path)
# If the default guess is too generic, try the python-magic library
if mimetype == "application/octet-stream" and magic_from_file:
mimetype = magic_from_file(path, mime=True)
return mimetype
def log_message(self, format, *args):
logger.info(format, *args)
class RootedHTTPServer(server.HTTPServer):
def __init__(self, base_path, *args, **kwargs):
server.HTTPServer.__init__(self, *args, **kwargs)
self.RequestHandlerClass.base_path = base_path
if __name__ == "__main__":
init_logging(level=logging.INFO)
logger.warning(
"'python -m pelican.server' is deprecated.\nThe "
"Pelican development server should be run via "
"'pelican --listen' or 'pelican -l'.\nThis can be combined "
"with regeneration as 'pelican -lr'.\nRerun 'pelican-"
"quickstart' to get new Makefile and tasks.py files."
)
args = parse_arguments()
RootedHTTPServer.allow_reuse_address = True
try:
httpd = RootedHTTPServer(
args.path, (args.server, args.port), ComplexHTTPRequestHandler
)
if args.ssl:
httpd.socket = ssl.wrap_socket(
httpd.socket, keyfile=args.key, certfile=args.cert, server_side=True
)
except ssl.SSLError as e:
logger.error(
"Couldn't open certificate file %s or key file %s", args.cert, args.key
)
logger.error("Could not listen on port %s, server %s.", args.port, args.server)
sys.exit(getattr(e, "exitcode", 1))
logger.info("Serving at port %s, server %s.", args.port, args.server)
try:
httpd.serve_forever()
except KeyboardInterrupt:
logger.info("Shutting down server.")
httpd.socket.close()

View file

@ -1,742 +0,0 @@
import copy
import importlib.util
import inspect
import locale
import logging
import os
import re
import sys
from os.path import isabs
from pathlib import Path
from types import ModuleType
from typing import Any, Dict, Optional
from pelican.log import LimitFilter
def load_source(name: str, path: str) -> ModuleType:
spec = importlib.util.spec_from_file_location(name, path)
mod = importlib.util.module_from_spec(spec)
sys.modules[name] = mod
spec.loader.exec_module(mod)
return mod
logger = logging.getLogger(__name__)
Settings = Dict[str, Any]
DEFAULT_THEME = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "themes", "notmyidea"
)
DEFAULT_CONFIG = {
"PATH": os.curdir,
"ARTICLE_PATHS": [""],
"ARTICLE_EXCLUDES": [],
"PAGE_PATHS": ["pages"],
"PAGE_EXCLUDES": [],
"THEME": DEFAULT_THEME,
"OUTPUT_PATH": "output",
"READERS": {},
"STATIC_PATHS": ["images"],
"STATIC_EXCLUDES": [],
"STATIC_EXCLUDE_SOURCES": True,
"THEME_STATIC_DIR": "theme",
"THEME_STATIC_PATHS": [
"static",
],
"FEED_ALL_ATOM": "feeds/all.atom.xml",
"CATEGORY_FEED_ATOM": "feeds/{slug}.atom.xml",
"AUTHOR_FEED_ATOM": "feeds/{slug}.atom.xml",
"AUTHOR_FEED_RSS": "feeds/{slug}.rss.xml",
"TRANSLATION_FEED_ATOM": "feeds/all-{lang}.atom.xml",
"FEED_MAX_ITEMS": 100,
"RSS_FEED_SUMMARY_ONLY": True,
"FEED_APPEND_REF": False,
"SITEURL": "",
"SITENAME": "A Pelican Blog",
"DISPLAY_PAGES_ON_MENU": True,
"DISPLAY_CATEGORIES_ON_MENU": True,
"DOCUTILS_SETTINGS": {},
"OUTPUT_SOURCES": False,
"OUTPUT_SOURCES_EXTENSION": ".text",
"USE_FOLDER_AS_CATEGORY": True,
"DEFAULT_CATEGORY": "misc",
"WITH_FUTURE_DATES": True,
"CSS_FILE": "main.css",
"NEWEST_FIRST_ARCHIVES": True,
"REVERSE_CATEGORY_ORDER": False,
"DELETE_OUTPUT_DIRECTORY": False,
"OUTPUT_RETENTION": [],
"INDEX_SAVE_AS": "index.html",
"ARTICLE_URL": "{slug}.html",
"ARTICLE_SAVE_AS": "{slug}.html",
"ARTICLE_ORDER_BY": "reversed-date",
"ARTICLE_LANG_URL": "{slug}-{lang}.html",
"ARTICLE_LANG_SAVE_AS": "{slug}-{lang}.html",
"DRAFT_URL": "drafts/{slug}.html",
"DRAFT_SAVE_AS": "drafts/{slug}.html",
"DRAFT_LANG_URL": "drafts/{slug}-{lang}.html",
"DRAFT_LANG_SAVE_AS": "drafts/{slug}-{lang}.html",
"PAGE_URL": "pages/{slug}.html",
"PAGE_SAVE_AS": "pages/{slug}.html",
"PAGE_ORDER_BY": "basename",
"PAGE_LANG_URL": "pages/{slug}-{lang}.html",
"PAGE_LANG_SAVE_AS": "pages/{slug}-{lang}.html",
"DRAFT_PAGE_URL": "drafts/pages/{slug}.html",
"DRAFT_PAGE_SAVE_AS": "drafts/pages/{slug}.html",
"DRAFT_PAGE_LANG_URL": "drafts/pages/{slug}-{lang}.html",
"DRAFT_PAGE_LANG_SAVE_AS": "drafts/pages/{slug}-{lang}.html",
"STATIC_URL": "{path}",
"STATIC_SAVE_AS": "{path}",
"STATIC_CREATE_LINKS": False,
"STATIC_CHECK_IF_MODIFIED": False,
"CATEGORY_URL": "category/{slug}.html",
"CATEGORY_SAVE_AS": "category/{slug}.html",
"TAG_URL": "tag/{slug}.html",
"TAG_SAVE_AS": "tag/{slug}.html",
"AUTHOR_URL": "author/{slug}.html",
"AUTHOR_SAVE_AS": "author/{slug}.html",
"PAGINATION_PATTERNS": [
(1, "{name}{extension}", "{name}{extension}"),
(2, "{name}{number}{extension}", "{name}{number}{extension}"),
],
"YEAR_ARCHIVE_URL": "",
"YEAR_ARCHIVE_SAVE_AS": "",
"MONTH_ARCHIVE_URL": "",
"MONTH_ARCHIVE_SAVE_AS": "",
"DAY_ARCHIVE_URL": "",
"DAY_ARCHIVE_SAVE_AS": "",
"RELATIVE_URLS": False,
"DEFAULT_LANG": "en",
"ARTICLE_TRANSLATION_ID": "slug",
"PAGE_TRANSLATION_ID": "slug",
"DIRECT_TEMPLATES": ["index", "tags", "categories", "authors", "archives"],
"THEME_TEMPLATES_OVERRIDES": [],
"PAGINATED_TEMPLATES": {
"index": None,
"tag": None,
"category": None,
"author": None,
},
"PELICAN_CLASS": "pelican.Pelican",
"DEFAULT_DATE_FORMAT": "%a %d %B %Y",
"DATE_FORMATS": {},
"MARKDOWN": {
"extension_configs": {
"markdown.extensions.codehilite": {"css_class": "highlight"},
"markdown.extensions.extra": {},
"markdown.extensions.meta": {},
},
"output_format": "html5",
},
"JINJA_FILTERS": {},
"JINJA_GLOBALS": {},
"JINJA_TESTS": {},
"JINJA_ENVIRONMENT": {
"trim_blocks": True,
"lstrip_blocks": True,
"extensions": [],
},
"LOG_FILTER": [],
"LOCALE": [""], # defaults to user locale
"DEFAULT_PAGINATION": False,
"DEFAULT_ORPHANS": 0,
"DEFAULT_METADATA": {},
"FILENAME_METADATA": r"(?P<date>\d{4}-\d{2}-\d{2}).*",
"PATH_METADATA": "",
"EXTRA_PATH_METADATA": {},
"ARTICLE_PERMALINK_STRUCTURE": "",
"TYPOGRIFY": False,
"TYPOGRIFY_IGNORE_TAGS": [],
"TYPOGRIFY_DASHES": "default",
"SUMMARY_END_SUFFIX": "",
"SUMMARY_MAX_LENGTH": 50,
"PLUGIN_PATHS": [],
"PLUGINS": None,
"PYGMENTS_RST_OPTIONS": {},
"TEMPLATE_PAGES": {},
"TEMPLATE_EXTENSIONS": [".html"],
"IGNORE_FILES": [".#*"],
"SLUG_REGEX_SUBSTITUTIONS": [
(r"[^\w\s-]", ""), # remove non-alphabetical/whitespace/'-' chars
(r"(?u)\A\s*", ""), # strip leading whitespace
(r"(?u)\s*\Z", ""), # strip trailing whitespace
(r"[-\s]+", "-"), # reduce multiple whitespace or '-' to single '-'
],
"INTRASITE_LINK_REGEX": "[{|](?P<what>.*?)[|}]",
"SLUGIFY_SOURCE": "title",
"SLUGIFY_USE_UNICODE": False,
"SLUGIFY_PRESERVE_CASE": False,
"CACHE_CONTENT": False,
"CONTENT_CACHING_LAYER": "reader",
"CACHE_PATH": "cache",
"GZIP_CACHE": True,
"CHECK_MODIFIED_METHOD": "mtime",
"LOAD_CONTENT_CACHE": False,
"FORMATTED_FIELDS": ["summary"],
"PORT": 8000,
"BIND": "127.0.0.1",
}
PYGMENTS_RST_OPTIONS = None
def read_settings(
path: Optional[str] = None, override: Optional[Settings] = None
) -> Settings:
settings = override or {}
if path:
settings = dict(get_settings_from_file(path), **settings)
if settings:
settings = handle_deprecated_settings(settings)
if path:
# Make relative paths absolute
def getabs(maybe_relative, base_path=path):
if isabs(maybe_relative):
return maybe_relative
return os.path.abspath(
os.path.normpath(
os.path.join(os.path.dirname(base_path), maybe_relative)
)
)
for p in ["PATH", "OUTPUT_PATH", "THEME", "CACHE_PATH"]:
if settings.get(p) is not None:
absp = getabs(settings[p])
# THEME may be a name rather than a path
if p != "THEME" or os.path.exists(absp):
settings[p] = absp
if settings.get("PLUGIN_PATHS") is not None:
settings["PLUGIN_PATHS"] = [
getabs(pluginpath) for pluginpath in settings["PLUGIN_PATHS"]
]
settings = dict(copy.deepcopy(DEFAULT_CONFIG), **settings)
settings = configure_settings(settings)
# This is because there doesn't seem to be a way to pass extra
# parameters to docutils directive handlers, so we have to have a
# variable here that we'll import from within Pygments.run (see
# rstdirectives.py) to see what the user defaults were.
global PYGMENTS_RST_OPTIONS # noqa: PLW0603
PYGMENTS_RST_OPTIONS = settings.get("PYGMENTS_RST_OPTIONS", None)
return settings
def get_settings_from_module(module: Optional[ModuleType] = None) -> Settings:
"""Loads settings from a module, returns a dictionary."""
context = {}
if module is not None:
context.update((k, v) for k, v in inspect.getmembers(module) if k.isupper())
return context
def get_settings_from_file(path: str) -> Settings:
"""Loads settings from a file path, returning a dict."""
name, ext = os.path.splitext(os.path.basename(path))
module = load_source(name, path)
return get_settings_from_module(module)
def get_jinja_environment(settings: Settings) -> Settings:
"""Sets the environment for Jinja"""
jinja_env = settings.setdefault(
"JINJA_ENVIRONMENT", DEFAULT_CONFIG["JINJA_ENVIRONMENT"]
)
# Make sure we include the defaults if the user has set env variables
for key, value in DEFAULT_CONFIG["JINJA_ENVIRONMENT"].items():
if key not in jinja_env:
jinja_env[key] = value
return settings
def _printf_s_to_format_field(printf_string: str, format_field: str) -> str:
"""Tries to replace %s with {format_field} in the provided printf_string.
Raises ValueError in case of failure.
"""
TEST_STRING = "PELICAN_PRINTF_S_DEPRECATION"
expected = printf_string % TEST_STRING
result = printf_string.replace("{", "{{").replace("}", "}}") % f"{{{format_field}}}"
if result.format(**{format_field: TEST_STRING}) != expected:
raise ValueError(f"Failed to safely replace %s with {{{format_field}}}")
return result
def handle_deprecated_settings(settings: Settings) -> Settings:
"""Converts deprecated settings and issues warnings. Issues an exception
if both old and new setting is specified.
"""
# PLUGIN_PATH -> PLUGIN_PATHS
if "PLUGIN_PATH" in settings:
logger.warning(
"PLUGIN_PATH setting has been replaced by "
"PLUGIN_PATHS, moving it to the new setting name."
)
settings["PLUGIN_PATHS"] = settings["PLUGIN_PATH"]
del settings["PLUGIN_PATH"]
# PLUGIN_PATHS: str -> [str]
if isinstance(settings.get("PLUGIN_PATHS"), str):
logger.warning(
"Defining PLUGIN_PATHS setting as string "
"has been deprecated (should be a list)"
)
settings["PLUGIN_PATHS"] = [settings["PLUGIN_PATHS"]]
# JINJA_EXTENSIONS -> JINJA_ENVIRONMENT > extensions
if "JINJA_EXTENSIONS" in settings:
logger.warning(
"JINJA_EXTENSIONS setting has been deprecated, "
"moving it to JINJA_ENVIRONMENT setting."
)
settings["JINJA_ENVIRONMENT"]["extensions"] = settings["JINJA_EXTENSIONS"]
del settings["JINJA_EXTENSIONS"]
# {ARTICLE,PAGE}_DIR -> {ARTICLE,PAGE}_PATHS
for key in ["ARTICLE", "PAGE"]:
old_key = key + "_DIR"
new_key = key + "_PATHS"
if old_key in settings:
logger.warning(
"Deprecated setting %s, moving it to %s list", old_key, new_key
)
settings[new_key] = [settings[old_key]] # also make a list
del settings[old_key]
# EXTRA_TEMPLATES_PATHS -> THEME_TEMPLATES_OVERRIDES
if "EXTRA_TEMPLATES_PATHS" in settings:
logger.warning(
"EXTRA_TEMPLATES_PATHS is deprecated use "
"THEME_TEMPLATES_OVERRIDES instead."
)
if settings.get("THEME_TEMPLATES_OVERRIDES"):
raise Exception(
"Setting both EXTRA_TEMPLATES_PATHS and "
"THEME_TEMPLATES_OVERRIDES is not permitted. Please move to "
"only setting THEME_TEMPLATES_OVERRIDES."
)
settings["THEME_TEMPLATES_OVERRIDES"] = settings["EXTRA_TEMPLATES_PATHS"]
del settings["EXTRA_TEMPLATES_PATHS"]
# MD_EXTENSIONS -> MARKDOWN
if "MD_EXTENSIONS" in settings:
logger.warning(
"MD_EXTENSIONS is deprecated use MARKDOWN "
"instead. Falling back to the default."
)
settings["MARKDOWN"] = DEFAULT_CONFIG["MARKDOWN"]
# LESS_GENERATOR -> Webassets plugin
# FILES_TO_COPY -> STATIC_PATHS, EXTRA_PATH_METADATA
for old, new, doc in [
("LESS_GENERATOR", "the Webassets plugin", None),
(
"FILES_TO_COPY",
"STATIC_PATHS and EXTRA_PATH_METADATA",
"https://github.com/getpelican/pelican/"
"blob/main/docs/settings.rst#path-metadata",
),
]:
if old in settings:
message = f"The {old} setting has been removed in favor of {new}"
if doc:
message += f", see {doc} for details"
logger.warning(message)
# PAGINATED_DIRECT_TEMPLATES -> PAGINATED_TEMPLATES
if "PAGINATED_DIRECT_TEMPLATES" in settings:
message = "The {} setting has been removed in favor of {}".format(
"PAGINATED_DIRECT_TEMPLATES", "PAGINATED_TEMPLATES"
)
logger.warning(message)
# set PAGINATED_TEMPLATES
if "PAGINATED_TEMPLATES" not in settings:
settings["PAGINATED_TEMPLATES"] = {
"tag": None,
"category": None,
"author": None,
}
for t in settings["PAGINATED_DIRECT_TEMPLATES"]:
if t not in settings["PAGINATED_TEMPLATES"]:
settings["PAGINATED_TEMPLATES"][t] = None
del settings["PAGINATED_DIRECT_TEMPLATES"]
# {SLUG,CATEGORY,TAG,AUTHOR}_SUBSTITUTIONS ->
# {SLUG,CATEGORY,TAG,AUTHOR}_REGEX_SUBSTITUTIONS
url_settings_url = "http://docs.getpelican.com/en/latest/settings.html#url-settings"
flavours = {"SLUG", "CATEGORY", "TAG", "AUTHOR"}
old_values = {
f: settings[f + "_SUBSTITUTIONS"]
for f in flavours
if f + "_SUBSTITUTIONS" in settings
}
new_values = {
f: settings[f + "_REGEX_SUBSTITUTIONS"]
for f in flavours
if f + "_REGEX_SUBSTITUTIONS" in settings
}
if old_values and new_values:
raise Exception(
"Setting both {new_key} and {old_key} (or variants thereof) is "
"not permitted. Please move to only setting {new_key}.".format(
old_key="SLUG_SUBSTITUTIONS", new_key="SLUG_REGEX_SUBSTITUTIONS"
)
)
if old_values:
message = (
"{} and variants thereof are deprecated and will be "
"removed in the future. Please use {} and variants thereof "
"instead. Check {}.".format(
"SLUG_SUBSTITUTIONS", "SLUG_REGEX_SUBSTITUTIONS", url_settings_url
)
)
logger.warning(message)
if old_values.get("SLUG"):
for f in ("CATEGORY", "TAG"):
if old_values.get(f):
old_values[f] = old_values["SLUG"] + old_values[f]
old_values["AUTHOR"] = old_values.get("AUTHOR", [])
for f in flavours:
if old_values.get(f) is not None:
regex_subs = []
# by default will replace non-alphanum characters
replace = True
for tpl in old_values[f]:
try:
src, dst, skip = tpl
if skip:
replace = False
except ValueError:
src, dst = tpl
regex_subs.append((re.escape(src), dst.replace("\\", r"\\")))
if replace:
regex_subs += [
(r"[^\w\s-]", ""),
(r"(?u)\A\s*", ""),
(r"(?u)\s*\Z", ""),
(r"[-\s]+", "-"),
]
else:
regex_subs += [
(r"(?u)\A\s*", ""),
(r"(?u)\s*\Z", ""),
]
settings[f + "_REGEX_SUBSTITUTIONS"] = regex_subs
settings.pop(f + "_SUBSTITUTIONS", None)
# `%s` -> '{slug}` or `{lang}` in FEED settings
for key in ["TRANSLATION_FEED_ATOM", "TRANSLATION_FEED_RSS"]:
if (
settings.get(key)
and not isinstance(settings[key], Path)
and "%s" in settings[key]
):
logger.warning("%%s usage in %s is deprecated, use {lang} instead.", key)
try:
settings[key] = _printf_s_to_format_field(settings[key], "lang")
except ValueError:
logger.warning(
"Failed to convert %%s to {lang} for %s. "
"Falling back to default.",
key,
)
settings[key] = DEFAULT_CONFIG[key]
for key in [
"AUTHOR_FEED_ATOM",
"AUTHOR_FEED_RSS",
"CATEGORY_FEED_ATOM",
"CATEGORY_FEED_RSS",
"TAG_FEED_ATOM",
"TAG_FEED_RSS",
]:
if (
settings.get(key)
and not isinstance(settings[key], Path)
and "%s" in settings[key]
):
logger.warning("%%s usage in %s is deprecated, use {slug} instead.", key)
try:
settings[key] = _printf_s_to_format_field(settings[key], "slug")
except ValueError:
logger.warning(
"Failed to convert %%s to {slug} for %s. "
"Falling back to default.",
key,
)
settings[key] = DEFAULT_CONFIG[key]
# CLEAN_URLS
if settings.get("CLEAN_URLS", False):
logger.warning(
"Found deprecated `CLEAN_URLS` in settings."
" Modifying the following settings for the"
" same behaviour."
)
settings["ARTICLE_URL"] = "{slug}/"
settings["ARTICLE_LANG_URL"] = "{slug}-{lang}/"
settings["PAGE_URL"] = "pages/{slug}/"
settings["PAGE_LANG_URL"] = "pages/{slug}-{lang}/"
for setting in ("ARTICLE_URL", "ARTICLE_LANG_URL", "PAGE_URL", "PAGE_LANG_URL"):
logger.warning("%s = '%s'", setting, settings[setting])
# AUTORELOAD_IGNORE_CACHE -> --ignore-cache
if settings.get("AUTORELOAD_IGNORE_CACHE"):
logger.warning(
"Found deprecated `AUTORELOAD_IGNORE_CACHE` in "
"settings. Use --ignore-cache instead."
)
settings.pop("AUTORELOAD_IGNORE_CACHE")
# ARTICLE_PERMALINK_STRUCTURE
if settings.get("ARTICLE_PERMALINK_STRUCTURE", False):
logger.warning(
"Found deprecated `ARTICLE_PERMALINK_STRUCTURE` in"
" settings. Modifying the following settings for"
" the same behaviour."
)
structure = settings["ARTICLE_PERMALINK_STRUCTURE"]
# Convert %(variable) into {variable}.
structure = re.sub(r"%\((\w+)\)s", r"{\g<1>}", structure)
# Convert %x into {date:%x} for strftime
structure = re.sub(r"(%[A-z])", r"{date:\g<1>}", structure)
# Strip a / prefix
structure = re.sub("^/", "", structure)
for setting in (
"ARTICLE_URL",
"ARTICLE_LANG_URL",
"PAGE_URL",
"PAGE_LANG_URL",
"DRAFT_URL",
"DRAFT_LANG_URL",
"ARTICLE_SAVE_AS",
"ARTICLE_LANG_SAVE_AS",
"DRAFT_SAVE_AS",
"DRAFT_LANG_SAVE_AS",
"PAGE_SAVE_AS",
"PAGE_LANG_SAVE_AS",
):
settings[setting] = os.path.join(structure, settings[setting])
logger.warning("%s = '%s'", setting, settings[setting])
# {,TAG,CATEGORY,TRANSLATION}_FEED -> {,TAG,CATEGORY,TRANSLATION}_FEED_ATOM
for new, old in [
("FEED", "FEED_ATOM"),
("TAG_FEED", "TAG_FEED_ATOM"),
("CATEGORY_FEED", "CATEGORY_FEED_ATOM"),
("TRANSLATION_FEED", "TRANSLATION_FEED_ATOM"),
]:
if settings.get(new, False):
logger.warning(
"Found deprecated `%(new)s` in settings. Modify %(new)s "
"to %(old)s in your settings and theme for the same "
"behavior. Temporarily setting %(old)s for backwards "
"compatibility.",
{"new": new, "old": old},
)
settings[old] = settings[new]
# Warn if removed WRITE_SELECTED is present
if "WRITE_SELECTED" in settings:
logger.warning(
"WRITE_SELECTED is present in settings but this functionality was removed. "
"It will have no effect."
)
return settings
def configure_settings(settings: Settings) -> Settings:
"""Provide optimizations, error checking, and warnings for the given
settings.
Also, specify the log messages to be ignored.
"""
if "PATH" not in settings or not os.path.isdir(settings["PATH"]):
raise Exception(
"You need to specify a path containing the content"
" (see pelican --help for more information)"
)
# specify the log messages to be ignored
log_filter = settings.get("LOG_FILTER", DEFAULT_CONFIG["LOG_FILTER"])
LimitFilter._ignore.update(set(log_filter))
# lookup the theme in "pelican/themes" if the given one doesn't exist
if not os.path.isdir(settings["THEME"]):
theme_path = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "themes", settings["THEME"]
)
if os.path.exists(theme_path):
settings["THEME"] = theme_path
else:
raise Exception("Could not find the theme {}".format(settings["THEME"]))
# standardize strings to lowercase strings
for key in ["DEFAULT_LANG"]:
if key in settings:
settings[key] = settings[key].lower()
# set defaults for Jinja environment
settings = get_jinja_environment(settings)
# standardize strings to lists
for key in ["LOCALE"]:
if key in settings and isinstance(settings[key], str):
settings[key] = [settings[key]]
# check settings that must be a particular type
for key, types in [
("OUTPUT_SOURCES_EXTENSION", str),
("FILENAME_METADATA", str),
]:
if key in settings and not isinstance(settings[key], types):
value = settings.pop(key)
logger.warn(
"Detected misconfigured %s (%s), falling back to the default (%s)",
key,
value,
DEFAULT_CONFIG[key],
)
# try to set the different locales, fallback on the default.
locales = settings.get("LOCALE", DEFAULT_CONFIG["LOCALE"])
for locale_ in locales:
try:
locale.setlocale(locale.LC_ALL, str(locale_))
break # break if it is successful
except locale.Error:
pass
else:
logger.warning(
"Locale could not be set. Check the LOCALE setting, ensuring it "
"is valid and available on your system."
)
if "SITEURL" in settings:
# If SITEURL has a trailing slash, remove it and provide a warning
siteurl = settings["SITEURL"]
if siteurl.endswith("/"):
settings["SITEURL"] = siteurl[:-1]
logger.warning("Removed extraneous trailing slash from SITEURL.")
# If SITEURL is defined but FEED_DOMAIN isn't,
# set FEED_DOMAIN to SITEURL
if "FEED_DOMAIN" not in settings:
settings["FEED_DOMAIN"] = settings["SITEURL"]
# check content caching layer and warn of incompatibilities
if (
settings.get("CACHE_CONTENT", False)
and settings.get("CONTENT_CACHING_LAYER", "") == "generator"
and not settings.get("WITH_FUTURE_DATES", True)
):
logger.warning(
"WITH_FUTURE_DATES conflicts with CONTENT_CACHING_LAYER "
"set to 'generator', use 'reader' layer instead"
)
# Warn if feeds are generated with both SITEURL & FEED_DOMAIN undefined
feed_keys = [
"FEED_ATOM",
"FEED_RSS",
"FEED_ALL_ATOM",
"FEED_ALL_RSS",
"CATEGORY_FEED_ATOM",
"CATEGORY_FEED_RSS",
"AUTHOR_FEED_ATOM",
"AUTHOR_FEED_RSS",
"TAG_FEED_ATOM",
"TAG_FEED_RSS",
"TRANSLATION_FEED_ATOM",
"TRANSLATION_FEED_RSS",
]
if any(settings.get(k) for k in feed_keys):
if not settings.get("SITEURL"):
logger.warning(
"Feeds generated without SITEURL set properly may not be valid"
)
if "TIMEZONE" not in settings:
logger.warning(
"No timezone information specified in the settings. Assuming"
" your timezone is UTC for feed generation. Check "
"https://docs.getpelican.com/en/latest/settings.html#TIMEZONE "
"for more information"
)
# fix up pagination rules
from pelican.paginator import PaginationRule
pagination_rules = [
PaginationRule(*r)
for r in settings.get(
"PAGINATION_PATTERNS",
DEFAULT_CONFIG["PAGINATION_PATTERNS"],
)
]
settings["PAGINATION_PATTERNS"] = sorted(
pagination_rules,
key=lambda r: r[0],
)
# Save people from accidentally setting a string rather than a list
path_keys = (
"ARTICLE_EXCLUDES",
"DEFAULT_METADATA",
"DIRECT_TEMPLATES",
"THEME_TEMPLATES_OVERRIDES",
"FILES_TO_COPY",
"IGNORE_FILES",
"PAGINATED_DIRECT_TEMPLATES",
"PLUGINS",
"STATIC_EXCLUDES",
"STATIC_PATHS",
"THEME_STATIC_PATHS",
"ARTICLE_PATHS",
"PAGE_PATHS",
)
for PATH_KEY in filter(lambda k: k in settings, path_keys):
if isinstance(settings[PATH_KEY], str):
logger.warning(
"Detected misconfiguration with %s setting "
"(must be a list), falling back to the default",
PATH_KEY,
)
settings[PATH_KEY] = DEFAULT_CONFIG[PATH_KEY]
# Add {PAGE,ARTICLE}_PATHS to {ARTICLE,PAGE}_EXCLUDES
mutually_exclusive = ("ARTICLE", "PAGE")
for type_1, type_2 in [mutually_exclusive, mutually_exclusive[::-1]]:
try:
includes = settings[type_1 + "_PATHS"]
excludes = settings[type_2 + "_EXCLUDES"]
for path in includes:
if path not in excludes:
excludes.append(path)
except KeyError:
continue # setting not specified, nothing to do
return settings

View file

@ -1,4 +0,0 @@
raise ImportError(
"Importing from `pelican.signals` is deprecated. "
"Use `from pelican import signals` or `import pelican.plugins.signals` instead."
)

View file

@ -1,8 +0,0 @@
This is a test bad page
#######################
:status: invalid
The quick brown fox jumped over the lazy dog's back.
The status here is invalid, the page should not render.

View file

@ -1,8 +0,0 @@
This is a test draft page
##########################
:status: draft
The quick brown fox .
This page is a draft.

View file

@ -1,12 +0,0 @@
title: This is a markdown test draft page
status: draft
Test Markdown File Header
=========================
Used for pelican test
---------------------
The quick brown fox .
This page is a draft

View file

@ -1,11 +0,0 @@
This is a test draft page with a custom template
#################################################
:status: draft
:template: custom
The quick brown fox .
This page is a draft
This page has a custom template to be called when rendered

View file

@ -1,8 +0,0 @@
This is a test hidden page
##########################
:status: hidden
The quick brown fox jumped over the lazy dog's back.
This page is hidden

View file

@ -1,12 +0,0 @@
title: This is a markdown test hidden page
status: hidden
Test Markdown File Header
=========================
Used for pelican test
---------------------
The quick brown fox jumped over the lazy dog's back.
This page is hidden

View file

@ -1,11 +0,0 @@
This is a test hidden page with a custom template
#################################################
:status: hidden
:template: custom
The quick brown fox jumped over the lazy dog's back.
This page is hidden
This page has a custom template to be called when rendered

View file

@ -1,4 +0,0 @@
This is a test page
###################
The quick brown fox jumped over the lazy dog's back.

View file

@ -1,9 +0,0 @@
title: This is a markdown test page
Test Markdown File Header
=========================
Used for pelican test
---------------------
The quick brown fox jumped over the lazy dog's back.

View file

@ -1,6 +0,0 @@
A Page (Test) for sorting
#########################
:slug: zzzz
When using title, should be first. When using slug, should be last.

View file

@ -1,7 +0,0 @@
Title: Page with a bunch of links
My links:
[Link 1]({tag}マック)
[Link 2]({category}Yeah)

View file

@ -1,7 +0,0 @@
Title: Page with static links
My links:
[Link 0]({static}image0.jpg)
[Link 1]({attach}image1.jpg)

View file

@ -1,8 +0,0 @@
This is a test page with a preset template
##########################################
:template: custom
The quick brown fox jumped over the lazy dog's back.
This article has a custom template to be called when rendered

View file

@ -1,15 +0,0 @@
import logging
import warnings
from pelican.log import log_warnings
# redirect warnings module to use logging instead
log_warnings()
# setup warnings to log DeprecationWarning's and error on
# warnings in pelican's codebase
warnings.simplefilter("default", DeprecationWarning)
warnings.filterwarnings("error", ".*", Warning, "pelican")
# Add a NullHandler to silence warning about no available handlers
logging.getLogger().addHandler(logging.NullHandler())

View file

@ -1,7 +0,0 @@
def pytest_addoption(parser):
parser.addoption(
"--check-build",
action="store",
default=False,
help="Check wheel contents.",
)

View file

@ -1,69 +0,0 @@
import importlib.metadata
import tarfile
from pathlib import Path
from re import match
from zipfile import ZipFile
import pytest
version = importlib.metadata.version("pelican")
@pytest.mark.skipif(
"not config.getoption('--check-build')",
reason="Only run when --check-build is given",
)
def test_wheel_contents(pytestconfig):
"""
This test should test the contents of the wheel to make sure
that everything that is needed is included in the final build
"""
dist_folder = pytestconfig.getoption("--check-build")
wheels = Path(dist_folder).rglob(f"pelican-{version}-py3-none-any.whl")
for wheel_file in wheels:
files_list = ZipFile(wheel_file).namelist()
# Check if theme files are copied to wheel
simple_theme = Path("./pelican/themes/simple/templates")
for x in simple_theme.iterdir():
assert str(x) in files_list
# Check if tool templates are copied to wheel
tools = Path("./pelican/tools/templates")
for x in tools.iterdir():
assert str(x) in files_list
assert "pelican/tools/templates/tasks.py.jinja2" in files_list
@pytest.mark.skipif(
"not config.getoption('--check-build')",
reason="Only run when --check-build is given",
)
@pytest.mark.parametrize(
"expected_file",
[
("THANKS"),
("README.rst"),
("CONTRIBUTING.rst"),
("docs/changelog.rst"),
("samples/"),
],
)
def test_sdist_contents(pytestconfig, expected_file):
"""
This test should test the contents of the source distribution to make sure
that everything that is needed is included in the final build.
"""
dist_folder = pytestconfig.getoption("--check-build")
sdist_files = Path(dist_folder).rglob(f"pelican-{version}.tar.gz")
for dist in sdist_files:
files_list = tarfile.open(dist, "r:gz").getnames()
dir_matcher = ""
if expected_file.endswith("/"):
dir_matcher = ".*"
filtered_values = [
path
for path in files_list
if match(rf"^pelican-{version}/{expected_file}{dir_matcher}$", path)
]
assert len(filtered_values) > 0

View file

@ -1,6 +0,0 @@
Rst with filename metadata
##########################
:category: yeah
:author: Alexis Métaireau

View file

@ -1,5 +0,0 @@
category: yeah
author: Alexis Métaireau
Markdown with filename metadata
===============================

View file

@ -1,7 +0,0 @@
This is an article with category !
##################################
:category: yeah
:date: 1970-01-01
This article should be in 'yeah' category.

View file

@ -1,4 +0,0 @@
This is an article without category !
#####################################
This article should be in 'TestCategory' category.

View file

@ -1,6 +0,0 @@
Article title
#############
THIS is some content. With some stuff to "typogrify"...
Now with added support for :abbr:`TLA (three letter acronym)`.

View file

@ -1,5 +0,0 @@
Title: Draft article
Date: 2012-10-31
Status: draft
This is some content.

View file

@ -1,5 +0,0 @@
Title: Hidden article
Date: 2012-10-31
Status: hidden
This is some unlisted content.

View file

@ -1,5 +0,0 @@
Title: Skipped article
Date: 2024-06-30
Status: skip
This content will not be rendered.

View file

@ -1,11 +0,0 @@
<html>
<head>
</head>
<body>
Ensure that if an attribute value contains a double quote, it is
surrounded with single quotes, otherwise with double quotes.
<span data-test="'single quoted string'">Span content</span>
<span data-test='"double quoted string"'>Span content</span>
<span data-test="string without quotes">Span content</span>
</body>
</html>

View file

@ -1,16 +0,0 @@
This is a super article !
#########################
:TAGS: foo, bar, foobar
:DATE: 2010-12-02 10:14
:MODIFIED: 2010-12-02 10:20
:CATEGORY: yeah
:AUTHOR: Alexis Métaireau
:SUMMARY:
Multi-line metadata should be supported
as well as **inline markup** and stuff to "typogrify"...
:CUSTOM_FIELD: http://notmyidea.org
:CUSTOM_FORMATTED_FIELD:
Multi-line metadata should also be supported
as well as *inline markup* and stuff to "typogrify"...

View file

@ -1,15 +0,0 @@
An Article With Code Block To Test Typogrify Ignore
###################################################
An article with some code
.. code-block:: python
x & y
A block quote:
x & y
Normal:
x & y

View file

@ -1,8 +0,0 @@
<html>
<head>
</head>
<body>
Body content
<!-- This comment is included (including extra whitespace) -->
</body>
</html>

View file

@ -1,15 +0,0 @@
Title: Test metadata duplicates
Category: test
Tags: foo, bar, foobar, foo, bar
Authors: Author, First; Author, Second; Author, First
Date: 2010-12-02 10:14
Modified: 2010-12-02 10:20
Summary: I have a lot to test
Test Markdown File Header
=========================
Used for pelican test
---------------------
The quick brown fox jumped over the lazy dog's back.

View file

@ -1,17 +0,0 @@
<html>
<head>
<title>Article with an inline SVG</title>
</head>
<body>
Ensure that the title attribute in an inline svg is not handled as an HTML title.
<svg xmlns="http://www.w3.org/2000/svg" width="210mm" height="297mm" viewBox="0 0 210 297">
<path fill="#b2b2ff" stroke="#000" stroke-width="2.646" d="M88.698 89.869l-8.899 15.63a38.894 38.894 0 00-16.474 31.722 38.894 38.894 0 0038.894 38.894 38.894 38.894 0 0038.894-38.894 38.894 38.894 0 00-9-24.83l-2.38-16.886-14.828 4.994a38.894 38.894 0 00-12.13-2.144z">
<title>A different title inside the inline SVG</title>
</path>
<ellipse cx="100.806" cy="125.285" rx="3.704" ry="10.583"/>
<ellipse cx="82.021" cy="125.285" rx="3.704" ry="10.583"/>
<ellipse cx="-111.432" cy="146.563" rx="3.704" ry="10.583" transform="rotate(-64.822)"/>
<ellipse cx="-118.245" cy="91.308" rx="6.18" ry="8.62" transform="matrix(.063 -.99801 .96163 .27436 0 0)"/>
</svg>
</body>
</html>

View file

@ -1,6 +0,0 @@
<html>
<head>
<title>This is a super article !</title>
<meta name="keywords" content="foo, bar, foobar" />
</head>
</html>

View file

@ -1,4 +0,0 @@
Title: Article with markdown and empty tags
Tags:
This is some content.

View file

@ -1,15 +0,0 @@
Title: Article with markdown containing footnotes
Date: 2012-10-31
Modified: 2012-11-01
Summary: Summary with **inline** markup *should* be supported.
Multiline: Line Metadata should be handle properly.
See syntax of Meta-Data extension of Python Markdown package:
If a line is indented by 4 or more spaces,
that line is assumed to be an additional line of the value
for the previous keyword.
A keyword may have as many lines as desired.
This is some content[^1] with some footnotes[^footnote]
[^1]: Numbered footnote
[^footnote]: Named footnote

View file

@ -1,5 +0,0 @@
Title: Article with markdown and nested summary metadata
Date: 2012-10-30
Summary: Test: This metadata value looks like metadata
This is some content.

View file

@ -1,19 +0,0 @@
Title: マックOS X 10.8でパイソンとVirtualenvをインストールと設定
Slug: python-virtualenv-on-mac-osx-mountain-lion-10.8
Date: 2012-12-20
Modified: 2012-12-22
Tags: パイソン, マック
Category: 指導書
Summary: パイソンとVirtualenvをまっくでインストールする方法について明確に説明します。
Writing unicode is certainly fun.
パイソンとVirtualenvをまっくでインストールする方法について明確に説明します。
And let's mix languages.
первый пост
Now another.
İlk yazı çok özel değil.

View file

@ -1,10 +0,0 @@
Title: Article with markdown and summary metadata multi
Date: 2012-10-31
Summary:
A multi-line summary should be supported
as well as **inline markup**.
custom_formatted_field:
Multi-line metadata should also be supported
as well as *inline markup* and stuff to "typogrify"...
This is some content.

View file

@ -1,5 +0,0 @@
Title: Article with markdown and summary metadata single
Date: 2012-10-30
Summary: A single-line summary should be supported as well as **inline markup**.
This is some content.

View file

@ -1,10 +0,0 @@
title: Test markdown File
category: test
Test Markdown File Header
=========================
Used for pelican test
---------------------
This is another markdown test file. Uses the markdown extension.

View file

@ -1,7 +0,0 @@
Title: Test Markdown extensions
[TOC]
## Level1
### Level2

View file

@ -1,14 +0,0 @@
Title: Test md File
Category: test
Tags: foo, bar, foobar
Date: 2010-12-02 10:14
Modified: 2010-12-02 10:20
Summary: I have a lot to test
Test Markdown File Header
=========================
Used for pelican test
---------------------
The quick brown fox jumped over the lazy dog's back.

View file

@ -1,10 +0,0 @@
title: Test mdown File
category: test
Test Markdown File Header
=========================
Used for pelican test
---------------------
This is another markdown test file. Uses the mdown extension.

View file

@ -1,15 +0,0 @@
<html>
<head>
<title>This is a super article !</title>
<meta name="tags" content="foo, bar, foobar" />
<meta name="date" content="2010-12-02 10:14" />
<meta name="category" content="yeah" />
<meta name="author" content="Alexis Métaireau" />
<meta name="summary" content="Summary and stuff" />
<meta name="custom_field" content="http://notmyidea.org" />
</head>
<body>
Multi-line metadata should be supported
as well as <strong>inline markup</strong>.
</body>
</html>

View file

@ -1,16 +0,0 @@
This is a super article !
#########################
:tags: foo, bar, foobar
:date: 2010-12-02 10:14
:modified: 2010-12-02 10:20
:category: yeah
:author: Alexis Métaireau
:summary:
Multi-line metadata should be supported
as well as **inline markup** and stuff to "typogrify"...
:custom_field: http://notmyidea.org
:custom_formatted_field:
Multi-line metadata should also be supported
as well as *inline markup* and stuff to "typogrify"...

View file

@ -1,12 +0,0 @@
This is a super article !
#########################
:tags: foo, bar, foobar
:date: 2010-12-02 10:14
:category: yeah
:author: Alexis Métaireau
:summary:
Multi-line metadata should be supported
as well as **inline markup**.
:custom_field: http://notmyidea.org

View file

@ -1,15 +0,0 @@
<html>
<head>
<title>This is a super article !</title>
<meta name="tags" contents="foo, bar, foobar" />
<meta name="date" contents="2010-12-02 10:14" />
<meta name="category" contents="yeah" />
<meta name="author" contents="Alexis Métaireau" />
<meta name="summary" contents="Summary and stuff" />
<meta name="custom_field" contents="http://notmyidea.org" />
</head>
<body>
Multi-line metadata should be supported
as well as <strong>inline markup</strong>.
</body>
</html>

View file

@ -1,15 +0,0 @@
<html>
<head>
<title>This is a super article !</title>
<meta name="tags" content="foo, bar, foobar" />
<meta name="date" content="2010-12-02 10:14" />
<meta name="category" content="yeah" />
<meta name="author" content="Alexis Métaireau" />
<meta name="summary" content="Summary and stuff" />
<meta name="custom_field" content="http://notmyidea.org" />
</head>
<body>
Multi-line metadata should be supported
as well as <strong>inline markup</strong>.
</body>
</html>

View file

@ -1,16 +0,0 @@
<html>
<head>
<title>This is a super article !</title>
<meta name="tags" content="foo, bar, foobar" />
<meta name="date" content="2010-12-02 10:14" />
<meta name="modified" content="2010-12-31 23:59" />
<meta name="category" content="yeah" />
<meta name="author" content="Alexis Métaireau" />
<meta name="summary" content="Summary and stuff" />
<meta name="custom_field" content="http://notmyidea.org" />
</head>
<body>
Multi-line metadata should be supported
as well as <strong>inline markup</strong>.
</body>
</html>

View file

@ -1,15 +0,0 @@
<html>
<head>
<title>This is a super article !</title>
<meta name="tags" content="foo, bar, foobar" />
<meta name="modified" content="2010-12-02 10:14" />
<meta name="category" content="yeah" />
<meta name="author" content="Alexis Métaireau" />
<meta name="summary" content="Summary and stuff" />
<meta name="custom_field" content="http://notmyidea.org" />
</head>
<body>
Multi-line metadata should be supported
as well as <strong>inline markup</strong>.
</body>
</html>

View file

@ -1,14 +0,0 @@
<html>
<head>
<title>This is a super article !</title>
<meta name="tags" content="foo, bar, foobar" />
<meta name="category" content="yeah" />
<meta name="author" content="Alexis Métaireau" />
<meta name="summary" content="Summary and stuff" />
<meta name="custom_field" content="http://notmyidea.org" />
</head>
<body>
Multi-line metadata should be supported
as well as <strong>inline markup</strong>.
</body>
</html>

View file

@ -1,10 +0,0 @@
title: Test mkd File
category: test
Test Markdown File Header
=========================
Used for pelican test
---------------------
This is another markdown test file. Uses the mkd extension.

View file

@ -1,6 +0,0 @@
<html>
<head>
<title>This is an article with multiple authors!</title>
<meta name="authors" content="First Author, Second Author" />
</head>
</html>

View file

@ -1,6 +0,0 @@
This is an article with multiple authors!
#########################################
:date: 2014-02-09 02:20
:modified: 2014-02-09 02:20
:authors: First Author, Second Author

View file

@ -1,10 +0,0 @@
This is an article with multiple authors in list format!
########################################################
:date: 2014-02-09 02:20
:modified: 2014-02-09 02:20
:authors: - Author, First
- Author, Second
The author names are in last,first form to verify that
they are not just getting split on commas.

View file

@ -1,6 +0,0 @@
This is an article with multiple authors in lastname, firstname format!
#######################################################################
:date: 2014-02-09 02:20
:modified: 2014-02-09 02:20
:authors: Author, First; Author, Second

View file

@ -1,11 +0,0 @@
<html>
<head>
<title>Metadata tags as list!</title>
<meta name="custom_field" content="https://getpelican.com" />
<meta name="custom_field" content="https://www.eff.org" />
</head>
<body>
When custom metadata tags are specified more than once
they are collected into a list!
</body>
</html>

View file

@ -1,13 +0,0 @@
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Article with Nonconformant HTML meta tags</title>
<meta name="summary" content="Summary and stuff" />
</head>
<body>
Multi-line metadata should be supported
as well as <strong>inline markup</strong>.
</body>
</html>

View file

@ -1,8 +0,0 @@
<html>
<head>
</head>
<body>
Ensure that empty attributes are copied properly.
<input name="test" disabled style="" />
</body>
</html>

View file

@ -1,8 +0,0 @@
Article with template
#####################
:template: custom
This article has a custom template to be called when rendered
This is some content. With some stuff to "typogrify".

View file

@ -1,3 +0,0 @@
Title: One -, two --, three --- dashes!
One: -; Two: --; Three: ---

View file

@ -1,4 +0,0 @@
One -, two --, three --- dashes!
################################
One: -; Two: --; Three: ---

View file

@ -1,6 +0,0 @@
<html>
<head>
<title>This is a super article !</title>
<meta name="Category" content="Yeah" />
</head>
</html>

View file

@ -1,5 +0,0 @@
This is a super article !
#########################
:Category: Yeah

View file

@ -1,5 +0,0 @@
This is an article without category !
#####################################
This article should be in the DEFAULT_CATEGORY.

View file

@ -1,3 +0,0 @@
Title: Bad Extension
This file shouldn't be included because its file extension is `.mmd`.

File diff suppressed because it is too large Load diff

View file

@ -1 +0,0 @@


View file

@ -1,4 +0,0 @@
<hr/><h3>Title header</h3><p>A paragraph of content.</p><p>Paragraph number two.</p><p>A list:</p><ol><li>One.</li><li>Two.</li><li>Three.</li></ol><p>A link: <a data-href="https://example.com/example" href="https://example.com/example" target="_blank">link text</a>.</p><h3>Header 2</h3><p>A block quote:</p><blockquote>quote words <strong>strong words</strong></blockquote><p>after blockquote</p><figure><img data-height="282" data-image-id="image1.png" data-width="739" src="https://cdn-images-1.medium.com/max/800/image1.png"/><figcaption>A figure caption.</figcaption></figure><p>A final note: <a data-href="http://stats.stackexchange.com/" href="http://stats.stackexchange.com/" rel="noopener" target="_blank">Cross-Validated</a> has sometimes been helpful.</p><hr/><p><em>Next: </em><a data-href="https://medium.com/@username/post-url" href="https://medium.com/@username/post-url" target="_blank"><em>Next post</em>
</a></p>
<p>By <a href="https://medium.com/@username">User Name</a> on <a href="https://medium.com/p/medium-short-url"><time datetime="2017-04-21T17:11:55.799Z">April 21, 2017</time></a>.</p><p><a href="https://medium.com/@username/this-post-url">Canonical link</a></p><p>Exported from <a href="https://medium.com">Medium</a> on December 1, 2023.</p>

View file

@ -1,72 +0,0 @@
<!DOCTYPE html><html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"><title>A title</title><style>
* {
font-family: Georgia, Cambria, "Times New Roman", Times, serif;
}
html, body {
margin: 0;
padding: 0;
}
h1 {
font-size: 50px;
margin-bottom: 17px;
color: #333;
}
h2 {
font-size: 24px;
line-height: 1.6;
margin: 30px 0 0 0;
margin-bottom: 18px;
margin-top: 33px;
color: #333;
}
h3 {
font-size: 30px;
margin: 10px 0 20px 0;
color: #333;
}
header {
width: 640px;
margin: auto;
}
section {
width: 640px;
margin: auto;
}
section p {
margin-bottom: 27px;
font-size: 20px;
line-height: 1.6;
color: #333;
}
section img {
max-width: 640px;
}
footer {
padding: 0 20px;
margin: 50px 0;
text-align: center;
font-size: 12px;
}
.aspectRatioPlaceholder {
max-width: auto !important;
max-height: auto !important;
}
.aspectRatioPlaceholder-fill {
padding-bottom: 0 !important;
}
header,
section[data-field=subtitle],
section[data-field=description] {
display: none;
}
</style></head><body><article class="h-entry">
<header>
<h1 class="p-name">A name (like title)</h1>
</header>
<section data-field="subtitle" class="p-summary">
Summary (first several words of content)
</section>
<section data-field="body" class="e-content">
<section name="ad15" class="section section--body section--first"><div class="section-divider"><hr class="section-divider"></div><div class="section-content"><div class="section-inner sectionLayout--insetColumn"><h3 name="20a3" id="20a3" class="graf graf--h3 graf--leading graf--title">Title header</h3><p name="e3d6" id="e3d6" class="graf graf--p graf-after--h3">A paragraph of content.</p><p name="c7a8" id="c7a8" class="graf graf--p graf-after--p">Paragraph number two.</p><p name="42aa" id="42aa" class="graf graf--p graf-after--p">A list:</p><ol class="postList"><li name="d65f" id="d65f" class="graf graf--li graf-after--p">One.</li><li name="232b" id="232b" class="graf graf--li graf-after--li">Two.</li><li name="ef87" id="ef87" class="graf graf--li graf-after--li">Three.</li></ol><p name="e743" id="e743" class="graf graf--p graf-after--p">A link: <a href="https://example.com/example" data-href="https://example.com/example" class="markup--anchor markup--p-anchor" target="_blank">link text</a>.</p><h3 name="4cfd" id="4cfd" class="graf graf--h3 graf-after--p">Header 2</h3><p name="433c" id="433c" class="graf graf--p graf-after--p">A block quote:</p><blockquote name="3537" id="3537" class="graf graf--blockquote graf-after--p">quote words <strong class="markup--strong markup--blockquote-strong">strong words</strong></blockquote><p name="00cc" id="00cc" class="graf graf--p graf-after--blockquote">after blockquote</p><figure name="edb0" id="edb0" class="graf graf--figure graf-after--p"><img class="graf-image" data-image-id="image1.png" data-width="739" data-height="282" src="https://cdn-images-1.medium.com/max/800/image1.png"><figcaption class="imageCaption">A figure caption.</figcaption></figure><p name="f401" id="f401" class="graf graf--p graf-after--p graf--trailing">A final note: <a href="http://stats.stackexchange.com/" data-href="http://stats.stackexchange.com/" class="markup--anchor markup--p-anchor" rel="noopener" target="_blank">Cross-Validated</a> has sometimes been helpful.</p></div></div></section><section name="09a3" class="section section--body section--last"><div class="section-divider"><hr class="section-divider"></div><div class="section-content"><div class="section-inner sectionLayout--insetColumn"><p name="81e8" id="81e8" class="graf graf--p graf--leading"><em class="markup--em markup--p-em">Next: </em><a href="https://medium.com/@username/post-url" data-href="https://medium.com/@username/post-url" class="markup--anchor markup--p-anchor" target="_blank"><em class="markup--em markup--p-em">Next post</em>
</section>
<footer><p>By <a href="https://medium.com/@username" class="p-author h-card">User Name</a> on <a href="https://medium.com/p/medium-short-url"><time class="dt-published" datetime="2017-04-21T17:11:55.799Z">April 21, 2017</time></a>.</p><p><a href="https://medium.com/@username/this-post-url" class="p-canonical">Canonical link</a></p><p>Exported from <a href="https://medium.com">Medium</a> on December 1, 2023.</p></footer></article></body></html>

View file

@ -1,48 +0,0 @@
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p><object width="425" height="350"><param name="movie" value="http://www.youtube.com/v/XSrW-wAWZe4"></param><param name="wmode" value="transparent"></param><embed src="http://www.youtube.com/v/XSrW-wAWZe4" type="application/x-shockwave-flash" wmode="transparent" width="425" height="350"></embed></object></p>
<blockquote><p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p></blockquote>
<ul>
<li>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</li>
<li>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</li>
</ul>
<pre>
<code>
a = [1, 2, 3]
b = [4, 5, 6]
for i in zip(a, b):
print i
</code>
</pre>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>

View file

@ -1,54 +0,0 @@
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
<object width="425" height="350"><param name="movie" value="http://www.youtube.com/v/XSrW-wAWZe4"></param><param name="wmode" value="transparent"></param><embed src="http://www.youtube.com/v/XSrW-wAWZe4" type="application/x-shockwave-flash" wmode="transparent" width="425" height="350"></embed></object>
<blockquote>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</blockquote>
<ul>
<li>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</li>
<li>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</li>
</ul>
<pre>
<code>
a = [1, 2, 3]
b = [4, 5, 6]
for i in zip(a, b):
print i
</code>
</pre>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

File diff suppressed because it is too large Load diff

View file

@ -1,7 +0,0 @@
First article
#############
:date: 2018-11-10
:summary: Here's the `second <{filename}/second-article.rst>`_,
`third <{filename}/third-article.rst>`_ and a
`nonexistent article <{filename}/nonexistent.rst>`_.

View file

@ -1,7 +0,0 @@
Second article
##############
:date: 2018-11-10
:summary: Here's the `first <{filename}/first-article.rst>`_,
`third <{filename}/third-article.rst>`_ and a
`nonexistent article <{filename}/nonexistent.rst>`_.

View file

@ -1,7 +0,0 @@
Third article
#############
:date: 2018-11-10
:summary: Here's the `first <{filename}/first-article.rst>`_,
`second <{filename}/second-article.rst>`_ and a
`nonexistent article <{filename}/nonexistent.rst>`_.

View file

@ -1,48 +0,0 @@
AUTHOR = "Alexis Métaireau"
SITENAME = "Alexis' log"
SITEURL = "http://blog.notmyidea.org"
TIMEZONE = "UTC"
GITHUB_URL = "http://github.com/ametaireau/"
DISQUS_SITENAME = "blog-notmyidea"
PDF_GENERATOR = False
REVERSE_CATEGORY_ORDER = True
DEFAULT_PAGINATION = 2
FEED_RSS = "feeds/all.rss.xml"
CATEGORY_FEED_RSS = "feeds/{slug}.rss.xml"
LINKS = (
("Biologeek", "http://biologeek.org"),
("Filyb", "http://filyb.info/"),
("Libert-fr", "http://www.libert-fr.com"),
("N1k0", "http://prendreuncafe.com/blog/"),
("Tarek Ziadé", "http://ziade.org/blog"),
("Zubin Mithra", "http://zubin71.wordpress.com/"),
)
SOCIAL = (
("twitter", "http://twitter.com/ametaireau"),
("lastfm", "http://lastfm.com/user/akounet"),
("github", "http://github.com/ametaireau"),
)
# global metadata to all the contents
DEFAULT_METADATA = {"yeah": "it is"}
# path-specific metadata
EXTRA_PATH_METADATA = {
"extra/robots.txt": {"path": "robots.txt"},
}
# static paths will be copied without parsing their contents
STATIC_PATHS = [
"pictures",
"extra/robots.txt",
]
FORMATTED_FIELDS = ["summary", "custom_formatted_field"]
# foobar will not be used, because it's not in caps. All configuration keys
# have to be in caps
foobar = "barbaz"

View file

@ -1,5 +0,0 @@
NAME = "namespace plugin"
def register():
pass

View file

@ -1,5 +0,0 @@
from .submodule import noop # noqa: F401
def register():
pass

View file

@ -1,2 +0,0 @@
def noop():
pass

Some files were not shown because too many files have changed in this diff Show more