diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 00000000..2cb24879
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,3 @@
+[report]
+omit = pelican/tests/*
+
diff --git a/.editorconfig b/.editorconfig
index f1ec6c3d..b42ca8c2 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -3,13 +3,13 @@ root = true
[*]
charset = utf-8
end_of_line = lf
-indent_size = 2
+indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.py]
-max_line_length = 88
+max_line_length = 79
-[*.{yml,yaml}]
+[*.yml]
indent_size = 2
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..9053428d
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,7 @@
+# Auto detect text files and perform LF normalization
+* text=auto
+
+# Improve accuracy of GitHub's Linguist-powered language statistics
+pelican/tests/content/* linguist-vendored
+pelican/tests/output/* linguist-vendored
+samples/* linguist-vendored
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 00000000..7e0d5359
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,2 @@
+custom: https://donate.getpelican.com
+liberapay: pelican
diff --git a/.github/ISSUE_TEMPLATE/---bug-report.md b/.github/ISSUE_TEMPLATE/---bug-report.md
new file mode 100644
index 00000000..c0f9dec6
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/---bug-report.md
@@ -0,0 +1,40 @@
+---
+name: "\U0001F41E Bug Report"
+about: Did you find a bug?
+title: ''
+labels: bug
+assignees: ''
+
+---
+
+
+
+
+- [ ] I have read the [Filing Issues](https://docs.getpelican.com/en/latest/contribute.html#filing-issues) and subsequent “How to Get Help” sections of the documentation.
+- [ ] I have searched the [issues](https://github.com/getpelican/pelican/issues?q=is%3Aissue) (including closed ones) and believe that this is not a duplicate.
+
+
+
+- **OS version and name**:
+- **Python version**:
+- **Pelican version**:
+- **Link to theme**:
+- **Links to plugins**:
+- **Link to your site**:
+- **Link to your source**:
+- **Link to a [Gist](https://gist.github.com/) with the contents of your settings file**:
+
+## Issue
+
diff --git a/.github/ISSUE_TEMPLATE/---documentation.md b/.github/ISSUE_TEMPLATE/---documentation.md
new file mode 100644
index 00000000..03055b3a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/---documentation.md
@@ -0,0 +1,22 @@
+---
+name: "\U0001F4DA Documentation"
+about: Did you find errors, problems, or anything unclear in the docs (https://docs.getpelican.com/)?
+title: ''
+labels: docs
+assignees: ''
+
+---
+
+
+
+
+- [ ] I have searched the [issues](https://github.com/getpelican/pelican/issues?q=is%3Aissue) (including closed ones) and believe that this is not a duplicate.
+
+## Issue
+
diff --git a/.github/ISSUE_TEMPLATE/---enhancement-request.md b/.github/ISSUE_TEMPLATE/---enhancement-request.md
new file mode 100644
index 00000000..c9d545a7
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/---enhancement-request.md
@@ -0,0 +1,24 @@
+---
+name: "\U0001F381 Feature Request"
+about: Do you have ideas for new features and improvements?
+title: ''
+labels: enhancement
+assignees: ''
+
+---
+
+
+
+
+- [ ] I have searched the [issues](https://github.com/getpelican/pelican/issues?q=is%3Aissue) (including closed ones) and believe that this is not a duplicate.
+- [ ] I have searched the [documentation](https://docs.getpelican.com/) and believe that my question is not covered.
+- [ ] I am willing to lend a hand to help implement this feature.
+
+## Feature Request
+
diff --git a/.github/ISSUE_TEMPLATE/---everything-else.md b/.github/ISSUE_TEMPLATE/---everything-else.md
new file mode 100644
index 00000000..fd2a58ae
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/---everything-else.md
@@ -0,0 +1,23 @@
+---
+name: "\U0001F5C3 Everything Else"
+about: Do you have a question/issue that does not fall into any of the other categories?
+title: ''
+labels: question
+assignees: ''
+
+---
+
+
+
+
+- [ ] I have searched the [issues](https://github.com/getpelican/pelican/issues?q=is%3Aissue) (including closed ones) and believe that this is not a duplicate.
+- [ ] I have searched the [documentation](https://docs.getpelican.com/) and believe that my question is not covered.
+
+## Issue
+
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 00000000..d080329a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,7 @@
+# Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser
+blank_issues_enabled: true
+contact_links:
+- name: '💬 Pelican IRC Channel on Freenode'
+ url: https://kiwiirc.com/client/irc.freenode.net/?#pelican
+ about: |
+ Chat with the community, ask questions, and learn about best practices.
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 00000000..8300fb74
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,12 @@
+# Pull Request Checklist
+
+Resolves: #issue-number-here
+
+
+
+- [ ] Ensured **tests pass** and (if applicable) updated functional test output
+- [ ] Conformed to **code style guidelines** by running appropriate linting tools
+- [ ] Added **tests** for changed code
+- [ ] Updated **documentation** for changed code
+
+
diff --git a/.github/stale.yml b/.github/stale.yml
new file mode 100644
index 00000000..b6991ba2
--- /dev/null
+++ b/.github/stale.yml
@@ -0,0 +1,31 @@
+# Configuration for probot-stale - https://github.com/probot/stale
+
+# Number of days of inactivity before an Issue or Pull Request becomes stale
+daysUntilStale: 60
+
+# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
+daysUntilClose: 30
+
+# Set to true to ignore issues in a project (defaults to false)
+exemptProjects: true
+
+# Set to true to ignore issues in a milestone (defaults to false)
+exemptMilestones: true
+
+# Set to true to ignore issues with an assignee (defaults to false)
+exemptAssignees: true
+
+# Label to use when marking as stale
+staleLabel: stale
+
+# Comment to post when marking as stale. Set to `false` to disable
+markComment: >
+ This issue has been automatically marked as stale because it has not had
+ recent activity. It will be closed if no further activity occurs. Thank you
+ for your participation and understanding.
+
+# Limit the number of actions per hour, from 1-30. Default is 30
+limitPerRun: 1
+
+# Limit to only `issues` or `pulls`
+only: issues
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 00000000..4b81530f
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,211 @@
+name: build
+
+on: [push, pull_request]
+
+env:
+ # color output for pytest and tox
+ PYTEST_ADDOPTS: "--color=yes"
+ PY_COLORS: 1
+
+jobs:
+ test:
+ name: Test - ${{ matrix.config.python }} - ${{ matrix.config.os }}
+ runs-on: ${{ matrix.config.os }}-latest
+
+ strategy:
+ matrix:
+ config:
+ - os: ubuntu
+ python: 3.5
+ - os: ubuntu
+ python: 3.6
+ - os: ubuntu
+ python: 3.7
+ - os: ubuntu
+ python: 3.8
+ - os: macos
+ python: 3.7
+ - os: windows
+ python: 3.7
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup Python ${{ matrix.config.python }}
+ uses: actions/setup-python@v1.1.1
+ with:
+ python-version: ${{ matrix.config.python }}
+ - name: Set pip cache (Linux)
+ uses: actions/cache@v1
+ if: startsWith(runner.os, 'Linux')
+ with:
+ path: ~/.cache/pip
+ key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements/*') }}
+ restore-keys: |
+ ${{ runner.os }}-pip-
+ - name: Setup pip cache (macOS)
+ uses: actions/cache@v1
+ if: startsWith(runner.os, 'macOS')
+ with:
+ path: ~/Library/Caches/pip
+ key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements/*') }}
+ restore-keys: |
+ ${{ runner.os }}-pip-
+ - name: Setup pip cache (Windows)
+ uses: actions/cache@v1
+ if: startsWith(runner.os, 'Windows')
+ with:
+ path: ~\AppData\Local\pip\Cache
+ key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements/*') }}
+ restore-keys: |
+ ${{ runner.os }}-pip-
+ - name: Install locale (Linux)
+ if: startsWith(runner.os, 'Linux')
+ run: sudo locale-gen fr_FR.UTF-8 tr_TR.UTF-8
+ - name: Install pandoc
+ uses: r-lib/actions/setup-pandoc@v1
+ with:
+ pandoc-version: "2.9.2"
+ - name: Install tox
+ run: |
+ python -m pip install -U pip
+ python -m pip install --use-feature=2020-resolver -U tox
+ - name: Info
+ run: |
+ echo "===== PYTHON ====="
+ python --version
+ echo "===== PANDOC ====="
+ pandoc --version | head -2
+ - name: Run tests
+ run: tox -e py${{ matrix.config.python }}
+
+
+ lint:
+ name: Lint
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup Python
+ uses: actions/setup-python@v1.1.1
+ with:
+ python-version: 3.6
+ - name: Set pip cache (Linux)
+ uses: actions/cache@v1
+ if: startsWith(runner.os, 'Linux')
+ with:
+ path: ~/.cache/pip
+ key: ${{ runner.os }}-pip-${{ hashFiles('requirements/*') }}
+ restore-keys: |
+ ${{ runner.os }}-pip-
+ - name: Install tox
+ run: |
+ python -m pip install -U pip
+ python -m pip install --use-feature=2020-resolver -U tox
+ - name: Check
+ run: tox -e flake8
+
+
+ docs:
+ name: Build docs
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup Python
+ uses: actions/setup-python@v1.1.1
+ with:
+ python-version: 3.6
+ - name: Set pip cache (Linux)
+ uses: actions/cache@v1
+ if: startsWith(runner.os, 'Linux')
+ with:
+ path: ~/.cache/pip
+ key: ${{ runner.os }}-pip-${{ hashFiles('requirements/*') }}
+ restore-keys: |
+ ${{ runner.os }}-pip-
+ - name: Install tox
+ run: |
+ python -m pip install -U pip
+ python -m pip install --use-feature=2020-resolver -U tox
+ - name: Check
+ run: tox -e docs
+
+
+ install_sdist:
+ name: Install sdist on Windows
+ runs-on: windows-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup Python
+ uses: actions/setup-python@v1.1.1
+ with:
+ python-version: 3.6
+ - name: Test sdist installation on Windows
+ run: |
+ python -m pip install -U pip
+ python -m pip install --use-feature=2020-resolver -U poetry
+ poetry build
+ echo "Pip version:"
+ python -m pip --version
+ python -m pip install --use-feature=2020-resolver ${env:GITHUB_WORKSPACE}\dist\pelican-4.2.0.tar.gz
+ echo "Pelican version:"
+ pelican --version
+ python -m pip uninstall -y pelican
+
+
+ install_wheel:
+ name: Install wheel on Windows
+ runs-on: windows-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup Python
+ uses: actions/setup-python@v1.1.1
+ with:
+ python-version: 3.6
+ - name: Test wheel installation on Windows
+ run: |
+ python -m pip install -U pip
+ python -m pip install --use-feature=2020-resolver -U poetry
+ poetry build
+ echo "Pip version:"
+ python -m pip --version
+ python -m pip install --use-feature=2020-resolver ${env:GITHUB_WORKSPACE}\dist\pelican-4.2.0-py3-none-any.whl
+ echo "Pelican version:"
+ pelican --version
+ python -m pip uninstall -y pelican
+
+
+ deploy:
+ name: Deploy
+ needs: [test, lint, docs]
+ runs-on: ubuntu-latest
+ if: ${{ github.ref=='refs/heads/master' && github.event_name!='pull_request' }}
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup Python
+ uses: actions/setup-python@v1.1.1
+ with:
+ python-version: 3.7
+ - name: Check release
+ id: check_release
+ run: |
+ python -m pip install --use-feature=2020-resolver pip --upgrade
+ pip install --use-feature=2020-resolver poetry
+ pip install --use-feature=2020-resolver githubrelease
+ pip install --use-feature=2020-resolver --pre autopub
+ echo "##[set-output name=release;]$(autopub check)"
+ - name: Publish
+ if: ${{ steps.check_release.outputs.release=='' }}
+ env:
+ GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
+ PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
+ run: |
+ git remote set-url origin https://$GITHUB_TOKEN@github.com/${{ github.repository }}
+ autopub prepare
+ poetry build
+ autopub commit
+ autopub githubrelease
+ poetry publish -u __token__ -p $PYPI_PASSWORD
diff --git a/.gitignore b/.gitignore
index c2658d7d..b94526d6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,18 @@
-node_modules/
+*.egg-info
+.*.swp
+.*.swo
+*.pyc
+.DS_Store
+docs/_build
+docs/fr/_build
+build
+dist
+tags
+.tox
+.coverage
+htmlcov
+*.orig
+venv
+samples/output
+*.pem
+poetry.lock
diff --git a/.mailmap b/.mailmap
new file mode 100644
index 00000000..8dd40fb6
--- /dev/null
+++ b/.mailmap
@@ -0,0 +1,24 @@
+Alexis Métaireau
+Alexis Métaireau
+Alexis Métaireau
+Axel Haustant
+Axel Haustant
+Dave Mankoff
+Feth Arezki
+Guillaume
+Guillaume
+Guillaume B
+Guillermo López
+Guillermo López
+Jomel Imperio
+Justin Mayer
+Justin Mayer
+Marco Milanesi
+Massimo Santini
+Rémy HUBSCHER
+Simon Conseil
+Simon Liedtke
+Skami18
+Stuart Colville
+Stéphane Bunel
+tBunnyMan
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 00000000..579d3c45
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,20 @@
+# See https://pre-commit.com/hooks.html for info on hooks
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v2.5.0
+ hooks:
+ - id: check-added-large-files
+ - id: check-ast
+ - id: check-toml
+ - id: check-yaml
+ - id: debug-statements
+ - id: detect-private-key
+ - id: end-of-file-fixer
+ - id: trailing-whitespace
+ - id: flake8
+ name: Flake8 on commit diff
+ description: This hook limits Flake8 checks to changed lines of code.
+ entry: bash
+ args: [-c, 'git diff HEAD | flake8 --diff --max-line-length=88']
+
+exclude: ^pelican/tests/output/
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 00000000..c0637a7f
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,53 @@
+language: python
+python:
+ - "3.6"
+env:
+ global:
+ - PYPI_USERNAME=autopub
+ - secure: "h5V/+YL+CrqvfAesNkSb824Ngk5x+f0eFzj/LBbmnzjvArKAmc6R6WGyx8SDD7WF/PlaTf0M1fH3a7pjIS8Ee+TS1Rb0Lt1HPqUs1yntg1+Js2ZQp3p20wfsDc+bZ4/2g8xLsSMv1EJ4np7/GJ5fXqpSxjr/Xs5LYA7ZLwNNwDw="
+ - secure: "GiDFfmjH7uzYNnkjQMV/mIkbRdmgkGmtbFPeaj9taBNA5tPp3IBt3GOOS6UL/zm9xiwu9Xo6sxZWkGzY19Hsdv28YPH34N3abo0QSnz4IGiHs152Hi7Qi6Tb0QkT5D3OxuSIm8LmFL7+su89Q7vBFowrT6HL1Mn8CDDWSj3eqbo="
+ - TWINE_USERNAME=$PYPI_USERNAME
+ - TWINE_PASSWORD=$PYPI_PASSWORD
+ matrix:
+ - TOX_ENV=docs
+ - TOX_ENV=flake8
+ - TOX_ENV=py3.5
+ - TOX_ENV=py3.6
+matrix:
+ include:
+ - python: 3.7
+ sudo: true
+ dist: xenial
+ env:
+ - TOX_ENV=py3.7
+addons:
+ apt_packages:
+ - pandoc
+before_install:
+ - sudo apt-get update -qq
+ - sudo locale-gen fr_FR.UTF-8 tr_TR.UTF-8
+install:
+ - pip install tox==2.5.0
+script: tox -e $TOX_ENV
+before_deploy:
+ - 'if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then travis_terminate 0; fi'
+ - pip install githubrelease
+ - pip install --pre autopub
+ - autopub check || travis_terminate 0
+ - pip install poetry
+ - pip install twine
+ - git checkout ${TRAVIS_BRANCH}
+ - git remote set-url origin https://$GITHUB_TOKEN@github.com/$TRAVIS_REPO_SLUG
+deploy:
+ provider: script
+ script: autopub deploy
+ skip_cleanup: true
+ on:
+ branch: master
+ python: "3.7"
+# The channel name "irc.freenode.org#pelican" is encrypted against getpelican/pelican to prevent IRC spam of forks
+notifications:
+ irc:
+ channels:
+ - secure: "JP57f61QovrhmLoAF6oPOzIK2aXGfSO06FHg7yiuKBOWMiaxQejZUGJX919muCLhWJXDugsviIqCMoAWwNV3o1WQbqIr+G5TR+N9MrtCs4Zi6vpGj09bR8giKUKx+PPKEoe1Ew56E4y2LxzGO4Lj9hZx8M2YVdwPNWrWZgp6WXE="
+ on_success: change
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
new file mode 100644
index 00000000..61935f62
--- /dev/null
+++ b/CONTRIBUTING.rst
@@ -0,0 +1,158 @@
+Filing issues
+=============
+
+* Before you file an issue, try `asking for help`_ first.
+* If determined to file an issue, first check for `existing issues`_, including
+ closed issues.
+
+.. _`asking for help`: `How to get help`_
+.. _`existing issues`: https://github.com/getpelican/pelican/issues
+
+How to get help
+===============
+
+Before you ask for help, please make sure you do the following:
+
+1. Read the documentation_ thoroughly. If in a hurry, at least use the search
+ field that is provided at top-left on the documentation_ pages. Make sure
+ you read the docs for the Pelican version you are using.
+2. Use a search engine (e.g., DuckDuckGo, Google) to search for a solution to
+ your problem. Someone may have already found a solution, perhaps in the
+ form of a plugin_ or a specific combination of settings.
+
+3. Try reproducing the issue in a clean environment, ensuring you are using:
+
+* latest Pelican release (or an up-to-date Git clone of Pelican master)
+* latest releases of libraries used by Pelican
+* no plugins or only those related to the issue
+
+**NOTE:** The most common sources of problems are anomalies in (1) themes,
+(2) settings files, and (3) ``make``/``invoke`` automation wrappers. If you can't
+reproduce your problem when using the following steps to generate your site,
+then the problem is almost certainly with your chosen theme and/or settings
+file (and not Pelican itself)::
+
+ cd ~/projects/your-site
+ git clone https://github.com/getpelican/pelican ~/projects/pelican
+ pelican content -s ~/projects/pelican/samples/pelican.conf.py -t ~/projects/pelican/pelican/themes/notmyidea
+
+If despite the above efforts you still cannot resolve your problem, be sure to
+include in your inquiry the following information, preferably in the form of
+links to content uploaded to a `paste service`_, GitHub repository, or other
+publicly-accessible location:
+
+* Describe what version of Pelican you are running (output of ``pelican --version``
+ or the HEAD commit hash if you cloned the repo) and how exactly you installed
+ it (the full command you used, e.g. ``python -m pip install pelican``).
+* If you are looking for a way to get some end result, prepare a detailed
+ description of what the end result should look like (preferably in the form of
+ an image or a mock-up page) and explain in detail what you have done so far to
+ achieve it.
+* If you are trying to solve some issue, prepare a detailed description of how
+ to reproduce the problem. If the issue cannot be easily reproduced, it cannot
+ be debugged by developers or volunteers. Describe only the **minimum steps**
+ necessary to reproduce it (no extra plugins, etc.).
+* Upload your settings file or any other custom code that would enable people to
+ reproduce the problem or to see what you have already tried to achieve the
+ desired end result.
+* Upload detailed and **complete** output logs and backtraces (remember to add
+ the ``--debug`` flag: ``pelican --debug content [...]``)
+
+.. _documentation: https://docs.getpelican.com/
+.. _`paste service`: https://dpaste.de/
+
+Once the above preparation is ready, you can contact people willing to help via
+(preferably) the ``#pelican`` IRC channel or send a message to ``authors at getpelican dot com``.
+Remember to include all the information you prepared.
+
+The #pelican IRC channel
+------------------------
+
+* Because of differing time zones, you may not get an immediate response to your
+ question, but please be patient and stay logged into IRC — someone will almost
+ always respond if you wait long enough (it may take a few hours).
+* If you don't have an IRC client handy, use the webchat_.
+* You can direct your IRC client to the channel using this `IRC link`_ or you
+ can manually join the ``#pelican`` IRC channel on the `freenode IRC network`_.
+
+.. _webchat: https://kiwiirc.com/client/irc.freenode.net/?#pelican
+.. _`IRC link`: irc://irc.freenode.net/pelican
+.. _`freenode IRC network`: https://freenode.net/
+
+
+Contributing code
+=================
+
+Before you submit a contribution, please ask whether it is desired so that you
+don't spend a lot of time working on something that would be rejected for a
+known reason. Consider also whether your new feature might be better suited as
+a plugin_ — you can `ask for help`_ to make that determination.
+
+Using Git and GitHub
+--------------------
+
+* `Create a new git branch`_ specific to your change (as opposed to making
+ your commits in the master branch).
+* **Don't put multiple unrelated fixes/features in the same branch / pull request.**
+ For example, if you're working on a new feature and find a bugfix that
+ doesn't *require* your new feature, **make a new distinct branch and pull
+ request** for the bugfix.
+* Add a ``RELEASE.md`` file in the root of the project that contains the
+ release type (major, minor, patch) and a summary of the changes that will be
+ used as the release changelog entry. For example::
+
+ Release type: minor
+
+ Reload browser window upon changes to content, settings, or theme
+
+* Check for unnecessary whitespace via ``git diff --check`` before committing.
+* First line of your commit message should start with present-tense verb, be 50
+ characters or less, and include the relevant issue number(s) if applicable.
+ *Example:* ``Ensure proper PLUGIN_PATH behavior. Refs #428.`` If the commit
+ *completely fixes* an existing bug report, please use ``Fixes #585`` or ``Fix
+ #585`` syntax (so the relevant issue is automatically closed upon PR merge).
+* After the first line of the commit message, add a blank line and then a more
+ detailed explanation (when relevant).
+* `Squash your commits`_ to eliminate merge commits and ensure a clean and
+ readable commit history.
+* If you have previously filed a GitHub issue and want to contribute code that
+ addresses that issue, **please use** ``hub pull-request`` instead of using
+ GitHub's web UI to submit the pull request. This isn't an absolute
+ requirement, but makes the maintainers' lives much easier! Specifically:
+ `install hub `_ and then run
+ `hub pull-request -i [ISSUE] `_
+ to turn your GitHub issue into a pull request containing your code.
+* After you have issued a pull request, the continuous integration (CI) system
+ will run the test suite for all supported Python versions and check for PEP8
+ compliance. If any of these checks fail, you should fix them. (If tests fail
+ on the CI system but seem to pass locally, ensure that local test runs aren't
+ skipping any tests.)
+
+Contribution quality standards
+------------------------------
+
+* Adhere to `PEP8 coding standards`_. This can be eased via the `pycodestyle
+ `_ or `flake8
+ `_ tools, the latter of which in
+ particular will give you some useful hints about ways in which the
+ code/formatting can be improved. We try to keep line length within the
+ 79-character maximum specified by PEP8. Because that can sometimes compromise
+ readability, the hard/enforced maximum is 88 characters.
+* Ensure your code is compatible with the `officially-supported Python releases`_.
+* Add docs and tests for your changes. Undocumented and untested features will
+ not be accepted.
+* `Run all the tests`_ **on all versions of Python supported by Pelican** to
+ ensure nothing was accidentally broken.
+
+Check out our `Git Tips`_ page or `ask for help`_ if you
+need assistance or have any questions about these guidelines.
+
+.. _`plugin`: https://docs.getpelican.com/en/latest/plugins.html
+.. _`#pelican IRC channel`: https://webchat.freenode.net/?channels=pelican&uio=d4
+.. _`Create a new git branch`: https://github.com/getpelican/pelican/wiki/Git-Tips#making-your-changes
+.. _`Squash your commits`: https://github.com/getpelican/pelican/wiki/Git-Tips#squashing-commits
+.. _`Run all the tests`: https://docs.getpelican.com/en/latest/contribute.html#running-the-test-suite
+.. _`Git Tips`: https://github.com/getpelican/pelican/wiki/Git-Tips
+.. _`PEP8 coding standards`: https://www.python.org/dev/peps/pep-0008/
+.. _`ask for help`: `How to get help`_
+.. _`officially-supported Python releases`: https://devguide.python.org/#status-of-python-branches
diff --git a/LICENSE b/LICENSE
index 789896df..c03a4e46 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,20 +1,661 @@
-MIT License
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
-Copyright (c) 2024 Oliver Ladner
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files (the "Software"), to deal in
-the Software without restriction, including without limitation the rights to
-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
-the Software, and to permit persons to whom the Software is furnished to do so,
-subject to the following conditions:
+ Preamble
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
-FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
-COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
-IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
-CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+ .
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 00000000..138c8f00
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,3 @@
+include *.rst
+recursive-include pelican *.html *.css *png *.rst *.markdown *.md *.mkd *.xml *.py
+include LICENSE THANKS docs/changelog.rst pyproject.toml
diff --git a/README.md b/README.md
deleted file mode 100644
index f3fe9563..00000000
--- a/README.md
+++ /dev/null
@@ -1,43 +0,0 @@
-# lugh Pelican theme
-
-This theme is based on the [simple theme](https://github.com/getpelican/pelican/tree/main/pelican/themes/simple/templates).
-It's heavily customized to what I need here, so no efforts have been made to
-keep it useful for others. Amongst other things, I:
-
-- removed translations
-- changed the structure (HTML `` etc.)
-
-## Docs
-
-- [Pelican: how to create your own theme](https://docs.getpelican.com/en/stable/themes.html)
-- [Tailwind CSS quick start](https://tailwindcss.com/docs/installation)
-
-## Doing
-
-### Prepare Pelican development server config
-
-Adapt Pelican's `publishconf.py` for local development.
-E.g. `RELATIVE_URLS = False`
-
-### Install Tailwind CSS Typography plugin
-
-Typography enables sane defaults for longer texts. In this case, we use it for
-the body content only, which is always Markdown. Typography is a bit of a beast
-to configure/align to standard Tailwind.
-
-```shell
-npm install -D @tailwindcss/typography
-```
-
-### Run the Tailwind build process
-
-```shell
-npx tailwindcss -i static/css/in.css -o static/css/out.css --watch
-```
-
-### Run Pelican dev server
-
-```shell
-conda activate pelican
-./devserver.sh
-```
diff --git a/README.rst b/README.rst
new file mode 100644
index 00000000..dee92234
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,67 @@
+Pelican |build-status| |pypi-version| |repology|
+=====================================
+
+Pelican is a static site generator, written in Python_.
+
+* Write content in reStructuredText_ or Markdown_ using your editor of choice
+* Includes a simple command line tool to (re)generate site files
+* Easy to interface with version control systems and web hooks
+* Completely static output is simple to host anywhere
+
+
+Features
+--------
+
+Pelican currently supports:
+
+* Chronological content (e.g., articles, blog posts) as well as static pages
+* Integration with external services (e.g., Google Analytics and Disqus)
+* Site themes (created using Jinja2_ templates)
+* Publication of articles in multiple languages
+* Generation of Atom and RSS feeds
+* Syntax highlighting via Pygments_
+* Importing existing content from WordPress, Dotclear, and other services
+* Fast rebuild times due to content caching and selective output writing
+
+Check out `Pelican's documentation`_ for further information.
+
+
+How to get help, contribute, or provide feedback
+------------------------------------------------
+
+See our `contribution submission and feedback guidelines `_.
+
+
+Source code
+-----------
+
+Pelican's source code is `hosted on GitHub`_. If you feel like hacking,
+take a look at `Pelican's internals`_.
+
+
+Why the name "Pelican"?
+-----------------------
+
+"Pelican" is an anagram of *calepin*, which means "notebook" in French.
+
+
+.. Links
+
+.. _Python: https://www.python.org/
+.. _reStructuredText: http://docutils.sourceforge.net/rst.html
+.. _Markdown: https://daringfireball.net/projects/markdown/
+.. _Jinja2: https://palletsprojects.com/p/jinja/
+.. _Pygments: https://pygments.org/
+.. _`Pelican's documentation`: https://docs.getpelican.com/
+.. _`Pelican's internals`: https://docs.getpelican.com/en/latest/internals.html
+.. _`hosted on GitHub`: https://github.com/getpelican/pelican
+
+.. |build-status| image:: https://img.shields.io/github/workflow/status/getpelican/pelican/build
+ :target: https://github.com/getpelican/pelican/actions
+ :alt: GitHub Actions CI: continuous integration status
+.. |pypi-version| image:: https://img.shields.io/pypi/v/pelican.svg
+ :target: https://pypi.python.org/pypi/pelican
+ :alt: PyPI: the Python Package Index
+.. |repology| image:: https://repology.org/badge/tiny-repos/pelican.svg
+ :target: https://repology.org/project/pelican/versions
+ :alt: Repology: the packaging hub
diff --git a/THANKS b/THANKS
new file mode 100644
index 00000000..fd9f030e
--- /dev/null
+++ b/THANKS
@@ -0,0 +1,165 @@
+Pelican is a project originally created by Alexis Métaireau
+ and subsequently maintained by Justin Mayer
+ , but there are a large number of people that have
+contributed or implemented key features over time. We do our best to keep this
+list up-to-date, but you can also have a look at the nice contributor graphs
+produced by GitHub: https://github.com/getpelican/pelican/graphs/contributors
+
+If you want to contribute, check the documentation section about how to do so:
+
+
+Aaron Kavlie
+Abhishek L
+Albrecht Mühlenschulte
+Aldiantoro Nugroho
+Alen Mujezinovic
+Alessandro Martin
+Alexander Artemenko
+Alexandre RODIERE
+Alexis Daboville
+Alexis Métaireau
+Allan Whatmough
+Andrea Crotti
+Andrew Laski
+Andrew Spiers
+Arnaud BOS
+asselinpaul
+Axel Haustant
+Ben Rosser (TC01)
+Benoît HERVIER
+Bernhard Scheirle
+Borgar
+Brandon W Maister
+Brendan Wholihan
+Brian C. Lane
+Brian Hsu
+Brian St. Pierre
+Bruno Binet
+BunnyMan
+Chenguang Wang
+Chris Elston
+Chris McDonald (Wraithan)
+Chris Streeter
+Christophe Chauvet
+Clint Howarth
+Colin Dunklau
+Dafydd Crosby
+Dana Woodman
+Dave King
+Dave Mankoff
+David Beitey
+David Marble
+Deniz Turgut (Avaris)
+derdon
+Dirkjan Ochtman
+Dirk Makowski
+draftcode
+Edward Delaporte
+Emily Strickland
+epatters
+Eric Case
+Erik Hetzner
+FELD Boris
+Feth Arezki
+Florian Jacob
+Florian Preinstorfer
+Félix Delval
+Freeculture
+George V. Reilly
+Guillaume
+Guillaume B
+Guillermo López
+guillermooo
+Ian Cordasco
+Igor Kalnitsky
+Irfan Ahmad
+Iuri de Silvio
+Ivan Dyedov
+James King
+James Rowe
+jawher
+Jered Boxman
+Jerome
+Jiachen Yang
+Jochen Breuer
+joe di castro
+John Kristensen
+John Mastro
+Jökull Sólberg Auðunsson
+Jomel Imperio
+Joseph Reagle
+Joshua Adelman
+Julian Berman
+Justin Mayer
+Kevin Deldycke
+Kevin Yap
+Kyle Fuller
+Laureline Guerin
+Leonard Huang
+Leroy Jiang
+Lucas Cimon
+Marcel Hellkamp
+Marco Milanesi
+Marcus Fredriksson
+Mario Rodas
+Mark Caudill
+Martin Brochhaus
+Massimo Santini
+Matt Bowcock
+Matt Layman
+Meir Kriheli
+Michael Guntsche
+Michael Reneer
+Michael Yanovich
+Mike Yumatov
+Mikhail Korobov
+m-r-r
+mviera
+Nico Di Rocco
+Nicolas Duhamel
+Nicolas Perriault
+Nicolas Steinmetz
+Paolo Melchiorre
+Paul Asselin
+Pavel Puchkin
+Perry Roper
+Peter Desmet
+Philippe Pepiot
+Rachid Belaid
+Randall Degges
+Ranjhith Kalisamy
+Remi Rampin
+Rémy HUBSCHER
+renhbo
+Richard Duivenvoorde
+Rogdham
+Roman Skvazh
+Ronny Pfannschmidt
+Rory McCann
+Rıdvan Örsvuran
+saghul
+sam
+Samrat Man Singh
+Simon Conseil
+Simon Liedtke
+Skami18
+solsTiCe d'Hiver
+Steve Schwarz
+Stéphane Bunel
+Stéphane Raimbault
+Stuart Colville
+Talha Mansoor
+Tarek Ziade
+Thanos Lefteris
+Thomas Thurman
+Tobias
+Tomi Pieviläinen
+Trae Blain
+Tshepang Lekhonkhobe
+Valentin-Costel Hăloiu
+Vlad Niculae
+William Light
+William Minchin
+Wladislaw Merezhko
+W. Trevor King
+Zoresvit
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 00000000..bf49b542
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,130 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = _build
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
+
+help:
+ @echo "Please use \`make ' where is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+ -rm -rf $(BUILDDIR)/*
+
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Raclette.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Raclette.qhc"
+
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/Raclette"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Raclette"
+ @echo "# devhelp"
+
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ make -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
diff --git a/docs/_static/overall.png b/docs/_static/overall.png
new file mode 100644
index 00000000..d70641f0
Binary files /dev/null and b/docs/_static/overall.png differ
diff --git a/docs/_static/theme-basic.zip b/docs/_static/theme-basic.zip
new file mode 100644
index 00000000..d1e4754a
Binary files /dev/null and b/docs/_static/theme-basic.zip differ
diff --git a/docs/_static/theme_overrides.css b/docs/_static/theme_overrides.css
new file mode 100644
index 00000000..83afc78e
--- /dev/null
+++ b/docs/_static/theme_overrides.css
@@ -0,0 +1,12 @@
+
+/* override table width restrictions */
+.wy-table-responsive table td, .wy-table-responsive table th {
+ /* !important prevents the common CSS stylesheets from
+ overriding this as on RTD they are loaded after this stylesheet */
+ white-space: normal !important;
+}
+
+.wy-table-responsive {
+ overflow: visible !important;
+}
+
diff --git a/docs/_static/uml.jpg b/docs/_static/uml.jpg
new file mode 100644
index 00000000..03655d7e
Binary files /dev/null and b/docs/_static/uml.jpg differ
diff --git a/docs/changelog.rst b/docs/changelog.rst
new file mode 100644
index 00000000..febc5322
--- /dev/null
+++ b/docs/changelog.rst
@@ -0,0 +1,431 @@
+Release history
+###############
+
+4.2.0 - 2019-10-17
+==================
+
+* Support inline SVGs; don't treat titles in SVGs as HTML titles
+* Add category to feeds (in addition to tags)
+* Improve content metadata field docs
+* Add docs for including other Markdown/reST files in content
+
+4.1.3 - 2019-10-09
+==================
+
+* Fix quick-start docs regarding `pelican --listen`
+* Set default listen address to 127.0.0.1
+* Add extra/optional Markdown dependency to setup.py
+* Use correct SSH port syntax for rsync in tasks.py
+* Place all deprecated settings handling together
+* Add related project URLs for display on PyPI
+* Skip some tests on Windows that can't pass due to filesystem differences
+
+4.1.2 - 2019-09-23
+==================
+
+Fix pelican.settings.load_source to avoid caching issues - PR #2621
+
+4.1.1 - 2019-08-23
+==================
+
+* Add AutoPub to auto-publish releases on PR merge
+* Add CSS classes for reStructuredText figures
+* Pass `argv` to Pelican `main` entrypoint
+* Set default content status to a blank string rather than `None`
+
+4.1.0 - 2019-07-14
+==================
+
+* Live browser reload upon changed files (provided via Invoke task)
+* Add ``pyproject.toml``, managed by Poetry
+* Support for invoking ``python -m pelican``
+* Add relative source path attribute to content
+* Allow directories in ``EXTRA_PATH_METADATA``
+* Add ``all_articles`` variable to period pages (for recent posts functionality)
+* Improve debug mode output
+* Remove blank or duplicate summaries from Atom feed
+* Fix bugs in pagination, pelican-import, pelican-quickstart, and feed importer
+
+4.0.1 (2018-11-30)
+==================
+
+* Refactor ``pelican.server`` logging
+* Fix bug in which all static files were processed as "draft"
+* Bug fixes for Invoke/Makefile automation, Importer, and other miscellanea
+
+If upgrading from 3.7.x or earlier, please note that slug-related settings in
+4.0+ use ``{slug}`` and/or ``{lang}`` rather than ``%s``. If ``%s``-style
+settings are encountered, Pelican will emit a warning and fall back to the
+default setting. Some user-submitted themes might try to format setting values
+but fail upon site build with a ``TypeError``. In such cases, the theme needs
+to be updated. For example, instead of ``TAG_FEED_ATOM|format(tag.slug)``, use
+``TAG_FEED_ATOM.format(slug=tag.slug)``
+
+4.0.0 (2018-11-13)
+==================
+
+* Replace ``develop_server.sh`` script with ``pelican --listen``
+* Improved copy/link behavior for large static files (e.g., videos)
+* New ``{static}`` syntax to link to static content; content linked to by
+ ``{static}`` and ``{attach}`` is automatically copied over even if not in
+ ``STATIC_PATHS``
+* Pages can now have ``draft`` status
+* Show current settings via new ``--print-settings`` flag
+* All settings for slugs now use ``{slug}`` and/or ``{lang}`` rather than
+ ``%s``. If ``%s``-style settings are encountered, Pelican will emit a warning
+ and fallback to the default setting.
+* New signals: ``feed_generated`` and ``page_generated_write_page``
+* Replace Fabric with Invoke and ``fabfile.py`` template with ``tasks.py``
+* Replace ``PAGINATED_DIRECT_TEMPLATES`` by ``PAGINATED_TEMPLATES``, extending
+ control over pagination to all templates and making page size variable
+* Replace ``SLUG_SUBSTITUTIONS`` (and friends) by ``SLUG_REGEX_SUBSTITUTIONS``
+ for more finegrained control
+* ``'{base_name}'`` value in ``PAGINATION_PATTERNS`` setting no longer strips
+ ``'bar'`` from ``'foo/bar.html'`` (unless ``'bar' == 'index'``).
+* ``ARTICLE_ORDER_BY`` and ``PAGE_ORDER_BY`` now also affect 1) category, tag
+ and author pages 2) feeds 3) draft and hidden articles and pages
+* New ``ARTICLE_TRANSLATION_ID`` and ``PAGE_TRANSLATION_ID`` settings to
+ specify metadata attributes used to identify/disable translations
+* Make the HTML reader parse multiple occurrences of metadata tags as a list
+* New Blogger XML backup importer
+* Wordpress importer now updates file links to point to local copies if the
+ files were downloaded with ``--wp-attach``.
+* Importer no longer inserts extra newlines, to prevent breaking of HTML
+ attributes.
+* Pelican server now prioritises ``foo.html`` and ``foo/index.html`` over
+ ``foo/`` when resolving ``foo``.
+
+3.7.1 (2017-01-10)
+==================
+
+* Fix locale issues in Quickstart script
+* Specify encoding for README and CHANGELOG in setup.py
+
+3.7.0 (2016-12-12)
+==================
+
+* Atom feeds output ```` in addition to ````
+* Atom feeds use ```` for the original publication date and
+ ```` for modifications
+* Simplify Atom feed ID generation and support URL fragments
+* Produce category feeds with category-specific titles
+* RSS feeds now default to summary instead of full content;
+ set ``RSS_FEED_SUMMARY_ONLY = False`` to revert to previous behavior
+* Replace ``MD_EXTENSIONS`` with ``MARKDOWN`` setting
+* Replace ``JINJA_EXTENSIONS`` with more-robust ``JINJA_ENVIRONMENT`` setting
+* Improve summary truncation logic to handle special characters and tags that
+ span multiple lines, using HTML parser instead of regular expressions
+* Include summary when looking for intra-site link substitutions
+* Link to authors and index via ``{author}name`` and ``{index}`` syntax
+* Override widget names via ``LINKS_WIDGET_NAME`` and ``SOCIAL_WIDGET_NAME``
+* Add ``INDEX_SAVE_AS`` option to override default ``index.html`` value
+* Remove ``PAGES`` context variable for themes in favor of ``pages``
+* ``SLUG_SUBSTITUTIONS`` now accepts 3-tuple elements, allowing URL slugs to
+ contain non-alphanumeric characters
+* Tag and category slugs can be controlled with greater precision using the
+ ``TAG_SUBSTITUTIONS`` and ``CATEGORY_SUBSTITUTIONS`` settings
+* Author slugs can be controlled with greater precision using the
+ ``AUTHOR_SUBSTITUTIONS`` setting
+* ``DEFAULT_DATE`` can be defined as a string
+* Use ``mtime`` instead of ``ctime`` when ``DEFAULT_DATE = 'fs'``
+* Add ``--fatal=errors|warnings`` option for use with continuous integration
+* When using generator-level caching, ensure previously-cached files are
+ processed instead of just new files.
+* Add Python and Pelican version information to debug output
+* Improve compatibility with Python 3.5
+* Comply with and enforce PEP8 guidelines
+* Replace tables in settings documentation with ``data::`` directives
+
+3.6.3 (2015-08-14)
+==================
+
+* Fix permissions issue in release tarball
+
+3.6.2 (2015-08-01)
+==================
+
+* Fix installation errors related to Unicode in tests
+* Don't show pagination in ``notmyidea`` theme if there's only one page
+* Make hidden pages available in context
+* Improve URLWrapper comparison
+
+3.6.0 (2015-06-15)
+==================
+
+* Disable caching by default in order to prevent potential confusion
+* Improve caching behavior, replacing ``pickle`` with ``cpickle``
+* Allow Markdown or reST content in metadata fields other than ``summary``
+* Support semicolon-separated author/tag lists
+* Improve flexibility of article sorting
+* Add ``--relative-urls`` argument
+* Support devserver listening on addresses other than localhost
+* Unify HTTP server handlers to ``pelican.server`` throughout
+* Handle intra-site links to draft posts
+* Move ``tag_cloud`` from core to plugin
+* Load default theme's external resources via HTTPS
+* Import drafts from WordPress XML
+* Improve support for Windows users
+* Enhance logging and test suite
+* Clean up and refactor codebase
+* New signals: ``all_generators_finalized`` and ``page_writer_finalized``
+
+3.5.0 (2014-11-04)
+==================
+
+* Introduce ``ARTICLE_ORDER_BY`` and ``PAGE_ORDER_BY`` settings to control the
+ order of articles and pages.
+* Include time zone information in dates rendered in templates.
+* Expose the reader name in the metadata for articles and pages.
+* Add the ability to store static files along with content in the same
+ directory as articles and pages using ``{attach}`` in the path.
+* Prevent Pelican from raising an exception when there are duplicate pieces of
+ metadata in a Markdown file.
+* Introduce the ``TYPOGRIFY_IGNORE_TAGS`` setting to add HTML tags to be
+ ignored by Typogrify.
+* Add the ability to use ``-`` in date formats to strip leading zeros. For
+ example, ``%-d/%-m/%y`` will now result in the date ``9/8/12``.
+* Ensure feed generation is correctly disabled during quickstart configuration.
+* Fix ``PAGE_EXCLUDES`` and ``ARTICLE_EXCLUDES`` from incorrectly matching
+ sub-directories.
+* Introduce ``STATIC_EXCLUDE`` setting to add static file excludes.
+* Fix an issue when using ``PAGINATION_PATTERNS`` while ``RELATIVE_URLS``
+ is enabled.
+* Fix feed generation causing links to use the wrong language for month
+ names when using other locales.
+* Fix an issue where the authors list in the simple template wasn't correctly
+ formatted.
+* Fix an issue when parsing non-string URLs from settings.
+* Improve consistency of debug and warning messages.
+
+3.4.0 (2014-07-01)
+==================
+
+* Speed up content generation via new caching mechanism
+* Add selective post generation (instead of always building entire site)
+* Many documentation improvements, including switching to prettier RtD theme
+* Add support for multiple content and plugin paths
+* Add ``:modified:`` metadata field to complement ``:date:``.
+ Used to specify the last date and time an article was updated independently
+ from the date and time it was published.
+* Add support for multiple authors via new ``:authors:`` metadata field
+* Watch for changes in static directories when in auto-regeneration mode
+* Add filters to limit log output when desired
+* Add language support to drafts
+* Add ``SLUGIFY_SOURCE`` setting to control how post slugs are generated
+* Fix many issues relating to locale and encoding
+* Apply Typogrify filter to post summary
+* Preserve file metadata (e.g. time stamps) when copying static files to output
+* Move AsciiDoc support from Pelican core into separate plugin
+* Produce inline links instead of reference-style links when importing content
+* Improve handling of ``IGNORE_FILES`` setting behavior
+* Properly escape symbol characters in tag names (e.g., ``C++``)
+* Minor tweaks for Python 3.4 compatibility
+* Add several new signals
+
+3.3.0 (2013-09-24)
+==================
+
+* Drop Python 3.2 support in favor of Python 3.3
+* Add ``Fabfile`` so Fabric can be used for workflow automation instead of Make
+* ``OUTPUT_RETENTION`` setting can be used to preserve metadata (e.g., VCS
+ data such as ``.hg`` and ``.git``) from being removed from output directory
+* Tumblr import
+* Improve logic and consistency when cleaning output folder
+* Improve documentation versioning and release automation
+* Improve pagination flexibility
+* Rename signals for better consistency (some plugins may need to be updated)
+* Move metadata extraction from generators to readers; metadata extraction no
+ longer article-specific
+* Deprecate ``FILES_TO_COPY`` in favor of ``STATIC_PATHS`` and
+ ``EXTRA_PATH_METADATA``
+* Summaries in Markdown posts no longer include footnotes
+* Remove unnecessary whitespace in output via ``lstrip_blocks`` Jinja parameter
+* Move PDF generation from core to plugin
+* Replace ``MARKUP`` setting with ``READERS``
+* Add warning if img tag is missing ``alt`` attribute
+* Add support for ``{}`` in relative links syntax, besides ``||``
+* Add support for ``{tag}`` and ``{category}`` relative links
+* Add a ``content_written`` signal
+
+3.2.1 and 3.2.2
+===============
+
+* Facilitate inclusion in FreeBSD Ports Collection
+
+3.2 (2013-04-24)
+================
+
+* Support for Python 3!
+* Override page save-to location from meta-data (enables using a static page as
+ the site's home page, for example)
+* Time period archives (per-year, per-month, and per-day archives of posts)
+* Posterous blog import
+* Improve WordPress blog import
+* Migrate plugins to separate repository
+* Improve HTML parser
+* Provide ability to show or hide categories from menu using
+ ``DISPLAY_CATEGORIES_ON_MENU`` option
+* Auto-regeneration can be told to ignore files via ``IGNORE_FILES`` setting
+* Improve post-generation feedback to user
+* For multilingual posts, use meta-data to designate which is the original
+ and which is the translation
+* Add ``.mdown`` to list of supported Markdown file extensions
+* Document-relative URL generation (``RELATIVE_URLS``) is now off by default
+
+3.1 (2012-12-04)
+================
+
+* Importer now stores slugs within files by default. This can be disabled with
+ the ``--disable-slugs`` option.
+* Improve handling of links to intra-site resources
+* Ensure WordPress import adds paragraphs for all types of line endings
+ in post content
+* Decode HTML entities within WordPress post titles on import
+* Improve appearance of LinkedIn icon in default theme
+* Add GitHub and Google+ social icons support in default theme
+* Optimize social icons
+* Add ``FEED_ALL_ATOM`` and ``FEED_ALL_RSS`` to generate feeds containing all
+ posts regardless of their language
+* Split ``TRANSLATION_FEED`` into ``TRANSLATION_FEED_ATOM`` and
+ ``TRANSLATION_FEED_RSS``
+* Different feeds can now be enabled/disabled individually
+* Allow for blank author: if ``AUTHOR`` setting is not set, author won't
+ default to ``${USER}`` anymore, and a post won't contain any author
+ information if the post author is empty
+* Move LESS and Webassets support from Pelican core to plugin
+* The ``DEFAULT_DATE`` setting now defaults to ``None``, which means that
+ articles won't be generated unless date metadata is specified
+* Add ``FILENAME_METADATA`` setting to support metadata extraction from
+ filename
+* Add ``gzip_cache`` plugin to compress common text files into a ``.gz``
+ file within the same directory as the original file, preventing the server
+ (e.g. Nginx) from having to compress files during an HTTP call
+* Add support for AsciiDoc-formatted content
+* Add ``USE_FOLDER_AS_CATEGORY`` setting so that feature can be toggled on/off
+* Support arbitrary Jinja template files
+* Restore basic functional tests
+* New signals: ``generator_init``, ``get_generators``, and
+ ``article_generate_preread``
+
+3.0 (2012-08-08)
+================
+
+* Refactored the way URLs are handled
+* Improved the English documentation
+* Fixed packaging using ``setuptools`` entrypoints
+* Added ``typogrify`` support
+* Added a way to disable feed generation
+* Added support for ``DIRECT_TEMPLATES``
+* Allow multiple extensions for content files
+* Added LESS support
+* Improved the import script
+* Added functional tests
+* Rsync support in the generated Makefile
+* Improved feed support (easily pluggable with Feedburner for instance)
+* Added support for ``abbr`` in reST
+* Fixed a bunch of bugs :-)
+
+2.8 (2012-02-28)
+==================
+
+* Dotclear importer
+* Allow the usage of Markdown extensions
+* Themes are now easily extensible
+* Don't output pagination information if there is only one page
+* Add a page per author, with all their articles
+* Improved the test suite
+* Made the themes easier to extend
+* Removed Skribit support
+* Added a ``pelican-quickstart`` script
+* Fixed timezone-related issues
+* Added some scripts for Windows support
+* Date can be specified in seconds
+* Never fail when generating posts (skip and continue)
+* Allow the use of future dates
+* Support having different timezones per language
+* Enhanced the documentation
+
+2.7 (2011-06-11)
+==================
+
+* Use ``logging`` rather than echoing to stdout
+* Support custom Jinja filters
+* Compatibility with Python 2.5
+* Added a theme manager
+* Packaged for Debian
+* Added draft support
+
+2.6 (2011-03-08)
+==================
+
+* Changes in the output directory structure
+* Makes templates easier to work with / create
+* Added RSS support (was Atom-only)
+* Added tag support for the feeds
+* Enhance the documentation
+* Added another theme (brownstone)
+* Added translations
+* Added a way to use cleaner URLs with a rewrite url module (or equivalent)
+* Added a tag cloud
+* Added an autoreloading feature: the blog is automatically regenerated each
+ time a modification is detected
+* Translate the documentation into French
+* Import a blog from an RSS feed
+* Pagination support
+* Added Skribit support
+
+2.5 (2010-11-20)
+==================
+
+* Import from WordPress
+* Added some new themes (martyalchin / wide-notmyidea)
+* First bug report!
+* Linkedin support
+* Added a FAQ
+* Google Analytics support
+* Twitter support
+* Use relative URLs, not static ones
+
+2.4 (2010-11-06)
+================
+
+* Minor themes changes
+* Add Disqus support (so we have comments)
+* Another code refactoring
+* Added config settings about pages
+* Blog entries can also be generated in PDF
+
+2.3 (2010-10-31)
+================
+
+* Markdown support
+
+2.2 (2010-10-30)
+================
+
+* Prettify output
+* Manages static pages as well
+
+2.1 (2010-10-30)
+================
+
+* Make notmyidea the default theme
+
+2.0 (2010-10-30)
+================
+
+* Refactoring to be more extensible
+* Change into the setting variables
+
+1.2 (2010-09-28)
+================
+
+* Added a debug option
+* Added per-category feeds
+* Use filesystem to get dates if no metadata is provided
+* Add Pygments support
+
+1.1 (2010-08-19)
+================
+
+* First working version
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 00000000..fc49975a
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,81 @@
+import os
+import sys
+
+from pelican import __version__
+
+on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
+
+sys.path.append(os.path.abspath(os.pardir))
+
+# -- General configuration ----------------------------------------------------
+templates_path = ['_templates']
+extensions = ['sphinx.ext.autodoc',
+ 'sphinx.ext.ifconfig',
+ 'sphinx.ext.extlinks']
+source_suffix = '.rst'
+master_doc = 'index'
+project = 'Pelican'
+copyright = '2010 – present, Justin Mayer, Alexis Metaireau, and contributors'
+exclude_patterns = ['_build']
+release = __version__
+version = '.'.join(release.split('.')[:1])
+last_stable = __version__
+rst_prolog = '''
+.. |last_stable| replace:: :pelican-doc:`{}`
+'''.format(last_stable)
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+extlinks = {
+ 'pelican-doc': ('https://docs.getpelican.com/%s/', '')
+}
+
+# -- Options for HTML output --------------------------------------------------
+
+html_theme = 'default'
+if not on_rtd:
+ try:
+ import sphinx_rtd_theme
+ html_theme = 'sphinx_rtd_theme'
+ html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
+ except ImportError:
+ pass
+
+html_static_path = ['_static']
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'Pelicandoc'
+
+html_use_smartypants = True
+
+# If false, no module index is generated.
+html_use_modindex = False
+
+# If false, no index is generated.
+html_use_index = False
+
+# If true, links to the reST sources are added to the pages.
+html_show_sourcelink = False
+
+
+def setup(app):
+ # overrides for wide tables in RTD theme
+ app.add_css_file('theme_overrides.css') # path relative to _static
+
+
+# -- Options for LaTeX output -------------------------------------------------
+latex_documents = [
+ ('index', 'Pelican.tex', 'Pelican Documentation', 'Justin Mayer',
+ 'manual'),
+]
+
+# -- Options for manual page output -------------------------------------------
+man_pages = [
+ ('index', 'pelican', 'pelican documentation',
+ ['Justin Mayer'], 1),
+ ('pelican-themes', 'pelican-themes', 'A theme manager for Pelican',
+ ['Mickaël Raybaud'], 1),
+ ('themes', 'pelican-theming', 'How to create themes for Pelican',
+ ['The Pelican contributors'], 1)
+]
diff --git a/docs/content.rst b/docs/content.rst
new file mode 100644
index 00000000..48c851ab
--- /dev/null
+++ b/docs/content.rst
@@ -0,0 +1,625 @@
+Writing content
+###############
+
+Articles and pages
+==================
+
+Pelican considers "articles" to be chronological content, such as posts on a
+blog, and thus associated with a date.
+
+The idea behind "pages" is that they are usually not temporal in nature and are
+used for content that does not change very often (e.g., "About" or "Contact"
+pages).
+
+You can find sample content in the repository at ``samples/content/``.
+
+.. _internal_metadata:
+
+File metadata
+=============
+
+Pelican tries to be smart enough to get the information it needs from the
+file system (for instance, about the category of your articles), but some
+information you need to provide in the form of metadata inside your files.
+
+If you are writing your content in reStructuredText format, you can provide
+this metadata in text files via the following syntax (give your file the
+``.rst`` extension)::
+
+ My super title
+ ##############
+
+ :date: 2010-10-03 10:20
+ :modified: 2010-10-04 18:40
+ :tags: thats, awesome
+ :category: yeah
+ :slug: my-super-post
+ :authors: Alexis Metaireau, Conan Doyle
+ :summary: Short version for index and feeds
+
+Author and tag lists may be semicolon-separated instead, which allows
+you to write authors and tags containing commas::
+
+ :tags: pelican, publishing tool; pelican, bird
+ :authors: Metaireau, Alexis; Doyle, Conan
+
+Pelican implements an extension to reStructuredText to enable support for the
+``abbr`` HTML tag. To use it, write something like this in your post::
+
+ This will be turned into :abbr:`HTML (HyperText Markup Language)`.
+
+You can also use Markdown syntax (with a file ending in ``.md``, ``.markdown``,
+``.mkd``, or ``.mdown``). Markdown generation requires that you first
+explicitly install the Python-Markdown_ package, which can be done via ``pip
+install Markdown``.
+
+Pelican also supports `Markdown Extensions`_, which might have to be installed
+separately if they are not included in the default ``Markdown`` package and can
+be configured and loaded via the ``MARKDOWN`` setting.
+
+Metadata syntax for Markdown posts should follow this pattern::
+
+ Title: My super title
+ Date: 2010-12-03 10:20
+ Modified: 2010-12-05 19:30
+ Category: Python
+ Tags: pelican, publishing
+ Slug: my-super-post
+ Authors: Alexis Metaireau, Conan Doyle
+ Summary: Short version for index and feeds
+
+ This is the content of my super blog post.
+
+You can also have your own metadata keys (so long as they don't conflict with
+reserved metadata keywords) for use in your templates. The following table
+contains a list of reserved metadata keywords:
+
+=============== ===============================================================
+ Metadata Description
+=============== ===============================================================
+``title`` Title of the article or page
+``date`` Publication date (e.g., ``YYYY-MM-DD HH:SS``)
+``modified`` Modification date (e.g., ``YYYY-MM-DD HH:SS``)
+``tags`` Content tags, separated by commas
+``keywords`` Content keywords, separated by commas (HTML content only)
+``category`` Content category (one only — not multiple)
+``slug`` Identifier used in URLs and translations
+``author`` Content author, when there is only one
+``authors`` Content authors, when there are multiple
+``summary`` Brief description of content for index pages
+``lang`` Content language ID (``en``, ``fr``, etc.)
+``translation`` If content is a translation of another (``true`` or ``false``)
+``status`` Content status: ``draft``, ``hidden``, or ``published``
+``template`` Name of template to use to generate content (without extension)
+``save_as`` Save content to this relative file path
+``url`` URL to use for this article/page
+=============== ===============================================================
+
+Readers for additional formats (such as AsciiDoc_) are available via plugins.
+Refer to `pelican-plugins`_ repository for those.
+
+Pelican can also process HTML files ending in ``.html`` and ``.htm``. Pelican
+interprets the HTML in a very straightforward manner, reading metadata from
+``meta`` tags, the title from the ``title`` tag, and the body out from the
+``body`` tag::
+
+
+
+ My super title
+
+
+
+
+
+
+
+
+ This is the content of my super blog post.
+
+
+
+With HTML, there is one simple exception to the standard metadata: tags can be
+specified either via the ``tags`` metadata, as is standard in Pelican, or via
+the ``keywords`` metadata, as is standard in HTML. The two can be used
+interchangeably.
+
+Note that, aside from the title, none of this content metadata is mandatory:
+if the date is not specified and ``DEFAULT_DATE`` is set to ``'fs'``, Pelican
+will rely on the file's "mtime" timestamp, and the category can be determined
+by the directory in which the file resides. For example, a file located at
+``python/foobar/myfoobar.rst`` will have a category of ``foobar``. If you would
+like to organize your files in other ways where the name of the subfolder would
+not be a good category name, you can set the setting ``USE_FOLDER_AS_CATEGORY``
+to ``False``. When parsing dates given in the page metadata, Pelican supports
+the W3C's `suggested subset ISO 8601`__.
+
+So the title is the only required metadata. If that bothers you, worry not.
+Instead of manually specifying a title in your metadata each time, you can use
+the source content file name as the title. For example, a Markdown source file
+named ``Publishing via Pelican.md`` would automatically be assigned a title of
+*Publishing via Pelican*. If you would prefer this behavior, add the following
+line to your settings file::
+
+ FILENAME_METADATA = '(?P.*)'
+
+.. note::
+
+ When experimenting with different settings (especially the metadata
+ ones) caching may interfere and the changes may not be visible. In
+ such cases disable caching with ``LOAD_CONTENT_CACHE = False`` or
+ use the ``--ignore-cache`` command-line switch.
+
+__ `W3C ISO 8601`_
+
+``modified`` should be last time you updated the article, and defaults to
+``date`` if not specified. Besides you can show ``modified`` in the templates,
+feed entries in feed readers will be updated automatically when you set
+``modified`` to the current date after you modified your article.
+
+``authors`` is a comma-separated list of article authors. If there's only one
+author you can use ``author`` field.
+
+If you do not explicitly specify summary metadata for a given post, the
+``SUMMARY_MAX_LENGTH`` setting can be used to specify how many words from the
+beginning of an article are used as the summary.
+
+You can also extract any metadata from the filename through a regular
+expression to be set in the ``FILENAME_METADATA`` setting. All named groups
+that are matched will be set in the metadata object. The default value for the
+``FILENAME_METADATA`` setting will only extract the date from the filename. For
+example, if you would like to extract both the date and the slug, you could set
+something like: ``'(?P\d{4}-\d{2}-\d{2})_(?P.*)'``
+
+Please note that the metadata available inside your files takes precedence over
+the metadata extracted from the filename.
+
+Pages
+=====
+
+If you create a folder named ``pages`` inside the content folder, all the
+files in it will be used to generate static pages, such as **About** or
+**Contact** pages. (See example filesystem layout below.)
+
+You can use the ``DISPLAY_PAGES_ON_MENU`` setting to control whether all those
+pages are displayed in the primary navigation menu. (Default is ``True``.)
+
+If you want to exclude any pages from being linked to or listed in the menu
+then add a ``status: hidden`` attribute to its metadata. This is useful for
+things like making error pages that fit the generated theme of your site.
+
+Static content
+==============
+
+Static files are files other than articles and pages that are copied to the
+output folder as-is, without processing. You can control which static files
+are copied over with the ``STATIC_PATHS`` setting of the project's
+``pelicanconf.py`` file. Pelican's default configuration includes the
+``images`` directory for this, but others must be added manually. In addition,
+static files that are explicitly linked to are included (see below).
+
+Mixed content in the same directory
+-----------------------------------
+
+Starting with Pelican 3.5, static files can safely share a source directory
+with page source files, without exposing the page sources in the generated
+site. Any such directory must be added to both ``STATIC_PATHS`` and
+``PAGE_PATHS`` (or ``STATIC_PATHS`` and ``ARTICLE_PATHS``). Pelican will
+identify and process the page source files normally, and copy the remaining
+files as if they lived in a separate directory reserved for static files.
+
+Note: Placing static and content source files together in the same source
+directory does not guarantee that they will end up in the same place in the
+generated site. The easiest way to do this is by using the ``{attach}`` link
+syntax (described below). Alternatively, the ``STATIC_SAVE_AS``,
+``PAGE_SAVE_AS``, and ``ARTICLE_SAVE_AS`` settings (and the corresponding
+``*_URL`` settings) can be configured to place files of different types
+together, just as they could in earlier versions of Pelican.
+
+.. _ref-linking-to-internal-content:
+
+Linking to internal content
+===========================
+
+From Pelican 3.1 onwards, it is now possible to specify intra-site links to
+files in the *source content* hierarchy instead of files in the *generated*
+hierarchy. This makes it easier to link from the current post to other content
+that may be sitting alongside that post (instead of having to determine where
+the other content will be placed after site generation).
+
+To link to internal content (files in the ``content`` directory), use the
+following syntax for the link target: ``{filename}path/to/file``
+Note: forward slashes, ``/``,
+are the required path separator in the ``{filename}`` directive
+on all operating systems, including Windows.
+
+For example, a Pelican project might be structured like this::
+
+ website/
+ ├── content
+ │ ├── category/
+ │ │ └── article1.rst
+ │ ├── article2.md
+ │ └── pages
+ │ └── about.md
+ └── pelican.conf.py
+
+In this example, ``article1.rst`` could look like this::
+
+ The first article
+ #################
+
+ :date: 2012-12-01 10:02
+
+ See below intra-site link examples in reStructuredText format.
+
+ `a link relative to the current file <{filename}../article2.md>`_
+ `a link relative to the content root <{filename}/article2.md>`_
+
+and ``article2.md``::
+
+ Title: The second article
+ Date: 2012-12-01 10:02
+
+ See below intra-site link examples in Markdown format.
+
+ [a link relative to the current file]({filename}category/article1.rst)
+ [a link relative to the content root]({filename}/category/article1.rst)
+
+Linking to static files
+-----------------------
+
+You can link to static content using ``{static}path/to/file``. Files linked to
+with this syntax will automatically be copied to the output directory, even if
+the source directories containing them are not included in the ``STATIC_PATHS``
+setting of the project's ``pelicanconf.py`` file.
+
+For example, a project's content directory might be structured like this::
+
+ content
+ ├── images
+ │ └── han.jpg
+ ├── pdfs
+ │ └── menu.pdf
+ └── pages
+ └── test.md
+
+``test.md`` would include::
+
+ 
+ [Our Menu]({static}/pdfs/menu.pdf)
+
+Site generation would then copy ``han.jpg`` to ``output/images/han.jpg``,
+``menu.pdf`` to ``output/pdfs/menu.pdf``, and write the appropriate links
+in ``test.md``.
+
+If you use ``{static}`` to link to an article or a page, this will be turned
+into a link to its source code.
+
+Attaching static files
+----------------------
+
+Starting with Pelican 3.5, static files can be "attached" to a page or article
+using this syntax for the link target: ``{attach}path/to/file`` This works
+like the ``{static}`` syntax, but also relocates the static file into the
+linking document's output directory. If the static file originates from a
+subdirectory beneath the linking document's source, that relationship will be
+preserved on output. Otherwise, it will become a sibling of the linking
+document.
+
+This only works for linking to static files.
+
+For example, a project's content directory might be structured like this::
+
+ content
+ ├── blog
+ │ ├── icons
+ │ │ └── icon.png
+ │ ├── photo.jpg
+ │ └── testpost.md
+ └── downloads
+ └── archive.zip
+
+``pelicanconf.py`` would include::
+
+ PATH = 'content'
+ ARTICLE_PATHS = ['blog']
+ ARTICLE_SAVE_AS = '{date:%Y}/{slug}.html'
+ ARTICLE_URL = '{date:%Y}/{slug}.html'
+
+``testpost.md`` would include::
+
+ Title: Test Post
+ Category: test
+ Date: 2014-10-31
+
+ 
+ 
+ [Downloadable File]({attach}/downloads/archive.zip)
+
+Site generation would then produce an output directory structured like this::
+
+ output
+ └── 2014
+ ├── archive.zip
+ ├── icons
+ │ └── icon.png
+ ├── photo.jpg
+ └── test-post.html
+
+Notice that all the files linked using ``{attach}`` ended up in or beneath
+the article's output directory.
+
+If a static file is linked multiple times, the relocating feature of
+``{attach}`` will only work in the first of those links to be processed.
+After the first link, Pelican will treat ``{attach}`` like ``{static}``.
+This avoids breaking the already-processed links.
+
+**Be careful when linking to a file from multiple documents:**
+Since the first link to a file finalizes its location and Pelican does
+not define the order in which documents are processed, using ``{attach}`` on a
+file linked by multiple documents can cause its location to change from one
+site build to the next. (Whether this happens in practice will depend on the
+operating system, file system, version of Pelican, and documents being added,
+modified, or removed from the project.) Any external sites linking to the
+file's old location might then find their links broken. **It is therefore
+advisable to use {attach} only if you use it in all links to a file, and only
+if the linking documents share a single directory.** Under these conditions,
+the file's output location will not change in future builds. In cases where
+these precautions are not possible, consider using ``{static}`` links instead
+of ``{attach}``, and letting the file's location be determined by the project's
+``STATIC_SAVE_AS`` and ``STATIC_URL`` settings. (Per-file ``save_as`` and
+``url`` overrides can still be set in ``EXTRA_PATH_METADATA``.)
+
+.. note::
+ When using ``{attach}``, any parent directory in ``*_URL`` / ``*_SAVE_AS``
+ settings should match each other. See also: :ref:`url-settings`
+
+Linking to authors, categories, index and tags
+----------------------------------------------
+
+You can link to authors, categories, index and tags using the ``{author}name``,
+``{category}foobar``, ``{index}`` and ``{tag}tagname`` syntax.
+
+Deprecated internal link syntax
+-------------------------------
+
+To remain compatible with earlier versions, Pelican still supports vertical
+bars (``||``) in addition to curly braces (``{}``) for internal links. For
+example: ``|filename|an_article.rst``, ``|tag|tagname``, ``|category|foobar``.
+The syntax was changed from ``||`` to ``{}`` to avoid collision with Markdown
+extensions or reST directives. Similarly, Pelican also still supports linking
+to static content with ``{filename}``. The syntax was changed to ``{static}``
+to allow linking to both generated articles and pages and their static sources.
+
+Support for the old syntax may eventually be removed.
+
+Including other files
+---------------------
+Both Markdown and reStructuredText syntaxes provide mechanisms for this.
+
+Following below are some examples for **reStructuredText** using `the include directive`_:
+
+ .. code-block:: rst
+
+ .. include:: file.rst
+
+Include a fragment of a file delimited by two identifiers, highlighted as C++ (slicing based on line numbers is also possible):
+
+ .. code-block:: rst
+
+ .. include:: main.cpp
+ :code: c++
+ :start-after: // begin
+ :end-before: // end
+
+Include a raw HTML file (or an inline SVG) and put it directly into the output without any processing:
+
+ .. code-block:: rst
+
+ .. raw:: html
+ :file: table.html
+
+For **Markdown**, one must rely on an extension. For example, using the `mdx_include plugin`_:
+
+ .. code-block:: none
+
+ ```html
+ {! template.html !}
+ ```
+
+
+Importing an existing site
+==========================
+
+It is possible to import your site from WordPress, Tumblr, Dotclear, and RSS
+feeds using a simple script. See :ref:`import`.
+
+Translations
+============
+
+It is possible to translate articles. To do so, you need to add a ``lang`` meta
+attribute to your articles/pages and set a ``DEFAULT_LANG`` setting (which is
+English [en] by default). With those settings in place, only articles with the
+default language will be listed, and each article will be accompanied by a list
+of available translations for that article.
+
+.. note::
+
+ This core Pelican functionality does not create sub-sites
+ (e.g. ``example.com/de``) with translated templates for each
+ language. For such advanced functionality the `i18n_subsites
+ plugin`_ can be used.
+
+By default, Pelican uses the article's URL "slug" to determine if two or more
+articles are translations of one another. (This can be changed with the
+``ARTICLE_TRANSLATION_ID`` setting.) The slug can be set manually in the file's
+metadata; if not set explicitly, Pelican will auto-generate the slug from the
+title of the article.
+
+Here is an example of two articles, one in English and the other in French.
+
+The English article::
+
+ Foobar is not dead
+ ##################
+
+ :slug: foobar-is-not-dead
+ :lang: en
+
+ That's true, foobar is still alive!
+
+And the French version::
+
+ Foobar n'est pas mort !
+ #######################
+
+ :slug: foobar-is-not-dead
+ :lang: fr
+
+ Oui oui, foobar est toujours vivant !
+
+Post content quality notwithstanding, you can see that only item in common
+between the two articles is the slug, which is functioning here as an
+identifier. If you'd rather not explicitly define the slug this way, you must
+then instead ensure that the translated article titles are identical, since the
+slug will be auto-generated from the article title.
+
+If you do not want the original version of one specific article to be detected
+by the ``DEFAULT_LANG`` setting, use the ``translation`` metadata to specify
+which posts are translations::
+
+ Foobar is not dead
+ ##################
+
+ :slug: foobar-is-not-dead
+ :lang: en
+ :translation: true
+
+ That's true, foobar is still alive!
+
+
+.. _internal_pygments_options:
+
+Syntax highlighting
+===================
+
+Pelican can provide colorized syntax highlighting for your code blocks.
+To do so, you must use the following conventions inside your content files.
+
+For reStructuredText, use the ``code-block`` directive to specify the type
+of code to be highlighted (in these examples, we'll use ``python``)::
+
+ .. code-block:: python
+
+ print("Pelican is a static site generator.")
+
+For Markdown, which utilizes the `CodeHilite extension`_ to provide syntax
+highlighting, include the language identifier just above the code block,
+indenting both the identifier and the code::
+
+ There are two ways to specify the identifier:
+
+ :::python
+ print("The triple-colon syntax will *not* show line numbers.")
+
+ To display line numbers, use a path-less shebang instead of colons:
+
+ #!python
+ print("The path-less shebang syntax *will* show line numbers.")
+
+The specified identifier (e.g. ``python``, ``ruby``) should be one that
+appears on the `list of available lexers `_.
+
+When using reStructuredText the following options are available in the
+code-block directive:
+
+============= ============ =========================================
+Option Valid values Description
+============= ============ =========================================
+anchorlinenos N/A If present wrap line numbers in tags.
+classprefix string String to prepend to token class names
+hl_lines numbers List of lines to be highlighted, where
+ line numbers to highlight are separated
+ by a space. This is similar to
+ ``emphasize-lines`` in Sphinx, but it
+ does not support a range of line numbers
+ separated by a hyphen, or comma-separated
+ line numbers.
+lineanchors string Wrap each line in an anchor using this
+ string and -linenumber.
+linenos string If present or set to "table" output line
+ numbers in a table, if set to
+ "inline" output them inline. "none" means
+ do not output the line numbers for this
+ table.
+linenospecial number If set every nth line will be given the
+ 'special' css class.
+linenostart number Line number for the first line.
+linenostep number Print every nth line number.
+lineseparator string String to print between lines of code,
+ '\n' by default.
+linespans string Wrap each line in a span using this and
+ -linenumber.
+nobackground N/A If set do not output background color for
+ the wrapping element
+nowrap N/A If set do not wrap the tokens at all.
+tagsfile string ctags file to use for name definitions.
+tagurlformat string format for the ctag links.
+============= ============ =========================================
+
+Note that, depending on the version, your Pygments module might not have
+all of these options available. Refer to the *HtmlFormatter* section of the
+`Pygments documentation `_ for more
+details on each of the options.
+
+For example, the following code block enables line numbers, starting at 153,
+and prefixes the Pygments CSS classes with *pgcss* to make the names
+more unique and avoid possible CSS conflicts::
+
+ .. code-block:: identifier
+ :classprefix: pgcss
+ :linenos: table
+ :linenostart: 153
+
+
+
+It is also possible to specify the ``PYGMENTS_RST_OPTIONS`` variable in your
+Pelican settings file to include options that will be automatically applied to
+every code block.
+
+For example, if you want to have line numbers displayed for every code block
+and a CSS prefix you would set this variable to::
+
+ PYGMENTS_RST_OPTIONS = {'classprefix': 'pgcss', 'linenos': 'table'}
+
+If specified, settings for individual code blocks will override the defaults in
+your settings file.
+
+Publishing drafts
+=================
+
+If you want to publish an article or a page as a draft (for friends to review
+before publishing, for example), you can add a ``Status: draft`` attribute to
+its metadata. That article will then be output to the ``drafts`` folder and not
+listed on the index page nor on any category or tag page.
+
+If your articles should be automatically published as a draft (to not
+accidentally publish an article before it is finished) include the status in
+the ``DEFAULT_METADATA``::
+
+ DEFAULT_METADATA = {
+ 'status': 'draft',
+ }
+
+To publish a post when the default status is ``draft``, update the post's
+metadata to include ``Status: published``.
+
+.. _W3C ISO 8601: https://www.w3.org/TR/NOTE-datetime
+.. _AsciiDoc: https://www.methods.co.nz/asciidoc/
+.. _pelican-plugins: https://github.com/getpelican/pelican-plugins
+.. _Python-Markdown: https://github.com/Python-Markdown/markdown
+.. _Markdown Extensions: https://python-markdown.github.io/extensions/
+.. _CodeHilite extension: https://python-markdown.github.io/extensions/code_hilite/#syntax
+.. _i18n_subsites plugin: https://github.com/getpelican/pelican-plugins/tree/master/i18n_subsites
+.. _the include directive: http://docutils.sourceforge.net/docs/ref/rst/directives.html#include
+.. _mdx_include plugin: https://github.com/neurobin/mdx_include
diff --git a/docs/contribute.rst b/docs/contribute.rst
new file mode 100644
index 00000000..62ed51f9
--- /dev/null
+++ b/docs/contribute.rst
@@ -0,0 +1,242 @@
+Contributing and feedback guidelines
+####################################
+
+There are many ways to contribute to Pelican. You can improve the
+documentation, add missing features, and fix bugs (or just report them). You
+can also help out by reviewing and commenting on
+`existing issues `_.
+
+Don't hesitate to fork Pelican and submit an issue or pull request on GitHub.
+When doing so, please consider the following guidelines.
+
+.. include:: ../CONTRIBUTING.rst
+
+Setting up the development environment
+======================================
+
+While there are many ways to set up one's development environment, the following
+instructions will utilize Pip_ and Poetry_. These tools facilitate managing
+virtual environments for separate Python projects that are isolated from one
+another, so you can use different packages (and package versions) for each.
+
+Please note that Python 3.6+ is required for Pelican development.
+
+*(Optional)* If you prefer to install Poetry once for use with multiple projects,
+you can install it via::
+
+ curl -sSL https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py | python
+
+Point your web browser to the `Pelican repository`_ and tap the **Fork** button
+at top-right. Then clone the source for your fork and add the upstream project
+as a Git remote::
+
+ mkdir ~/projects
+ git clone https://github.com/YOUR_USERNAME/pelican.git ~/projects/pelican
+ cd ~/projects/pelican
+ git remote add upstream https://github.com/getpelican/pelican.git
+
+While Poetry can dynamically create and manage virtual environments, we're going
+to manually create and activate a virtual environment::
+
+ mkdir ~/virtualenvs
+ python3 -m venv ~/virtualenvs/pelican
+ source ~/virtualenvs/pelican/bin/activate
+
+Install the needed dependencies and set up the project::
+
+ python -m pip install invoke
+ invoke setup
+ python -m pip install -e ~/projects/pelican
+
+Your local environment should now be ready to go!
+
+.. _Pip: https://pip.pypa.io/
+.. _Poetry: https://poetry.eustace.io/docs/#installation
+.. _Pelican repository: https://github.com/getpelican/pelican
+
+Development
+===========
+
+Once Pelican has been set up for local development, create a topic branch for
+your bug fix or feature::
+
+ git checkout -b name-of-your-bugfix-or-feature
+
+Now you can make changes to Pelican, its documentation, and/or other aspects of
+the project.
+
+Running the test suite
+----------------------
+
+Each time you make changes to Pelican, there are two things to do regarding
+tests: check that the existing tests pass, and add tests for any new features
+or bug fixes. The tests are located in ``pelican/tests``, and you can run them
+via::
+
+ invoke tests
+
+In addition to running the test suite, the above invocation will also check code
+style and let you know whether non-conforming patterns were found. In some cases
+these linters will make the needed changes directly, while in other cases you
+may need to make additional changes until ``invoke tests`` no longer reports any
+code style violations.
+
+After making your changes and running the tests, you may see a test failure
+mentioning that "some generated files differ from the expected functional tests
+output." If you have made changes that affect the HTML output generated by
+Pelican, and the changes to that output are expected and deemed correct given
+the nature of your changes, then you should update the output used by the
+functional tests. To do so, **make sure you have both** ``en_EN.utf8`` **and**
+``fr_FR.utf8`` **locales installed**, and then run the following command::
+
+ invoke update-functional-tests
+
+You may also find that some tests are skipped because some dependency (e.g.,
+Pandoc) is not installed. This does not automatically mean that these tests
+have passed; you should at least verify that any skipped tests are not affected
+by your changes.
+
+You should run the test suite under each of the supported versions of Python.
+This is best done by creating a separate Python environment for each version.
+Tox_ is a useful tool to automate running tests inside ``virtualenv``
+environments.
+
+.. _Tox: https://tox.readthedocs.io/en/latest/
+
+Building the docs
+-----------------
+
+If you make changes to the documentation, you should build and inspect your
+changes before committing them::
+
+ invoke docserve
+
+Open http://localhost:8000 in your browser to review the documentation. While
+the above task is running, any changes you make and save to the documentation
+should automatically appear in the browser, as it live-reloads when it detects
+changes to the documentation source files.
+
+Plugin development
+------------------
+
+To create a *new* Pelican plugin, please refer to the `plugin template`_
+repository for detailed instructions.
+
+If you want to contribute to an *existing* Pelican plugin, follow the steps
+above to set up Pelican for local development, and then create a directory to
+store cloned plugin repositories::
+
+ mkdir -p ~/projects/pelican-plugins
+
+Assuming you wanted to contribute to the Simple Footnotes plugin, you would
+first browse to the `Simple Footnotes`_ repository on GitHub and tap the **Fork**
+button at top-right. Then clone the source for your fork and add the upstream
+project as a Git remote::
+
+ git clone https://github.com/YOUR_USERNAME/simple-footnotes.git ~/projects/pelican-plugins/simple-footnotes
+ cd ~/projects/pelican-plugins/simple-footnotes
+ git remote add upstream https://github.com/pelican-plugins/simple-footnotes.git
+
+Install the needed dependencies and set up the project::
+
+ invoke setup
+
+Create a topic branch for your plugin bug fix or feature::
+
+ git checkout -b name-of-your-bugfix-or-feature
+
+After writing new tests for your plugin changes, run the plugin test suite::
+
+ invoke tests
+
+.. _plugin template: https://github.com/getpelican/cookiecutter-pelican-plugin
+.. _Simple Footnotes: https://github.com/pelican-plugins/simple-footnotes
+
+Submitting your changes
+-----------------------
+
+Assuming linting validation and tests pass, add a ``RELEASE.md`` file in the root
+of the project that contains the release type (major, minor, patch) and a
+summary of the changes that will be used as the release changelog entry.
+For example::
+
+ Release type: patch
+
+ Fix browser reloading upon changes to content, settings, or theme
+
+Commit your changes and push your topic branch::
+
+ git add .
+ git commit -m "Your detailed description of your changes"
+ git push origin name-of-your-bugfix-or-feature
+
+Finally, browse to your repository fork on GitHub and submit a pull request.
+
+
+Logging tips
+============
+
+Try to use logging with appropriate levels.
+
+For logging messages that are not repeated, use the usual Python way::
+
+ # at top of file
+ import logging
+ logger = logging.getLogger(__name__)
+
+ # when needed
+ logger.warning("A warning with %s formatting", arg_to_be_formatted)
+
+Do not format log messages yourself. Use ``%s`` formatting in messages and pass
+arguments to logger. This is important, because the Pelican logger will
+preprocess some arguments, such as exceptions.
+
+Limiting extraneous log messages
+--------------------------------
+
+If the log message can occur several times, you may want to limit the log to
+prevent flooding. In order to do that, use the ``extra`` keyword argument for
+the logging message in the following format::
+
+ logger.warning("A warning with %s formatting", arg_to_be_formatted,
+ extra={'limit_msg': 'A generic message for too many warnings'})
+
+Optionally, you can also set ``'limit_args'`` as a tuple of arguments in
+``extra`` dict if your generic message needs formatting.
+
+Limit is set to ``5``, i.e, first four logs with the same ``'limit_msg'`` are
+outputted normally but the fifth one will be logged using ``'limit_msg'`` (and
+``'limit_args'`` if present). After the fifth, corresponding log messages will
+be ignored.
+
+For example, if you want to log missing resources, use the following code::
+
+ for resource in resources:
+ if resource.is_missing:
+ logger.warning(
+ 'The resource %s is missing', resource.name,
+ extra={'limit_msg': 'Other resources were missing'})
+
+The log messages will be displayed as follows::
+
+ WARNING: The resource prettiest_cat.jpg is missing
+ WARNING: The resource best_cat_ever.jpg is missing
+ WARNING: The resource cutest_cat.jpg is missing
+ WARNING: The resource lolcat.jpg is missing
+ WARNING: Other resources were missing
+
+
+Outputting traceback in the logs
+--------------------------------
+
+If you're logging inside an ``except`` block, you may want to provide the
+traceback information as well. You can do that by setting ``exc_info`` keyword
+argument to ``True`` during logging. However, doing so by default can be
+undesired because tracebacks are long and can be confusing to regular users.
+Try to limit them to ``--debug`` mode like the following::
+
+ try:
+ some_action()
+ except Exception as e:
+ logger.error('Exception occurred: %s', e,
+ exc_info=settings.get('DEBUG', False))
diff --git a/docs/faq.rst b/docs/faq.rst
new file mode 100644
index 00000000..8532ab3f
--- /dev/null
+++ b/docs/faq.rst
@@ -0,0 +1,291 @@
+Frequently Asked Questions (FAQ)
+################################
+
+Here are some frequently asked questions about Pelican.
+
+What's the best way to communicate a problem, question, or suggestion?
+======================================================================
+
+Please read our :doc:`feedback guidelines `.
+
+How can I help?
+===============
+
+There are several ways to help out. First, you can report any Pelican
+suggestions or problems you might have via IRC (preferred) or the `issue
+tracker `_. If submitting an
+issue report, please first check the existing issue list (both open and closed)
+in order to avoid submitting a duplicate issue.
+
+If you want to contribute, please fork `the git repository
+ `_, create a new feature branch, make
+your changes, and issue a pull request. Someone will review your changes as
+soon as possible. Please refer to the :doc:`How to Contribute `
+section for more details.
+
+You can also contribute by creating themes and improving the documentation.
+
+Is the Pelican settings file mandatory?
+=======================================
+
+Configuration files are optional and are just an easy way to configure Pelican.
+For basic operations, it's possible to specify options while invoking Pelican
+via the command line. See ``pelican --help`` for more information.
+
+Changes to the settings file take no effect
+===========================================
+
+When experimenting with different settings (especially the metadata ones)
+caching may interfere and the changes may not be visible. In such cases, ensure
+that caching is disabled via ``LOAD_CONTENT_CACHE = False`` or use the
+``--ignore-cache`` command-line switch.
+
+I'm creating my own theme. How do I use Pygments for syntax highlighting?
+=========================================================================
+
+Pygments adds some classes to the generated content. These classes are used by
+themes to style code syntax highlighting via CSS. Specifically, you can
+customize the appearance of your syntax highlighting via the ``.highlight pre``
+class in your theme's CSS file. To see how various styles can be used to render
+Django code, for example, use the style selector drop-down at top-right on the
+`Pygments project demo site `_.
+
+You can use the following example commands to generate a starting CSS file from
+a Pygments built-in style (in this case, "monokai") and then copy the generated
+CSS file to your new theme::
+
+ pygmentize -S monokai -f html -a .highlight > pygment.css
+ cp pygment.css path/to/theme/static/css/
+
+Don't forget to import your ``pygment.css`` file from your main CSS file.
+
+How do I create my own theme?
+=============================
+
+Please refer to :ref:`theming-pelican`.
+
+I want to use Markdown, but I got an error.
+===========================================
+
+If you try to generate Markdown content without first installing the Markdown
+library, may see a message that says ``No valid files found in content``.
+Markdown is not a hard dependency for Pelican, so if you have content in
+Markdown format, you will need to explicitly install the Markdown library. You
+can do so by typing the following command, prepending ``sudo`` if permissions
+require it::
+
+ python -m pip install markdown
+
+Can I use arbitrary metadata in my templates?
+=============================================
+
+Yes. For example, to include a modified date in a Markdown post, one could
+include the following at the top of the article::
+
+ Modified: 2012-08-08
+
+For reStructuredText, this metadata should of course be prefixed with a colon::
+
+ :Modified: 2012-08-08
+
+This metadata can then be accessed in templates such as ``article.html`` via::
+
+ {% if article.modified %}
+ Last modified: {{ article.modified }}
+ {% endif %}
+
+If you want to include metadata in templates outside the article context (e.g.,
+``base.html``), the ``if`` statement should instead be::
+
+ {% if article and article.modified %}
+
+How do I assign custom templates on a per-page basis?
+=====================================================
+
+It's as simple as adding an extra line of metadata to any page or article that
+you want to have its own template. For example, this is how it would be handled
+for content in reST format::
+
+ :template: template_name
+
+For content in Markdown format::
+
+ Template: template_name
+
+Then just make sure your theme contains the relevant template file (e.g.
+``template_name.html``).
+
+How can I override the generated URL of a specific page or article?
+===================================================================
+
+Include ``url`` and ``save_as`` metadata in any pages or articles that you want
+to override the generated URL. Here is an example page in reST format::
+
+ Override url/save_as page
+ #########################
+
+ :url: override/url/
+ :save_as: override/url/index.html
+
+With this metadata, the page will be written to ``override/url/index.html``
+and Pelican will use url ``override/url/`` to link to this page.
+
+How can I use a static page as my home page?
+============================================
+
+The override feature mentioned above can be used to specify a static page as
+your home page. The following Markdown example could be stored in
+``content/pages/home.md``::
+
+ Title: Welcome to My Site
+ URL:
+ save_as: index.html
+
+ Thank you for visiting. Welcome!
+
+If the original blog index is still wanted, it can then be saved in a
+different location by setting ``INDEX_SAVE_AS = 'blog_index.html'`` for
+the ``'index'`` direct template.
+
+What if I want to disable feed generation?
+==========================================
+
+To disable feed generation, all feed settings should be set to ``None``. All
+but three feed settings already default to ``None``, so if you want to disable
+all feed generation, you only need to specify the following settings::
+
+ FEED_ALL_ATOM = None
+ CATEGORY_FEED_ATOM = None
+ TRANSLATION_FEED_ATOM = None
+ AUTHOR_FEED_ATOM = None
+ AUTHOR_FEED_RSS = None
+
+The word ``None`` should not be surrounded by quotes. Please note that ``None``
+and ``''`` are not the same thing.
+
+I'm getting a warning about feeds generated without SITEURL being set properly
+==============================================================================
+
+`RSS and Atom feeds require all URL links to be absolute
+`_. In order to properly
+generate links in Pelican you will need to set ``SITEURL`` to the full path of
+your site.
+
+Feeds are still generated when this warning is displayed, but links within may
+be malformed and thus the feed may not validate.
+
+My feeds are broken since I upgraded to Pelican 3.x
+===================================================
+
+Starting in 3.0, some of the FEED setting names were changed to more explicitly
+refer to the Atom feeds they inherently represent (much like the FEED_RSS
+setting names). Here is an exact list of the renamed settings::
+
+ FEED -> FEED_ATOM
+ TAG_FEED -> TAG_FEED_ATOM
+ CATEGORY_FEED -> CATEGORY_FEED_ATOM
+
+Starting in 3.1, the new feed ``FEED_ALL_ATOM`` has been introduced: this feed
+will aggregate all posts regardless of their language. This setting generates
+``'feeds/all.atom.xml'`` by default and ``FEED_ATOM`` now defaults to ``None``.
+The following feed setting has also been renamed::
+
+ TRANSLATION_FEED -> TRANSLATION_FEED_ATOM
+
+Older themes that referenced the old setting names may not link properly. In
+order to rectify this, please update your theme for compatibility by changing
+the relevant values in your template files. For an example of complete feed
+headers and usage please check out the ``simple`` theme.
+
+Is Pelican only suitable for blogs?
+===================================
+
+No. Pelican can be easily configured to create and maintain any type of static
+site. This may require a little customization of your theme and Pelican
+configuration. For example, if you are building a launch site for your product
+and do not need tags on your site, you could remove the relevant HTML code from
+your theme. You can also disable generation of tag-related pages via::
+
+ TAGS_SAVE_AS = ''
+ TAG_SAVE_AS = ''
+
+Why does Pelican always write all HTML files even with content caching enabled?
+===============================================================================
+
+In order to reliably determine whether the HTML output is different before
+writing it, a large part of the generation environment including the template
+contexts, imported plugins, etc. would have to be saved and compared, at least
+in the form of a hash (which would require special handling of unhashable
+types), because of all the possible combinations of plugins, pagination, etc.
+which may change in many different ways. This would require a lot more
+processing time and memory and storage space. Simply writing the files each
+time is a lot faster and a lot more reliable.
+
+However, this means that the modification time of the files changes every time,
+so a ``rsync`` based upload will transfer them even if their content hasn't
+changed. A simple solution is to make ``rsync`` use the ``--checksum`` option,
+which will make it compare the file checksums in a much faster way than Pelican
+would.
+
+When only several specific output files are of interest (e.g. when working on
+some specific page or the theme templates), the `WRITE_SELECTED` option may
+help, see :ref:`writing_only_selected_content`.
+
+How to process only a subset of all articles?
+=============================================
+
+It is often useful to process only e.g. 10 articles for debugging purposes.
+This can be achieved by explicitly specifying only the filenames of those
+articles in ``ARTICLE_PATHS``. A list of such filenames could be found using a
+command similar to ``cd content; find -name '*.md' | head -n 10``.
+
+My tag-cloud is missing/broken since I upgraded Pelican
+=======================================================
+
+In an ongoing effort to steamline Pelican, `tag_cloud` generation has been
+moved out of the pelican core and into a separate `plugin
+`_. See
+the :ref:`plugins` documentation further information about the Pelican plugin
+system.
+
+Since I upgraded Pelican my pages are no longer rendered
+========================================================
+
+Pages were available to themes as lowercase ``pages`` and uppercase ``PAGES``.
+To bring this inline with the :ref:`templates-variables` section, ``PAGES`` has
+been removed. This is quickly resolved by updating your theme to iterate over
+``pages`` instead of ``PAGES``. Just replace::
+
+ {% for pg in PAGES %}
+
+with something like::
+
+ {% for pg in pages %}
+
+How can I stop Pelican from trying to parse my static files as content?
+=======================================================================
+
+Pelican's article and page generators run before it's static generator. That
+means if you use a setup similar to the default configuration, where a static
+source directory is defined inside a ``*_PATHS`` setting, all files that have a
+valid content file ending (``.html``, ``.rst``, ``.md``, ...) will be treated
+as articles or pages before they get treated as static files.
+
+To circumvent this issue either use the appropriate ``*_EXCLUDES`` setting or
+disable the offending reader via ``READERS`` if you don't need it.
+
+Why is [arbitrary Markdown syntax] not supported?
+=================================================
+
+Pelican does not directly handle Markdown processing and instead delegates that
+task to the Python-Markdown_ project, the core of which purposefully follows
+the original Markdown syntax rules and not the myriad Markdown "flavors" that
+have subsequently propagated. That said, Python-Markdown_ is quite modular, and
+the syntax you are looking for may be provided by one of the many available
+`Markdown Extensions`_. Alternatively, some folks have created Pelican plugins
+that support Markdown variants, so that may be your best choice if there is a
+particular variant you want to use when writing your content.
+
+
+.. _Python-Markdown: https://github.com/Python-Markdown/markdown
+.. _Markdown Extensions: https://python-markdown.github.io/extensions/
diff --git a/docs/importer.rst b/docs/importer.rst
new file mode 100644
index 00000000..b8313f49
--- /dev/null
+++ b/docs/importer.rst
@@ -0,0 +1,143 @@
+.. _import:
+
+Importing an existing site
+##########################
+
+Description
+===========
+
+``pelican-import`` is a command-line tool for converting articles from other
+software to reStructuredText or Markdown. The supported import formats are:
+
+- Blogger XML export
+- Dotclear export
+- Posterous API
+- Tumblr API
+- WordPress XML export
+- RSS/Atom feed
+
+The conversion from HTML to reStructuredText or Markdown relies on `Pandoc`_.
+For Dotclear, if the source posts are written with Markdown syntax, they will
+not be converted (as Pelican also supports Markdown).
+
+.. note::
+
+ Unlike Pelican, Wordpress supports multiple categories per article. These
+ are imported as a comma-separated string. You have to resolve these
+ manually, or use a plugin that enables multiple categories per article
+ (like `more_categories`_).
+
+Dependencies
+============
+
+``pelican-import`` has some dependencies not required by the rest of Pelican:
+
+- *BeautifulSoup4* and *lxml*, for WordPress and Dotclear import. Can be
+ installed like any other Python package (``pip install BeautifulSoup4
+ lxml``).
+- *Feedparser*, for feed import (``pip install feedparser``).
+- *Pandoc*, see the `Pandoc site`_ for installation instructions on your
+ operating system.
+
+.. _Pandoc: https://pandoc.org/
+.. _Pandoc site: https://pandoc.org/installing.html
+
+
+Usage
+=====
+
+::
+
+ pelican-import [-h] [--blogger] [--dotclear] [--posterous] [--tumblr] [--wpfile] [--feed]
+ [-o OUTPUT] [-m MARKUP] [--dir-cat] [--dir-page] [--strip-raw] [--wp-custpost]
+ [--wp-attach] [--disable-slugs] [-e EMAIL] [-p PASSWORD] [-b BLOGNAME]
+ input|api_token|api_key
+
+Positional arguments
+--------------------
+ ============= ============================================================================
+ ``input`` The input file to read
+ ``api_token`` (Posterous only) api_token can be obtained from http://posterous.com/api/
+ ``api_key`` (Tumblr only) api_key can be obtained from https://www.tumblr.com/oauth/apps
+ ============= ============================================================================
+
+Optional arguments
+------------------
+
+ -h, --help Show this help message and exit
+ --blogger Blogger XML export (default: False)
+ --dotclear Dotclear export (default: False)
+ --posterous Posterous API (default: False)
+ --tumblr Tumblr API (default: False)
+ --wpfile WordPress XML export (default: False)
+ --feed Feed to parse (default: False)
+ -o OUTPUT, --output OUTPUT
+ Output path (default: content)
+ -m MARKUP, --markup MARKUP
+ Output markup format: ``rst``, ``markdown``, or ``asciidoc``
+ (default: ``rst``)
+ --dir-cat Put files in directories with categories name
+ (default: False)
+ --dir-page Put files recognised as pages in "pages/" sub-
+ directory (blogger and wordpress import only)
+ (default: False)
+ --filter-author Import only post from the specified author
+ --strip-raw Strip raw HTML code that can't be converted to markup
+ such as flash embeds or iframes (wordpress import
+ only) (default: False)
+ --wp-custpost Put wordpress custom post types in directories. If
+ used with --dir-cat option directories will be created
+ as "/post_type/category/" (wordpress import only)
+ --wp-attach Download files uploaded to wordpress as attachments.
+ Files will be added to posts as a list in the post
+ header and links to the files within the post will be
+ updated. All files will be downloaded, even if they
+ aren't associated with a post. Files will be downloaded
+ with their original path inside the output directory,
+ e.g. "output/wp-uploads/date/postname/file.jpg".
+ (wordpress import only) (requires an internet
+ connection)
+ --disable-slugs Disable storing slugs from imported posts within
+ output. With this disabled, your Pelican URLs may not
+ be consistent with your original posts. (default:
+ False)
+ -e EMAIL, --email=EMAIL
+ Email used to authenticate Posterous API
+ -p PASSWORD, --password=PASSWORD
+ Password used to authenticate Posterous API
+ -b BLOGNAME, --blogname=BLOGNAME
+ Blog name used in Tumblr API
+
+
+Examples
+========
+
+For Blogger::
+
+ $ pelican-import --blogger -o ~/output ~/posts.xml
+
+For Dotclear::
+
+ $ pelican-import --dotclear -o ~/output ~/backup.txt
+
+for Posterous::
+
+ $ pelican-import --posterous -o ~/output --email= --password=
+
+For Tumblr::
+
+ $ pelican-import --tumblr -o ~/output --blogname=
+
+For WordPress::
+
+ $ pelican-import --wpfile -o ~/output ~/posts.xml
+
+Tests
+=====
+
+To test the module, one can use sample files:
+
+- for WordPress: https://www.wpbeginner.com/wp-themes/how-to-add-dummy-content-for-theme-development-in-wordpress/
+- for Dotclear: http://media.dotaddict.org/tda/downloads/lorem-backup.txt
+
+.. _more_categories: https://github.com/getpelican/pelican-plugins/tree/master/more_categories
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 00000000..4cbe41ae
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,85 @@
+Pelican |release|
+=================
+
+
+.. ifconfig:: release.endswith('.dev')
+
+ .. warning::
+
+ This documentation is for the version of Pelican currently under
+ development. Were you looking for version |last_stable| documentation?
+
+
+Pelican is a static site generator, written in Python_. Highlights include:
+
+* Write your content directly with your editor of choice in reStructuredText_
+ or Markdown_ formats
+* Includes a simple CLI tool to (re)generate your site
+* Easy to interface with distributed version control systems and web hooks
+* Completely static output is easy to host anywhere
+
+Ready to get started? Check out the :doc:`Quickstart` guide.
+
+Features
+--------
+
+Pelican |version| currently supports:
+
+* Articles (e.g., blog posts) and pages (e.g., "About", "Projects", "Contact")
+* Comments, via an external service (Disqus). If you prefer to have more
+ control over your comment data, self-hosted comments are another option.
+ Check out the `Pelican Plugins`_ repository for more details.
+* Theming support (themes are created using Jinja2_ templates)
+* Publication of articles in multiple languages
+* Atom/RSS feeds
+* Code syntax highlighting
+* Import from WordPress, Dotclear, or RSS feeds
+* Integration with external tools: Twitter, Google Analytics, etc. (optional)
+* Fast rebuild times thanks to content caching and selective output writing
+
+Why the name "Pelican"?
+-----------------------
+
+"Pelican" is an anagram for *calepin*, which means "notebook" in French. ;)
+
+Source code
+-----------
+
+You can access the source code at: https://github.com/getpelican/pelican
+
+How to get help, contribute, or provide feedback
+------------------------------------------------
+
+See our :doc:`feedback and contribution submission guidelines `.
+
+Documentation
+-------------
+
+.. toctree::
+ :maxdepth: 2
+
+ quickstart
+ install
+ content
+ publish
+ settings
+ themes
+ plugins
+ pelican-themes
+ importer
+ faq
+ tips
+ contribute
+ internals
+ report
+ changelog
+
+.. Links
+
+.. _Python: https://www.python.org/
+.. _reStructuredText: http://docutils.sourceforge.net/rst.html
+.. _Markdown: https://daringfireball.net/projects/markdown/
+.. _Jinja2: https://palletsprojects.com/p/jinja/
+.. _`Pelican documentation`: https://docs.getpelican.com/latest/
+.. _`Pelican's internals`: https://docs.getpelican.com/en/latest/internals.html
+.. _`Pelican plugins`: https://github.com/getpelican/pelican-plugins
diff --git a/docs/install.rst b/docs/install.rst
new file mode 100644
index 00000000..eb618035
--- /dev/null
+++ b/docs/install.rst
@@ -0,0 +1,123 @@
+Installing Pelican
+##################
+
+Pelican currently runs best on 3.6+; earlier versions of Python are not supported.
+
+You can install Pelican via several different methods. The simplest is via Pip_::
+
+ python -m pip install pelican
+
+Or, if you plan on using Markdown::
+
+ python -m pip install "pelican[markdown]"
+
+(Keep in mind that some operating systems will require you to prefix the above
+command with ``sudo`` in order to install Pelican system-wide.)
+
+While the above is the simplest method, the recommended approach is to create a
+virtual environment for Pelican via virtualenv_ before installing Pelican.
+Assuming you have virtualenv_ installed, you can then open a new terminal
+session and create a new virtual environment for Pelican::
+
+ virtualenv ~/virtualenvs/pelican
+ cd ~/virtualenvs/pelican
+ source bin/activate
+
+Once the virtual environment has been created and activated, Pelican can be
+installed via ``python -m pip install pelican`` as noted above. Alternatively, if you
+have the project source, you can install Pelican using the distutils method::
+
+ cd path-to-Pelican-source
+ python setup.py install
+
+If you have Git installed and prefer to install the latest bleeding-edge
+version of Pelican rather than a stable release, use the following command::
+
+ python -m pip install -e "git+https://github.com/getpelican/pelican.git#egg=pelican"
+
+Once Pelican is installed, you can run ``pelican --help`` to see basic usage
+options. For more detail, refer to the :doc:`Publish` section.
+
+Optional packages
+-----------------
+
+If you plan on using `Markdown `_ as a
+markup format, you can install Pelican with Markdown support::
+
+ python -m pip install "pelican[markdown]"
+
+Typographical enhancements can be enabled in your settings file, but first the
+requisite `Typogrify `_ library must be
+installed::
+
+ python -m pip install typogrify
+
+Dependencies
+------------
+
+When Pelican is installed, the following dependent Python packages should be
+automatically installed without any action on your part:
+
+* `feedgenerator `_, to generate the
+ Atom feeds
+* `jinja2 `_, for templating support
+* `pygments `_, for syntax highlighting
+* `docutils `_, for supporting
+ reStructuredText as an input format
+* `pytz `_, for timezone definitions
+* `blinker `_, an object-to-object and
+ broadcast signaling system
+* `unidecode `_, for ASCII
+ transliterations of Unicode text
+ utilities
+* `MarkupSafe `_, for a markup-safe
+ string implementation
+* `python-dateutil `_, to read
+ the date metadata
+
+Upgrading
+---------
+
+If you installed a stable Pelican release via Pip_ and wish to upgrade to
+the latest stable release, you can do so by adding ``--upgrade``::
+
+ python -m pip install --upgrade pelican
+
+If you installed Pelican via distutils or the bleeding-edge method, simply
+perform the same step to install the most recent version.
+
+Kickstart your site
+-------------------
+
+Once Pelican has been installed, you can create a skeleton project via the
+``pelican-quickstart`` command, which begins by asking some questions about
+your site::
+
+ pelican-quickstart
+
+If run inside an activated virtual environment, ``pelican-quickstart`` will
+look for an associated project path inside ``$VIRTUAL_ENV/.project``. If that
+file exists and contains a valid directory path, the new Pelican project will
+be saved at that location. Otherwise, the default is the current working
+directory. To set the new project path on initial invocation, use:
+``pelican-quickstart --path /your/desired/directory``
+
+Once you finish answering all the questions, your project will consist of the
+following hierarchy (except for *pages* — shown in parentheses below — which
+you can optionally add yourself if you plan to create non-chronological
+content)::
+
+ yourproject/
+ ├── content
+ │ └── (pages)
+ ├── output
+ ├── tasks.py
+ ├── Makefile
+ ├── pelicanconf.py # Main settings file
+ └── publishconf.py # Settings to use when ready to publish
+
+The next step is to begin to adding content to the *content* folder that has
+been created for you.
+
+.. _Pip: https://pip.pypa.io/
+.. _virtualenv: https://virtualenv.pypa.io/en/latest/
diff --git a/docs/internals.rst b/docs/internals.rst
new file mode 100644
index 00000000..01d60c39
--- /dev/null
+++ b/docs/internals.rst
@@ -0,0 +1,98 @@
+Pelican internals
+#################
+
+This section describe how Pelican works internally. As you'll see, it's quite
+simple, but a bit of documentation doesn't hurt. :)
+
+You can also find in the :doc:`report` section an excerpt of a report the
+original author wrote with some software design information.
+
+.. _report: :doc:`report`
+
+Overall structure
+=================
+
+What Pelican does is take a list of files and process them into some sort of
+output. Usually, the input files are reStructuredText and Markdown files, and
+the output is a blog, but both input and output can be anything you want.
+
+The logic is separated into different classes and concepts:
+
+* **Writers** are responsible for writing files: .html files, RSS feeds, and so
+ on. Since those operations are commonly used, the object is created once and
+ then passed to the generators.
+
+* **Readers** are used to read from various formats (HTML, Markdown and
+ reStructuredText for now, but the system is extensible). Given a file, they
+ return metadata (author, tags, category, etc.) and content (HTML-formatted).
+
+* **Generators** generate the different outputs. For instance, Pelican comes
+ with ``ArticlesGenerator`` and ``PageGenerator``. Given a configuration, they
+ can do whatever they want. Most of the time, it's generating files from
+ inputs.
+
+* Pelican also uses templates, so it's easy to write your own theme. The
+ syntax is `Jinja2 `_ and is very easy to learn, so
+ don't hesitate to jump in and build your own theme.
+
+How to implement a new reader?
+==============================
+
+Is there an awesome markup language you want to add to Pelican? Well, the only
+thing you have to do is to create a class with a ``read`` method that returns
+HTML content and some metadata.
+
+Take a look at the Markdown reader::
+
+ from pelican.readers import BaseReader
+ from pelican.utils import pelican_open
+ from markdown import Markdown
+
+ class MarkdownReader(BaseReader):
+ enabled = True
+
+ def read(self, source_path):
+ """Parse content and metadata of markdown files"""
+
+ with pelican_open(source_path) as text:
+ md_extensions = {'markdown.extensions.meta': {},
+ 'markdown.extensions.codehilite': {}}
+ md = Markdown(extensions=md_extensions.keys(),
+ extension_configs=md_extensions)
+ content = md.convert(text)
+
+ metadata = {}
+ for name, value in md.Meta.items():
+ name = name.lower()
+ meta = self.process_metadata(name, value[0])
+ metadata[name] = meta
+ return content, metadata
+
+Simple, isn't it?
+
+If your new reader requires additional Python dependencies, then you should
+wrap their ``import`` statements in a ``try...except`` block. Then inside the
+reader's class, set the ``enabled`` class attribute to mark import success or
+failure. This makes it possible for users to continue using their favourite
+markup method without needing to install modules for formats they don't use.
+
+How to implement a new generator?
+=================================
+
+Generators have two important methods. You're not forced to create both; only
+the existing ones will be called.
+
+* ``generate_context``, that is called first, for all the generators.
+ Do whatever you have to do, and update the global context if needed. This
+ context is shared between all generators, and will be passed to the
+ templates. For instance, the ``PageGenerator`` ``generate_context`` method
+ finds all the pages, transforms them into objects, and populates the context
+ with them. Be careful *not* to output anything using this context at this
+ stage, as it is likely to change by the effect of other generators.
+
+* ``generate_output`` is then called. And guess what is it made for? Oh,
+ generating the output. :) It's here that you may want to look at the context
+ and call the methods of the ``writer`` object that is passed as the first
+ argument of this function. In the ``PageGenerator`` example, this method will
+ look at all the pages recorded in the global context and output a file on the
+ disk (using the writer method ``write_file``) for each page encountered.
diff --git a/docs/pelican-themes.rst b/docs/pelican-themes.rst
new file mode 100644
index 00000000..e63f6a19
--- /dev/null
+++ b/docs/pelican-themes.rst
@@ -0,0 +1,170 @@
+pelican-themes
+##############
+
+
+
+Description
+===========
+
+``pelican-themes`` is a command line tool for managing themes for Pelican. See
+:ref:`settings/themes` for settings related to themes.
+
+
+Usage
+"""""
+
+| pelican-themes [-h] [-l] [-i theme path [theme path ...]]
+| [-r theme name [theme name ...]]
+| [-s theme path [theme path ...]] [-v] [--version]
+
+Optional arguments:
+"""""""""""""""""""
+
+
+-h, --help Show the help an exit
+
+-l, --list Show the themes already installed
+
+-i theme_path, --install theme_path One or more themes to install
+
+-r theme_name, --remove theme_name One or more themes to remove
+
+-s theme_path, --symlink theme_path Same as "--install", but create a symbolic link instead of copying the theme.
+ Useful for theme development
+
+-v, --verbose Verbose output
+
+--version Print the version of this script
+
+
+
+Examples
+========
+
+
+Listing the installed themes
+""""""""""""""""""""""""""""
+
+With ``pelican-themes``, you can see the available themes by using the ``-l``
+or ``--list`` option:
+
+.. code-block:: console
+
+ $ pelican-themes -l
+ notmyidea
+ two-column@
+ simple
+ $ pelican-themes --list
+ notmyidea
+ two-column@
+ simple
+
+In this example, we can see there are three themes available: ``notmyidea``,
+``simple``, and ``two-column``.
+
+``two-column`` is prefixed with an ``@`` because this theme is not copied to
+the Pelican theme path, but is instead just linked to it (see `Creating
+symbolic links`_ for details about creating symbolic links).
+
+Note that you can combine the ``--list`` option with the ``-v`` or
+``--verbose`` option to get more verbose output, like this:
+
+.. code-block:: console
+
+ $ pelican-themes -v -l
+ /usr/local/lib/python2.6/dist-packages/pelican-2.6.0-py2.6.egg/pelican/themes/notmyidea
+ /usr/local/lib/python2.6/dist-packages/pelican-2.6.0-py2.6.egg/pelican/themes/two-column (symbolic link to `/home/skami/Dev/Python/pelican-themes/two-column')
+ /usr/local/lib/python2.6/dist-packages/pelican-2.6.0-py2.6.egg/pelican/themes/simple
+
+
+Installing themes
+"""""""""""""""""
+
+You can install one or more themes using the ``-i`` or ``--install`` option.
+This option takes as argument the path(s) of the theme(s) you want to install,
+and can be combined with the verbose option:
+
+.. code-block:: console
+
+ # pelican-themes --install ~/Dev/Python/pelican-themes/notmyidea-cms --verbose
+
+.. code-block:: console
+
+ # pelican-themes --install ~/Dev/Python/pelican-themes/notmyidea-cms\
+ ~/Dev/Python/pelican-themes/martyalchin \
+ --verbose
+
+.. code-block:: console
+
+ # pelican-themes -vi ~/Dev/Python/pelican-themes/two-column
+
+
+Removing themes
+"""""""""""""""
+
+The ``pelican-themes`` command can also remove themes from the Pelican themes
+path. The ``-r`` or ``--remove`` option takes as argument the name(s) of the
+theme(s) you want to remove, and can be combined with the ``--verbose`` option.
+
+.. code-block:: console
+
+ # pelican-themes --remove two-column
+
+.. code-block:: console
+
+ # pelican-themes -r martyachin notmyidea-cmd -v
+
+
+
+
+
+Creating symbolic links
+"""""""""""""""""""""""
+
+``pelican-themes`` can also install themes by creating symbolic links instead
+of copying entire themes into the Pelican themes path.
+
+To symbolically link a theme, you can use the ``-s`` or ``--symlink``, which
+works exactly as the ``--install`` option:
+
+.. code-block:: console
+
+ # pelican-themes --symlink ~/Dev/Python/pelican-themes/two-column
+
+In this example, the ``two-column`` theme is now symbolically linked to the
+Pelican themes path, so we can use it, but we can also modify it without having
+to reinstall it after each modification.
+
+This is useful for theme development:
+
+.. code-block:: console
+
+ $ sudo pelican-themes -s ~/Dev/Python/pelican-themes/two-column
+ $ pelican ~/Blog/content -o /tmp/out -t two-column
+ $ firefox /tmp/out/index.html
+ $ vim ~/Dev/Pelican/pelican-themes/two-column/static/css/main.css
+ $ pelican ~/Blog/content -o /tmp/out -t two-column
+ $ cp /tmp/bg.png ~/Dev/Pelican/pelican-themes/two-column/static/img/bg.png
+ $ pelican ~/Blog/content -o /tmp/out -t two-column
+ $ vim ~/Dev/Pelican/pelican-themes/two-column/templates/index.html
+ $ pelican ~/Blog/content -o /tmp/out -t two-column
+
+
+
+Doing several things at once
+""""""""""""""""""""""""""""
+
+The ``--install``, ``--remove`` and ``--symlink`` option are not mutually
+exclusive, so you can combine them in the same command line to do more than one
+operation at time, like this:
+
+
+.. code-block:: console
+
+ # pelican-themes --remove notmyidea-cms two-column \
+ --install ~/Dev/Python/pelican-themes/notmyidea-cms-fr \
+ --symlink ~/Dev/Python/pelican-themes/two-column \
+ --verbose
+
+In this example, the theme ``notmyidea-cms`` is replaced by the theme
+``notmyidea-cms-fr``
diff --git a/docs/plugins.rst b/docs/plugins.rst
new file mode 100644
index 00000000..3a56ed07
--- /dev/null
+++ b/docs/plugins.rst
@@ -0,0 +1,279 @@
+.. _plugins:
+
+Plugins
+#######
+
+Beginning with version 3.0, Pelican supports plugins. Plugins are a way to add
+features to Pelican without having to directly modify the Pelican core.
+
+How to use plugins
+==================
+
+Starting with version 4.5, Pelican moved to a new plugin structure utilizing
+namespace packages that can be easily installed via Pip_. Plugins supporting
+this structure will install under the namespace package ``pelican.plugins`` and
+can be automatically discovered by Pelican. To see a list of plugins that are
+active in your environment, run::
+
+ pelican-plugins
+
+If you leave the ``PLUGINS`` setting as default (``None``), Pelican will
+automatically discover namespace plugins and register them. If, on the other
+hand, you specify a ``PLUGINS`` setting as a list of plugins, this
+auto-discovery will be disabled. At that point, only the plugins you specify
+will be registered, and you must explicitly list any namespace plugins as well.
+
+If you are using the ``PLUGINS`` setting, you can specify plugins in two ways.
+The first method specifies plugins as a list of strings. Namespace plugins can
+be specified either by their full names (``pelican.plugins.myplugin``) or by
+their short names (``myplugin``)::
+
+ PLUGINS = ['package.myplugin',
+ 'namespace_plugin1',
+ 'pelican.plugins.namespace_plugin2']
+
+Alternatively, you can import them in your settings file and pass the modules::
+
+ from package import myplugin
+ from pelican.plugins import namespace_plugin1, namespace_plugin2
+ PLUGINS = [myplugin, namespace_plugin1, namespace_plugin2]
+
+.. note::
+
+ When experimenting with different plugins (especially the ones that deal
+ with metadata and content) caching may interfere and the changes may not be
+ visible. In such cases disable caching with ``LOAD_CONTENT_CACHE = False``
+ or use the ``--ignore-cache`` command-line switch.
+
+If your plugins are not in an importable path, you can specify a list of paths
+via the ``PLUGIN_PATHS`` setting. As shown in the following example, paths in
+the ``PLUGIN_PATHS`` list can be absolute or relative to the settings file::
+
+ PLUGIN_PATHS = ["plugins", "/srv/pelican/plugins"]
+ PLUGINS = ["assets", "liquid_tags", "sitemap"]
+
+Where to find plugins
+=====================
+Namespace plugins can be found in the `pelican-plugins organization`_ as
+individual repositories. Legacy plugins are located in the `pelican-plugins
+repository`_ and will be gradually phased out in favor of the namespace
+versions.
+
+.. _pelican-plugins organization: https://github.com/pelican-plugins
+.. _pelican-plugins repository: https://github.com/getpelican/pelican-plugins
+
+Please note that while we do our best to review and maintain these plugins,
+they are submitted by the Pelican community and thus may have varying levels of
+support and interoperability.
+
+How to create plugins
+=====================
+
+Plugins are based on the concept of signals. Pelican sends signals, and plugins
+subscribe to those signals. The list of available signals is documented in a
+subsequent section.
+
+The only rule to follow for plugins is to define a ``register`` callable, in
+which you map the signals to your plugin logic. Let's take a simple example::
+
+ import logging
+
+ from pelican import signals
+
+ log = logging.getLogger(__name__)
+
+ def test(sender):
+ log.debug("%s initialized !!", sender)
+
+ def register():
+ signals.initialized.connect(test)
+
+.. note::
+
+ Signal receivers are weakly-referenced and thus must not be defined within
+ your ``register`` callable or they will be garbage-collected before the
+ signal is emitted.
+
+If multiple plugins connect to the same signal, there is no way to guarantee or
+control in which order the plugins will be executed. This is a limitation
+inherited from Blinker_, the dependency Pelican uses to implement signals.
+
+Namespace plugin structure
+--------------------------
+
+Namespace plugins must adhere to a certain structure in order to function
+properly. They need to be installable (i.e. contain ``setup.py`` or equivalent)
+and have a folder structure as follows::
+
+ myplugin
+ ├── pelican
+ │ └── plugins
+ │ └── myplugin
+ │ ├── __init__.py
+ │ └── ...
+ ├── ...
+ └── setup.py
+
+It is crucial that ``pelican`` or ``pelican/plugins`` folder **not**
+contain an ``__init__.py`` file. In fact, it is best to have those folders
+empty besides the listed folders in the above structure and keep your
+plugin related files contained solely in the ``pelican/plugins/myplugin``
+folder to avoid any issues.
+
+To easily set up the proper structure, a `cookiecutter template for plugins`_
+is provided. Refer to that project's README for instructions on how to use it.
+
+.. _cookiecutter template for plugins: https://github.com/getpelican/cookiecutter-pelican-plugin
+
+List of signals
+===============
+
+Here is the list of currently implemented signals:
+
+================================= ============================ ===========================================================================
+Signal Arguments Description
+================================= ============================ ===========================================================================
+initialized pelican object
+finalized pelican object invoked after all the generators are executed and just before pelican exits
+ useful for custom post processing actions, such as:
+ - minifying js/css assets.
+ - notify/ping search engines with an updated sitemap.
+generator_init generator invoked in the Generator.__init__
+all_generators_finalized generators invoked after all the generators are executed and before writing output
+readers_init readers invoked in the Readers.__init__
+article_generator_context article_generator, metadata
+article_generator_preread article_generator invoked before a article is read in ArticlesGenerator.generate_context;
+ use if code needs to do something before every article is parsed
+article_generator_init article_generator invoked in the ArticlesGenerator.__init__
+article_generator_pretaxonomy article_generator invoked before categories and tags lists are created
+ useful when e.g. modifying the list of articles to be generated
+ so that removed articles are not leaked in categories or tags
+article_generator_finalized article_generator invoked at the end of ArticlesGenerator.generate_context
+article_generator_write_article article_generator, content invoked before writing each article, the article is passed as content
+article_writer_finalized article_generator, writer invoked after all articles and related pages have been written, but before
+ the article generator is closed.
+get_generators pelican object invoked in Pelican.get_generator_classes,
+ can return a Generator, or several
+ generators in a tuple or in a list.
+get_writer pelican object invoked in Pelican.get_writer,
+ can return a custom Writer.
+page_generator_context page_generator, metadata
+page_generator_preread page_generator invoked before a page is read in PageGenerator.generate_context;
+ use if code needs to do something before every page is parsed.
+page_generator_init page_generator invoked in the PagesGenerator.__init__
+page_generator_finalized page_generator invoked at the end of PagesGenerator.generate_context
+page_generator_write_page page_generator, content invoked before writing each page, the page is passed as content
+page_writer_finalized page_generator, writer invoked after all pages have been written, but before the page generator
+ is closed.
+static_generator_context static_generator, metadata
+static_generator_preread static_generator invoked before a static file is read in StaticGenerator.generate_context;
+ use if code needs to do something before every static file is added to the
+ staticfiles list.
+static_generator_init static_generator invoked in the StaticGenerator.__init__
+static_generator_finalized static_generator invoked at the end of StaticGenerator.generate_context
+content_object_init content_object invoked at the end of Content.__init__
+content_written path, context invoked each time a content file is written.
+feed_generated context, feed invoked each time a feed gets generated. Can be used to modify a feed
+ object before it gets written.
+feed_written path, context, feed invoked each time a feed file is written.
+================================= ============================ ===========================================================================
+
+.. warning::
+
+ Avoid ``content_object_init`` signal if you intend to read ``summary`` or
+ ``content`` properties of the content object. That combination can result in
+ unresolved links when :ref:`ref-linking-to-internal-content` (see
+ `pelican-plugins bug #314`_). Use ``_summary`` and ``_content`` properties
+ instead, or, alternatively, run your plugin at a later stage (e.g.
+ ``all_generators_finalized``).
+
+.. note::
+
+ After Pelican 3.2, signal names were standardized. Older plugins may need
+ to be updated to use the new names:
+
+ ========================== ===========================
+ Old name New name
+ ========================== ===========================
+ article_generate_context article_generator_context
+ article_generate_finalized article_generator_finalized
+ article_generate_preread article_generator_preread
+ pages_generate_context page_generator_context
+ pages_generate_preread page_generator_preread
+ pages_generator_finalized page_generator_finalized
+ pages_generator_init page_generator_init
+ static_generate_context static_generator_context
+ static_generate_preread static_generator_preread
+ ========================== ===========================
+
+Recipes
+=======
+
+We eventually realised some of the recipes to create plugins would be best
+shared in the documentation somewhere, so here they are!
+
+How to create a new reader
+--------------------------
+
+One thing you might want is to add support for your very own input format.
+While it might make sense to add this feature in Pelican core, we wisely chose
+to avoid this situation and instead have the different readers defined via
+plugins.
+
+The rationale behind this choice is mainly that plugins are really easy to
+write and don't slow down Pelican itself when they're not active.
+
+No more talking — here is an example::
+
+ from pelican import signals
+ from pelican.readers import BaseReader
+
+ # Create a new reader class, inheriting from the pelican.reader.BaseReader
+ class NewReader(BaseReader):
+ enabled = True # Yeah, you probably want that :-)
+
+ # The list of file extensions you want this reader to match with.
+ # If multiple readers were to use the same extension, the latest will
+ # win (so the one you're defining here, most probably).
+ file_extensions = ['yeah']
+
+ # You need to have a read method, which takes a filename and returns
+ # some content and the associated metadata.
+ def read(self, filename):
+ metadata = {'title': 'Oh yeah',
+ 'category': 'Foo',
+ 'date': '2012-12-01'}
+
+ parsed = {}
+ for key, value in metadata.items():
+ parsed[key] = self.process_metadata(key, value)
+
+ return "Some content", parsed
+
+ def add_reader(readers):
+ readers.reader_classes['yeah'] = NewReader
+
+ # This is how pelican works.
+ def register():
+ signals.readers_init.connect(add_reader)
+
+
+Adding a new generator
+----------------------
+
+Adding a new generator is also really easy. You might want to have a look at
+:doc:`internals` for more information on how to create your own generator.
+
+::
+
+ def get_generators(pelican_object):
+ # define a new generator here if you need to
+ return MyGenerator
+
+ def register():
+ signals.get_generators.connect(get_generators)
+
+
+.. _Pip: https://pip.pypa.io/
+.. _pelican-plugins bug #314: https://github.com/getpelican/pelican-plugins/issues/314
+.. _Blinker: https://pythonhosted.org/blinker/
diff --git a/docs/publish.rst b/docs/publish.rst
new file mode 100644
index 00000000..ae1428f5
--- /dev/null
+++ b/docs/publish.rst
@@ -0,0 +1,210 @@
+Publish your site
+#################
+
+.. _site_generation:
+
+Site generation
+===============
+
+Once Pelican is installed and you have some content (e.g., in Markdown or reST
+format), you can convert your content into HTML via the ``pelican`` command,
+specifying the path to your content and (optionally) the path to your
+:doc:`settings` file::
+
+ pelican /path/to/your/content/ [-s path/to/your/settings.py]
+
+The above command will generate your site and save it in the ``output/``
+folder, using the default theme to produce a simple site. The default theme
+consists of very simple HTML without styling and is provided so folks may use
+it as a basis for creating their own themes.
+
+When working on a single article or page, it is possible to generate only the
+file that corresponds to that content. To do this, use the ``--write-selected``
+argument, like so::
+
+ pelican --write-selected output/posts/my-post-title.html
+
+Note that you must specify the path to the generated *output* file — not the
+source content. To determine the output file name and location, use the
+``--debug`` flag. If desired, ``--write-selected`` can take a comma-separated
+list of paths or can be configured as a setting. (See:
+:ref:`writing_only_selected_content`)
+
+You can also tell Pelican to watch for your modifications, instead of manually
+re-running it every time you want to see your changes. To enable this, run the
+``pelican`` command with the ``-r`` or ``--autoreload`` option. On non-Windows
+environments, this option can also be combined with the ``-l`` or ``--listen``
+option to simultaneously both auto-regenerate *and* serve the output at
+http://localhost:8000::
+
+ pelican --autoreload --listen
+
+Pelican has other command-line switches available. Have a look at the help to
+see all the options you can use::
+
+ pelican --help
+
+Viewing the generated files
+---------------------------
+
+The files generated by Pelican are static files, so you don't actually need
+anything special to view them. You can use your browser to open the generated
+HTML files directly::
+
+ firefox output/index.html
+
+Because the above method may have trouble locating your CSS and other linked
+assets, running Pelican's simple built-in web server will often provide a more
+reliable previewing experience::
+
+ pelican --listen
+
+Once the web server has been started, you can preview your site at:
+http://localhost:8000/
+
+Deployment
+==========
+
+After you have generated your site, previewed it in your local development
+environment, and are ready to deploy it to production, you might first
+re-generate your site with any production-specific settings (e.g., analytics
+feeds, etc.) that you may have defined::
+
+ pelican content -s publishconf.py
+
+To base your publish configuration on top of your ``pelicanconf.py``, you can
+import your ``pelicanconf`` settings by including the following line in your
+``publishconf.py``::
+
+ from pelicanconf import *
+
+If you have generated a ``publishconf.py`` using ``pelican-quickstart``, this
+line is included by default.
+
+The steps for deploying your site will depend on where it will be hosted. If
+you have SSH access to a server running Nginx or Apache, you might use the
+``rsync`` tool to transmit your site files::
+
+ rsync -avc --delete output/ host.example.com:/var/www/your-site/
+
+There are many other deployment options, some of which can be configured when
+first setting up your site via the ``pelican-quickstart`` command. See the
+:doc:`Tips` page for detail on publishing via GitHub Pages.
+
+Automation
+==========
+
+While the ``pelican`` command is the canonical way to generate your site,
+automation tools can be used to streamline the generation and publication flow.
+One of the questions asked during the ``pelican-quickstart`` process pertains
+to whether you want to automate site generation and publication. If you
+answered "yes" to that question, a ``tasks.py`` and ``Makefile`` will be
+generated in the root of your project. These files, pre-populated with certain
+information gleaned from other answers provided during the
+``pelican-quickstart`` process, are meant as a starting point and should be
+customized to fit your particular needs and usage patterns. If you find one or
+both of these automation tools to be of limited utility, these files can
+deleted at any time and will not affect usage of the canonical ``pelican``
+command.
+
+Following are automation tools that "wrap" the ``pelican`` command and can
+simplify the process of generating, previewing, and uploading your site.
+
+Invoke
+------
+
+The advantage of Invoke_ is that it is written in Python and thus can be used
+in a wide range of environments. The downside is that it must be installed
+separately. Use the following command to install Invoke, prefixing with
+``sudo`` if your environment requires it::
+
+ python -m pip install invoke
+
+Take a moment to open the ``tasks.py`` file that was generated in your project
+root. You will see a number of commands, any one of which can be renamed,
+removed, and/or customized to your liking. Using the out-of-the-box
+configuration, you can generate your site via::
+
+ invoke build
+
+If you'd prefer to have Pelican automatically regenerate your site every time a
+change is detected (which is handy when testing locally), use the following
+command instead::
+
+ invoke regenerate
+
+To serve the generated site so it can be previewed in your browser at
+http://localhost:8000/::
+
+ invoke serve
+
+To serve the generated site with automatic browser reloading every time a
+change is detected, first ``python -m pip install livereload``, then use the
+following command::
+
+ invoke livereload
+
+If during the ``pelican-quickstart`` process you answered "yes" when asked
+whether you want to upload your site via SSH, you can use the following command
+to publish your site via rsync over SSH::
+
+ invoke publish
+
+These are just a few of the commands available by default, so feel free to
+explore ``tasks.py`` and see what other commands are available. More
+importantly, don't hesitate to customize ``tasks.py`` to suit your specific
+needs and preferences.
+
+Make
+----
+
+A ``Makefile`` is also automatically created for you when you say "yes" to the
+relevant question during the ``pelican-quickstart`` process. The advantage of
+this method is that the ``make`` command is built into most POSIX systems and
+thus doesn't require installing anything else in order to use it. The downside
+is that non-POSIX systems (e.g., Windows) do not include ``make``, and
+installing it on those systems can be a non-trivial task.
+
+If you want to use ``make`` to generate your site using the settings in
+``pelicanconf.py``, run::
+
+ make html
+
+To generate the site for production, using the settings in ``publishconf.py``,
+run::
+
+ make publish
+
+If you'd prefer to have Pelican automatically regenerate your site every time a
+change is detected (which is handy when testing locally), use the following
+command instead::
+
+ make regenerate
+
+To serve the generated site so it can be previewed in your browser at
+http://localhost:8000/::
+
+ make serve
+
+Normally you would need to run ``make regenerate`` and ``make serve`` in two
+separate terminal sessions, but you can run both at once via::
+
+ make devserver
+
+The above command will simultaneously run Pelican in regeneration mode as well
+as serve the output at http://localhost:8000.
+
+When you're ready to publish your site, you can upload it via the method(s) you
+chose during the ``pelican-quickstart`` questionnaire. For this example, we'll
+use rsync over ssh::
+
+ make rsync_upload
+
+That's it! Your site should now be live.
+
+(The default ``Makefile`` and ``devserver.sh`` scripts use the ``python`` and
+``pelican`` executables to complete its tasks. If you want to use different
+executables, such as ``python3``, you can set the ``PY`` and ``PELICAN``
+environment variables, respectively, to override the default executable names.)
+
+.. _Invoke: https://www.pyinvoke.org/
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
new file mode 100644
index 00000000..ddc42651
--- /dev/null
+++ b/docs/quickstart.rst
@@ -0,0 +1,82 @@
+Quickstart
+##########
+
+Reading through all the documentation is highly recommended, but for the truly
+impatient, following are some quick steps to get started.
+
+Installation
+------------
+
+Install Pelican (and optionally Markdown if you intend to use it) on Python
+3.6+ by running the following command in your preferred terminal, prefixing
+with ``sudo`` if permissions warrant::
+
+ python -m pip install "pelican[markdown]"
+
+Create a project
+----------------
+
+First, choose a name for your project, create an appropriately-named directory
+for your site, and switch to that directory::
+
+ mkdir -p ~/projects/yoursite
+ cd ~/projects/yoursite
+
+Create a skeleton project via the ``pelican-quickstart`` command, which begins
+by asking some questions about your site::
+
+ pelican-quickstart
+
+For questions that have default values denoted in brackets, feel free to use
+the Return key to accept those default values [#tzlocal_fn]_. When asked for
+your URL prefix, enter your domain name as indicated (e.g.,
+``https://example.com``).
+
+Create an article
+-----------------
+
+You cannot run Pelican until you have created some content. Use your preferred
+text editor to create your first article with the following content::
+
+ Title: My First Review
+ Date: 2010-12-03 10:20
+ Category: Review
+
+ Following is a review of my favorite mechanical keyboard.
+
+Given that this example article is in Markdown format, save it as
+``~/projects/yoursite/content/keyboard-review.md``.
+
+Generate your site
+------------------
+
+From your project root directory, run the ``pelican`` command to generate your site::
+
+ pelican content
+
+Your site has now been generated inside the ``output/`` directory. (You may see
+a warning related to feeds, but that is normal when developing locally and can
+be ignored for now.)
+
+Preview your site
+-----------------
+
+Open a new terminal session, navigate to your project root directory, and
+run the following command to launch Pelican's web server::
+
+ pelican --listen
+
+Preview your site by navigating to http://localhost:8000/ in your browser.
+
+Continue reading the other documentation sections for more detail, and check
+out the Pelican wiki's Tutorials_ page for links to community-published
+tutorials.
+
+.. _Tutorials: https://github.com/getpelican/pelican/wiki/Tutorials
+
+Footnotes
+---------
+
+.. [#tzlocal_fn] You can help localize default fields by installing the
+ optional `tzlocal `_
+ module.
diff --git a/docs/report.rst b/docs/report.rst
new file mode 100644
index 00000000..278b5ab2
--- /dev/null
+++ b/docs/report.rst
@@ -0,0 +1,114 @@
+Some history about Pelican
+##########################
+
+.. warning::
+
+ This page comes from a report the original author (Alexis Métaireau) wrote
+ right after writing Pelican, in December 2010. The information may not be
+ up-to-date.
+
+Pelican is a simple static blog generator. It parses markup files (Markdown or
+reStructuredText for now) and generates an HTML folder with all the files in
+it. I've chosen to use Python to implement Pelican because it seemed to be
+simple and to fit to my needs. I did not wanted to define a class for each
+thing, but still wanted to keep my things loosely coupled. It turns out that it
+was exactly what I wanted. From time to time, thanks to the feedback of some
+users, it took me a very few time to provide fixes on it. So far, I've
+re-factored the Pelican code by two
+times; each time took less than 30 minutes.
+
+Use case
+========
+
+I was previously using WordPress, a solution you can host on a web server to
+manage your blog. Most of the time, I prefer using markup languages such as
+Markdown or reStructuredText to type my articles. To do so, I use vim. I think
+it is important to let the people choose the tool they want to write the
+articles. In my opinion, a blog manager should just allow you to take any kind
+of input and transform it to a weblog. That's what Pelican does. You can write
+your articles using the tool you want, and the markup language you want, and
+then generate a static HTML weblog.
+
+.. image:: _static/overall.png
+
+To be flexible enough, Pelican has template support, so you can easily write
+your own themes if you want to.
+
+Design process
+==============
+
+Pelican came from a need I have. I started by creating a single file
+application, and I have make it grow to support what it does by now. To start,
+I wrote a piece of documentation about what I wanted to do. Then, I created the
+content I wanted to parse (the reStructuredText files) and started
+experimenting with the code. Pelican was 200 lines long and contained almost
+ten functions and one class when it was first usable.
+
+I have been facing different problems all over the time and wanted to add
+features to Pelican while using it. The first change I have done was to add the
+support of a settings file. It is possible to pass the options to the command
+line, but can be tedious if there is a lot of them. In the same way, I have
+added the support of different things over time: Atom feeds, multiple themes,
+multiple markup support, etc. At some point, it appears that the "only one
+file" mantra was not good enough for Pelican, so I decided to rework a bit all
+that, and split this in multiple different files.
+
+I’ve separated the logic in different classes and concepts:
+
+* *writers* are responsible of all the writing process of the files.
+ They are responsible of writing .html files, RSS feeds and so on. Since those
+ operations are commonly used, the object is created once, and then passed to
+ the generators.
+
+* *readers* are used to read from various formats (Markdown and
+ reStructuredText for now, but the system is extensible). Given a file, they
+ return metadata (author, tags, category, etc) and content (HTML formatted).
+
+* *generators* generate the different outputs. For instance, Pelican
+ comes with an ArticlesGenerator and PagesGenerator, into others. Given a
+ configuration, they can do whatever you want them to do. Most of the time
+ it's generating files from inputs (user inputs and files).
+
+I also deal with contents objects. They can be ``Articles``, ``Pages``,
+``Quotes``, or whatever you want. They are defined in the ``contents.py``
+module and represent some content to be used by the program.
+
+In more detail
+==============
+
+Here is an overview of the classes involved in Pelican.
+
+.. image:: _static/uml.jpg
+
+The interface does not really exist, and I have added it only to clarify the
+whole picture. I do use duck typing and not interfaces.
+
+Internally, the following process is followed:
+
+* First of all, the command line is parsed, and some content from the user is
+ used to initialize the different generator objects.
+
+* A ``context`` is created. It contains the settings from the command line and
+ a settings file if provided.
+* The ``generate_context`` method of each generator is called, updating
+ the context.
+* The writer is created and given to the ``generate_output`` method of each
+ generator.
+
+I make two calls because it is important that when the output is generated by
+the generators, the context will not change. In other words, the first method
+``generate_context`` should modify the context, whereas the second
+``generate_output`` method should not.
+
+Then, it is up to the generators to do what the want, in the
+``generate_context`` and ``generate_content`` method. Taking the
+``ArticlesGenerator`` class will help to understand some others concepts. Here
+is what happens when calling the ``generate_context`` method:
+
+* Read the folder “path”, looking for restructured text files, load each of
+ them, and construct a content object (``Article``) with it. To do so, use
+ ``Reader`` objects.
+* Update the ``context`` with all those articles.
+
+Then, the ``generate_content`` method uses the ``context`` and the ``writer``
+to generate the wanted output.
diff --git a/docs/settings.rst b/docs/settings.rst
new file mode 100644
index 00000000..377051b0
--- /dev/null
+++ b/docs/settings.rst
@@ -0,0 +1,1401 @@
+Settings
+########
+
+Pelican is configurable thanks to a settings file you can pass to the command
+line::
+
+ pelican content -s path/to/your/pelicanconf.py
+
+If you used the ``pelican-quickstart`` command, your primary settings file will
+be named ``pelicanconf.py`` by default.
+
+You can also specify extra settings via ``-e`` / ``--extra-settings`` option
+flags, which will override default settings as well as any defined within
+settings files::
+
+ pelican content -e DELETE_OUTPUT_DIRECTORY=true
+
+.. note::
+
+ When experimenting with different settings (especially the metadata ones)
+ caching may interfere and the changes may not be visible. In such cases
+ disable caching with ``LOAD_CONTENT_CACHE = False`` or use the
+ ``--ignore-cache`` command-line switch.
+
+Settings are configured in the form of a Python module (a file). There is an
+`example settings file
+`_
+available for reference.
+
+To see a list of current settings in your environment, including both default
+and any customized values, run the following command (append one or more
+specific setting names as arguments to see values for those settings only)::
+
+ pelican --print-settings
+
+All the setting identifiers must be set in all-caps, otherwise they will not be
+processed. Setting values that are numbers (5, 20, etc.), booleans (True,
+False, None, etc.), dictionaries, or tuples should *not* be enclosed in
+quotation marks. All other values (i.e., strings) *must* be enclosed in
+quotation marks.
+
+Unless otherwise specified, settings that refer to paths can be either absolute
+or relative to the configuration file. The settings you define in the
+configuration file will be passed to the templates, which allows you to use
+your settings to add site-wide content.
+
+Here is a list of settings for Pelican:
+
+
+Basic settings
+==============
+
+.. data:: USE_FOLDER_AS_CATEGORY = True
+
+ When you don't specify a category in your post metadata, set this setting to
+ ``True``, and organize your articles in subfolders, the subfolder will
+ become the category of your post. If set to ``False``, ``DEFAULT_CATEGORY``
+ will be used as a fallback.
+
+.. data:: DEFAULT_CATEGORY = 'misc'
+
+ The default category to fall back on.
+
+.. data:: DISPLAY_PAGES_ON_MENU = True
+
+ Whether to display pages on the menu of the template. Templates may or may
+ not honor this setting.
+
+.. data:: DISPLAY_CATEGORIES_ON_MENU = True
+
+ Whether to display categories on the menu of the template. Templates may or
+ not honor this setting.
+
+.. data:: DOCUTILS_SETTINGS = {}
+
+ Extra configuration settings for the docutils publisher (applicable only to
+ reStructuredText). See `Docutils Configuration`_ settings for more details.
+
+.. data:: DELETE_OUTPUT_DIRECTORY = False
+
+ Delete the output directory, and **all** of its contents, before generating
+ new files. This can be useful in preventing older, unnecessary files from
+ persisting in your output. However, **this is a destructive setting and
+ should be handled with extreme care.**
+
+.. data:: OUTPUT_RETENTION = []
+
+ A list of filenames that should be retained and not deleted from the output
+ directory. One use case would be the preservation of version control data.
+
+ Example::
+
+ OUTPUT_RETENTION = [".hg", ".git", ".bzr"]
+
+.. data:: JINJA_ENVIRONMENT = {'trim_blocks': True, 'lstrip_blocks': True}
+
+ A dictionary of custom Jinja2 environment variables you want to use. This
+ also includes a list of extensions you may want to include. See `Jinja
+ Environment documentation`_.
+
+.. data:: JINJA_FILTERS = {}
+
+ A dictionary of custom Jinja2 filters you want to use. The dictionary
+ should map the filtername to the filter function.
+
+ Example::
+ import sys
+ sys.path.append('to/your/path')
+
+ from custom_filter import urlencode_filter
+ JINJA_FILTERS = {'urlencode': urlencode_filter}
+
+ See: `Jinja custom filters documentation`_.
+
+.. data:: JINJA_GLOBALS = {}
+
+ A dictionary of custom objects to map into the Jinja2 global environment
+ namespace. The dictionary should map the global name to the global
+ variable/function. See: `Jinja global namespace documentation`_.
+
+.. data:: JINJA_TESTS = {}
+
+ A dictionary of custom Jinja2 tests you want to use. The dictionary should
+ map test names to test functions. See: `Jinja custom tests documentation`_.
+
+.. data:: LOG_FILTER = []
+
+ A list of tuples containing the logging level (up to ``warning``) and the
+ message to be ignored.
+
+ Example::
+
+ LOG_FILTER = [(logging.WARN, 'TAG_SAVE_AS is set to False')]
+
+.. data:: READERS = {}
+
+ A dictionary of file extensions / Reader classes for Pelican to process or
+ ignore.
+
+ For example, to avoid processing .html files, set::
+
+ READERS = {'html': None}
+
+ To add a custom reader for the ``foo`` extension, set::
+
+ READERS = {'foo': FooReader}
+
+.. data:: IGNORE_FILES = ['.#*']
+
+ A list of glob patterns. Files and directories matching any of these
+ patterns will be ignored by the processor. For example, the default
+ ``['.#*']`` will ignore emacs lock files, and ``['__pycache__']`` would
+ ignore Python 3's bytecode caches.
+
+.. data:: MARKDOWN = {...}
+
+ Extra configuration settings for the Markdown processor. Refer to the Python
+ Markdown documentation's `Options section
+ `_ for a complete
+ list of supported options. The ``extensions`` option will be automatically
+ computed from the ``extension_configs`` option.
+
+ Defaults to::
+
+ MARKDOWN = {
+ 'extension_configs': {
+ 'markdown.extensions.codehilite': {'css_class': 'highlight'},
+ 'markdown.extensions.extra': {},
+ 'markdown.extensions.meta': {},
+ },
+ 'output_format': 'html5',
+ }
+
+ .. Note::
+ The dictionary defined in your settings file will replace this default
+ one.
+
+.. data:: OUTPUT_PATH = 'output/'
+
+ Where to output the generated files. This should correspond to your web
+ server's virtual host root directory.
+
+.. data:: PATH
+
+ Path to content directory to be processed by Pelican. If undefined, and
+ content path is not specified via an argument to the ``pelican`` command,
+ Pelican will use the current working directory.
+
+.. data:: PAGE_PATHS = ['pages']
+
+ A list of directories and files to look at for pages, relative to ``PATH``.
+
+.. data:: PAGE_EXCLUDES = []
+
+ A list of directories to exclude when looking for pages in addition to
+ ``ARTICLE_PATHS``.
+
+.. data:: ARTICLE_PATHS = ['']
+
+ A list of directories and files to look at for articles, relative to
+ ``PATH``.
+
+.. data:: ARTICLE_EXCLUDES = []
+
+ A list of directories to exclude when looking for articles in addition to
+ ``PAGE_PATHS``.
+
+.. data:: OUTPUT_SOURCES = False
+
+ Set to True if you want to copy the articles and pages in their original
+ format (e.g. Markdown or reStructuredText) to the specified ``OUTPUT_PATH``.
+
+.. data:: OUTPUT_SOURCES_EXTENSION = '.text'
+
+ Controls the extension that will be used by the SourcesGenerator. Defaults
+ to ``.text``. If not a valid string the default value will be used.
+
+.. data:: PLUGINS = None
+
+ The list of plugins to load. See :ref:`plugins`.
+
+.. data:: PLUGIN_PATHS = []
+
+ A list of directories where to look for plugins. See :ref:`plugins`.
+
+.. data:: SITENAME = 'A Pelican Blog'
+
+ Your site name
+
+.. data:: SITEURL
+
+ Base URL of your web site. Not defined by default, so it is best to specify
+ your SITEURL; if you do not, feeds will not be generated with
+ properly-formed URLs. If your site is available via HTTPS, this setting
+ should begin with ``https://`` — otherwise use ``http://``. Then append your
+ domain, with no trailing slash at the end. Example: ``SITEURL =
+ 'https://example.com'``
+
+.. data:: STATIC_PATHS = ['images']
+
+ A list of directories (relative to ``PATH``) in which to look for static
+ files. Such files will be copied to the output directory without
+ modification. Articles, pages, and other content source files will normally
+ be skipped, so it is safe for a directory to appear both here and in
+ ``PAGE_PATHS`` or ``ARTICLE_PATHS``. Pelican's default settings include the
+ "images" directory here.
+
+.. data:: STATIC_EXCLUDES = []
+
+ A list of directories to exclude when looking for static files.
+
+.. data:: STATIC_EXCLUDE_SOURCES = True
+
+ If set to False, content source files will not be skipped when copying files
+ found in ``STATIC_PATHS``. This setting is for backward compatibility with
+ Pelican releases before version 3.5. It has no effect unless
+ ``STATIC_PATHS`` contains a directory that is also in ``ARTICLE_PATHS`` or
+ ``PAGE_PATHS``. If you are trying to publish your site's source files,
+ consider using the ``OUTPUT_SOURCES`` setting instead.
+
+.. data:: STATIC_CREATE_LINKS = False
+
+ Create links instead of copying files. If the content and output directories
+ are on the same device, then create hard links. Falls back to symbolic
+ links if the output directory is on a different filesystem. If symlinks are
+ created, don't forget to add the ``-L`` or ``--copy-links`` option to rsync
+ when uploading your site.
+
+.. data:: STATIC_CHECK_IF_MODIFIED = False
+
+ If set to ``True``, and ``STATIC_CREATE_LINKS`` is ``False``, compare mtimes
+ of content and output files, and only copy content files that are newer than
+ existing output files.
+
+.. data:: TYPOGRIFY = False
+
+ If set to True, several typographical improvements will be incorporated into
+ the generated HTML via the `Typogrify
+ `_ library, which can be installed
+ via: ``python -m pip install typogrify``
+
+.. data:: TYPOGRIFY_IGNORE_TAGS = []
+
+ A list of tags for Typogrify to ignore. By default Typogrify will ignore
+ ``pre`` and ``code`` tags. This requires that Typogrify version 2.0.4 or
+ later is installed
+
+.. data:: TYPOGRIFY_DASHES = 'default'
+
+ This setting controls how Typogrify sets up the Smartypants filter to
+ interpret multiple dash/hyphen/minus characters. A single ASCII dash
+ character (``-``) is always rendered as a hyphen. The ``default`` setting
+ does not handle en-dashes and converts double-hyphens into em-dashes. The
+ ``oldschool`` setting renders both en-dashes and em-dashes when it sees two
+ (``--``) and three (``---``) hyphen characters, respectively. The
+ ``oldschool_inverted`` setting turns two hyphens into an em-dash and three
+ hyphens into an en-dash.
+
+.. data:: SUMMARY_MAX_LENGTH = 50
+
+ When creating a short summary of an article, this will be the default length
+ (measured in words) of the text created. This only applies if your content
+ does not otherwise specify a summary. Setting to ``None`` will cause the
+ summary to be a copy of the original content.
+
+.. data:: SUMMARY_END_SUFFIX = '…'
+
+ When creating a short summary of an article and the result was truncated to
+ match the required word length, this will be used as the truncation suffix.
+
+.. data:: WITH_FUTURE_DATES = True
+
+ If disabled, content with dates in the future will get a default status of
+ ``draft``. See :ref:`reading_only_modified_content` for caveats.
+
+.. data:: INTRASITE_LINK_REGEX = '[{|](?P.*?)[|}]'
+
+ Regular expression that is used to parse internal links. Default syntax when
+ linking to internal files, tags, etc., is to enclose the identifier, say
+ ``filename``, in ``{}`` or ``||``. Identifier between ``{`` and ``}`` goes
+ into the ``what`` capturing group. For details see
+ :ref:`ref-linking-to-internal-content`.
+
+.. data:: PYGMENTS_RST_OPTIONS = []
+
+ A list of default Pygments settings for your reStructuredText code blocks.
+ See :ref:`internal_pygments_options` for a list of supported options.
+
+.. data:: CACHE_CONTENT = False
+
+ If ``True``, saves content in caches. See
+ :ref:`reading_only_modified_content` for details about caching.
+
+.. data:: CONTENT_CACHING_LAYER = 'reader'
+
+ If set to ``'reader'``, save only the raw content and metadata returned by
+ readers. If set to ``'generator'``, save processed content objects.
+
+.. data:: CACHE_PATH = 'cache'
+
+ Directory in which to store cache files.
+
+.. data:: GZIP_CACHE = True
+
+ If ``True``, use gzip to (de)compress the cache files.
+
+.. data:: CHECK_MODIFIED_METHOD = 'mtime'
+
+ Controls how files are checked for modifications.
+
+.. data:: LOAD_CONTENT_CACHE = False
+
+ If ``True``, load unmodified content from caches.
+
+.. data:: WRITE_SELECTED = []
+
+ If this list is not empty, **only** output files with their paths in this
+ list are written. Paths should be either absolute or relative to the current
+ Pelican working directory. For possible use cases see
+ :ref:`writing_only_selected_content`.
+
+.. data:: FORMATTED_FIELDS = ['summary']
+
+ A list of metadata fields containing reST/Markdown content to be parsed and
+ translated to HTML.
+
+.. data:: PORT = 8000
+
+ The TCP port to serve content from the output folder via HTTP when pelican
+ is run with --listen
+
+.. data:: BIND = ''
+
+ The IP to which to bind the HTTP server.
+
+.. _url-settings:
+
+URL settings
+============
+
+The first thing to understand is that there are currently two supported methods
+for URL formation: *relative* and *absolute*. Relative URLs are useful when
+testing locally, and absolute URLs are reliable and most useful when
+publishing. One method of supporting both is to have one Pelican configuration
+file for local development and another for publishing. To see an example of
+this type of setup, use the ``pelican-quickstart`` script as described in the
+:doc:`Installation ` section, which will produce two separate
+configuration files for local development and publishing, respectively.
+
+You can customize the URLs and locations where files will be saved. The
+``*_URL`` and ``*_SAVE_AS`` variables use Python's format strings. These
+variables allow you to place your articles in a location such as
+``{slug}/index.html`` and link to them as ``{slug}`` for clean URLs (see
+example below). These settings give you the flexibility to place your articles
+and pages anywhere you want.
+
+.. note::
+ If a ``*_SAVE_AS`` setting contains a parent directory that doesn't match
+ the parent directory inside the corresponding ``*_URL`` setting, this may
+ cause Pelican to generate unexpected URLs in a few cases, such as when
+ using the ``{attach}`` syntax.
+
+If you don't want that flexibility and instead prefer that your generated
+output paths mirror your source content's filesystem path hierarchy, try the
+following settings::
+
+ PATH_METADATA = '(?P.*)\..*'
+ ARTICLE_URL = ARTICLE_SAVE_AS = PAGE_URL = PAGE_SAVE_AS = '{path_no_ext}.html'
+
+Otherwise, you can use a variety of file metadata attributes within URL-related
+settings:
+
+* slug
+* date
+* lang
+* author
+* category
+
+Example usage::
+
+ ARTICLE_URL = 'posts/{date:%Y}/{date:%b}/{date:%d}/{slug}/'
+ ARTICLE_SAVE_AS = 'posts/{date:%Y}/{date:%b}/{date:%d}/{slug}/index.html'
+ PAGE_URL = 'pages/{slug}/'
+ PAGE_SAVE_AS = 'pages/{slug}/index.html'
+
+This would save your articles into something like
+``/posts/2011/Aug/07/sample-post/index.html``, save your pages into
+``/pages/about/index.html``, and render them available at URLs of
+``/posts/2011/Aug/07/sample-post/`` and ``/pages/about/``, respectively.
+
+.. note::
+ If you specify a ``datetime`` directive, it will be substituted using the
+ input files' date metadata attribute. If the date is not specified for a
+ particular file, Pelican will rely on the file's ``mtime`` timestamp. Check
+ the `Python datetime documentation`_ for more information.
+
+.. _Python datetime documentation:
+ https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior
+
+.. data:: RELATIVE_URLS = False
+
+ Defines whether Pelican should use document-relative URLs or not. Only set
+ this to ``True`` when developing/testing and only if you fully understand
+ the effect it can have on links/feeds.
+
+.. data:: ARTICLE_URL = '{slug}.html'
+
+ The URL to refer to an article.
+
+.. data:: ARTICLE_SAVE_AS = '{slug}.html'
+
+ The place where we will save an article.
+
+.. data:: ARTICLE_LANG_URL = '{slug}-{lang}.html'
+
+ The URL to refer to an article which doesn't use the default language.
+
+.. data:: ARTICLE_LANG_SAVE_AS = '{slug}-{lang}.html'
+
+ The place where we will save an article which doesn't use the default
+ language.
+
+.. data:: DRAFT_URL = 'drafts/{slug}.html'
+
+ The URL to refer to an article draft.
+
+.. data:: DRAFT_SAVE_AS = 'drafts/{slug}.html'
+
+ The place where we will save an article draft.
+
+.. data:: DRAFT_LANG_URL = 'drafts/{slug}-{lang}.html'
+
+ The URL to refer to an article draft which doesn't use the default language.
+
+.. data:: DRAFT_LANG_SAVE_AS = 'drafts/{slug}-{lang}.html'
+
+ The place where we will save an article draft which doesn't use the default
+ language.
+
+.. data:: PAGE_URL = 'pages/{slug}.html'
+
+ The URL we will use to link to a page.
+
+.. data:: PAGE_SAVE_AS = 'pages/{slug}.html'
+
+ The location we will save the page. This value has to be the same as
+ PAGE_URL or you need to use a rewrite in your server config.
+
+.. data:: PAGE_LANG_URL = 'pages/{slug}-{lang}.html'
+
+ The URL we will use to link to a page which doesn't use the default
+ language.
+
+.. data:: PAGE_LANG_SAVE_AS = 'pages/{slug}-{lang}.html'
+
+ The location we will save the page which doesn't use the default language.
+
+.. data:: DRAFT_PAGE_URL = 'drafts/pages/{slug}.html'
+
+ The URL used to link to a page draft.
+
+.. data:: DRAFT_PAGE_SAVE_AS = 'drafts/pages/{slug}.html'
+
+ The actual location a page draft is saved at.
+
+.. data:: DRAFT_PAGE_LANG_URL = 'drafts/pages/{slug}-{lang}.html'
+
+ The URL used to link to a page draft which doesn't use the default
+ language.
+
+.. data:: DRAFT_PAGE_LANG_SAVE_AS = 'drafts/pages/{slug}-{lang}.html'
+
+ The actual location a page draft which doesn't use the default language is
+ saved at.
+
+.. data:: AUTHOR_URL = 'author/{slug}.html'
+
+ The URL to use for an author.
+
+.. data:: AUTHOR_SAVE_AS = 'author/{slug}.html'
+
+ The location to save an author.
+
+.. data:: CATEGORY_URL = 'category/{slug}.html'
+
+ The URL to use for a category.
+
+.. data:: CATEGORY_SAVE_AS = 'category/{slug}.html'
+
+ The location to save a category.
+
+.. data:: TAG_URL = 'tag/{slug}.html'
+
+ The URL to use for a tag.
+
+.. data:: TAG_SAVE_AS = 'tag/{slug}.html'
+
+ The location to save the tag page.
+
+.. note::
+
+ If you do not want one or more of the default pages to be created (e.g.,
+ you are the only author on your site and thus do not need an Authors page),
+ set the corresponding ``*_SAVE_AS`` setting to ``''`` to prevent the
+ relevant page from being generated.
+
+Pelican can optionally create per-year, per-month, and per-day archives of your
+posts. These secondary archives are disabled by default but are automatically
+enabled if you supply format strings for their respective ``_SAVE_AS``
+settings. Period archives fit intuitively with the hierarchical model of web
+URLs and can make it easier for readers to navigate through the posts you've
+written over time.
+
+Example usage::
+
+ YEAR_ARCHIVE_SAVE_AS = 'posts/{date:%Y}/index.html'
+ MONTH_ARCHIVE_SAVE_AS = 'posts/{date:%Y}/{date:%b}/index.html'
+
+With these settings, Pelican will create an archive of all your posts for the
+year at (for instance) ``posts/2011/index.html`` and an archive of all your
+posts for the month at ``posts/2011/Aug/index.html``.
+
+.. note::
+ Period archives work best when the final path segment is ``index.html``.
+ This way a reader can remove a portion of your URL and automatically arrive
+ at an appropriate archive of posts, without having to specify a page name.
+
+.. data:: YEAR_ARCHIVE_URL = ''
+
+ The URL to use for per-year archives of your posts. Used only if you have
+ the ``{url}`` placeholder in ``PAGINATION_PATTERNS``.
+
+.. data:: YEAR_ARCHIVE_SAVE_AS = ''
+
+ The location to save per-year archives of your posts.
+
+.. data:: MONTH_ARCHIVE_URL = ''
+
+ The URL to use for per-month archives of your posts. Used only if you have
+ the ``{url}`` placeholder in ``PAGINATION_PATTERNS``.
+
+.. data:: MONTH_ARCHIVE_SAVE_AS = ''
+
+ The location to save per-month archives of your posts.
+
+.. data:: DAY_ARCHIVE_URL = ''
+
+ The URL to use for per-day archives of your posts. Used only if you have the
+ ``{url}`` placeholder in ``PAGINATION_PATTERNS``.
+
+.. data:: DAY_ARCHIVE_SAVE_AS = ''
+
+ The location to save per-day archives of your posts.
+
+``DIRECT_TEMPLATES`` work a bit differently than noted above. Only the
+``_SAVE_AS`` settings are available, but it is available for any direct
+template.
+
+.. data:: ARCHIVES_SAVE_AS = 'archives.html'
+
+ The location to save the article archives page.
+
+.. data:: AUTHORS_SAVE_AS = 'authors.html'
+
+ The location to save the author list.
+
+.. data:: CATEGORIES_SAVE_AS = 'categories.html'
+
+ The location to save the category list.
+
+.. data:: TAGS_SAVE_AS = 'tags.html'
+
+ The location to save the tag list.
+
+.. data:: INDEX_SAVE_AS = 'index.html'
+
+ The location to save the list of all articles.
+
+URLs for direct template pages are theme-dependent. Some themes use
+corresponding ``*_URL`` setting as string, while others hard-code them:
+``'archives.html'``, ``'authors.html'``, ``'categories.html'``,
+``'tags.html'``.
+
+.. data:: SLUGIFY_SOURCE = 'title'
+
+ Specifies from where you want the slug to be automatically generated. Can be
+ set to ``title`` to use the "Title:" metadata tag or ``basename`` to use the
+ article's file name when creating the slug.
+
+.. data:: SLUGIFY_USE_UNICODE = False
+
+ Allow Unicode characters in slugs. Set ``True`` to keep Unicode characters
+ in auto-generated slugs. Otherwise, Unicode characters will be replaced
+ with ASCII equivalents.
+
+.. data:: SLUGIFY_PRESERVE_CASE = False
+
+ Preserve uppercase characters in slugs. Set ``True`` to keep uppercase
+ characters from ``SLUGIFY_SOURCE`` as-is.
+
+.. data:: 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 '-'
+ ]
+
+ Regex substitutions to make when generating slugs of articles and pages.
+ Specified as a list of pairs of ``(from, to)`` which are applied in order,
+ ignoring case. The default substitutions have the effect of removing
+ non-alphanumeric characters and converting internal whitespace to dashes.
+ Apart from these substitutions, slugs are always converted to lowercase
+ ascii characters and leading and trailing whitespace is stripped. Useful for
+ backward compatibility with existing URLs.
+
+.. data:: AUTHOR_REGEX_SUBSTITUTIONS = SLUG_REGEX_SUBSTITUTIONS
+
+ Regex substitutions for author slugs. Defaults to
+ ``SLUG_REGEX_SUBSTITUTIONS``.
+
+.. data:: CATEGORY_REGEX_SUBSTITUTIONS = SLUG_REGEX_SUBSTITUTIONS
+
+ Regex substitutions for category slugs. Defaults to
+ ``SLUG_REGEX_SUBSTITUTIONS``.
+
+.. data:: TAG_REGEX_SUBSTITUTIONS = SLUG_REGEX_SUBSTITUTIONS
+
+ Regex substitutions for tag slugs. Defaults to ``SLUG_REGEX_SUBSTITUTIONS``.
+
+Time and Date
+=============
+
+.. data:: TIMEZONE
+
+ The timezone used in the date information, to generate Atom and RSS feeds.
+
+ If no timezone is defined, UTC is assumed. This means that the generated
+ Atom and RSS feeds will contain incorrect date information if your locale is
+ not UTC.
+
+ Pelican issues a warning in case this setting is not defined, as it was not
+ mandatory in previous versions.
+
+ Have a look at `the wikipedia page`_ to get a list of valid timezone values.
+
+.. _the wikipedia page: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
+
+.. data:: DEFAULT_DATE = None
+
+ The default date you want to use. If ``'fs'``, Pelican will use the file
+ system timestamp information (mtime) if it can't get date information from
+ the metadata. If given any other string, it will be parsed by the same
+ method as article metadata. If set to a tuple object, the default datetime
+ object will instead be generated by passing the tuple to the
+ ``datetime.datetime`` constructor.
+
+.. data:: DEFAULT_DATE_FORMAT = '%a %d %B %Y'
+
+ The default date format you want to use.
+
+.. data:: DATE_FORMATS = {}
+
+ If you manage multiple languages, you can set the date formatting here.
+
+ If no ``DATE_FORMATS`` are set, Pelican will fall back to
+ ``DEFAULT_DATE_FORMAT``. If you need to maintain multiple languages with
+ different date formats, you can set the ``DATE_FORMATS`` dictionary using
+ the language name (``lang`` metadata in your post content) as the key.
+
+ In addition to the standard C89 strftime format codes that are listed in
+ `Python datetime documentation`_, you can use the ``-`` character between
+ ``%`` and the format character to remove any leading zeros. For example,
+ ``%d/%m/%Y`` will output ``01/01/2014`` whereas ``%-d/%-m/%Y`` will result
+ in ``1/1/2014``.
+
+ .. parsed-literal::
+
+ DATE_FORMATS = {
+ 'en': '%a, %d %b %Y',
+ 'jp': '%Y-%m-%d(%a)',
+ }
+
+ It is also possible to set different locale settings for each language by
+ using a ``(locale, format)`` tuple as a dictionary value which will override
+ the ``LOCALE`` setting:
+
+ .. parsed-literal::
+
+ # On Unix/Linux
+ DATE_FORMATS = {
+ 'en': ('en_US','%a, %d %b %Y'),
+ 'jp': ('ja_JP','%Y-%m-%d(%a)'),
+ }
+
+ # On Windows
+ DATE_FORMATS = {
+ 'en': ('usa','%a, %d %b %Y'),
+ 'jp': ('jpn','%Y-%m-%d(%a)'),
+ }
+
+.. data:: LOCALE
+
+ Change the locale [#]_. A list of locales can be provided here or a single
+ string representing one locale. When providing a list, all the locales will
+ be tried until one works.
+
+ You can set locale to further control date format:
+
+ .. parsed-literal::
+
+ LOCALE = ('usa', 'jpn', # On Windows
+ 'en_US', 'ja_JP' # On Unix/Linux
+ )
+
+ For a list of available locales refer to `locales on Windows`_ or on
+ Unix/Linux, use the ``locale -a`` command; see manpage
+ `locale(1)`_ for more information.
+
+
+.. [#] Default is the system locale.
+
+.. _Jinja custom filters documentation: https://jinja.palletsprojects.com/en/master/api/#custom-filters
+.. _Jinja global namespace documentation: https://jinja.palletsprojects.com/en/master/api/#the-global-namespace
+.. _Jinja custom tests documentation: https://jinja.palletsprojects.com/en/master/api/#custom-tests
+
+.. _locales on Windows: https://www.microsoft.com/en-us/download/details.aspx?id=55979
+
+.. _locale(1): https://linux.die.net/man/1/locale
+
+
+.. _template_pages:
+
+Template pages
+==============
+
+.. data:: TEMPLATE_PAGES = None
+
+ A mapping containing template pages that will be rendered with the blog
+ entries.
+
+ If you want to generate custom pages besides your blog entries, you can
+ point any Jinja2 template file with a path pointing to the file and the
+ destination path for the generated file.
+
+ For instance, if you have a blog with three static pages — a list of books,
+ your resume, and a contact page — you could have::
+
+ TEMPLATE_PAGES = {'src/books.html': 'dest/books.html',
+ 'src/resume.html': 'dest/resume.html',
+ 'src/contact.html': 'dest/contact.html'}
+
+.. data:: TEMPLATE_EXTENSIONS = ['.html']
+
+ The extensions to use when looking up template files from template names.
+
+.. data:: DIRECT_TEMPLATES = ['index', 'authors', 'categories', 'tags', 'archives']
+
+ List of templates that are used directly to render content. Typically direct
+ templates are used to generate index pages for collections of content (e.g.,
+ category and tag index pages). If the author, category and tag collections are not
+ needed, set ``DIRECT_TEMPLATES = ['index', 'archives']``
+
+ ``DIRECT_TEMPLATES`` are searched for over paths maintained in
+ ``THEME_TEMPLATES_OVERRIDES``.
+
+
+Metadata
+========
+
+.. data:: AUTHOR
+
+ Default author (usually your name).
+
+.. data:: DEFAULT_METADATA = {}
+
+ The default metadata you want to use for all articles and pages.
+
+.. data:: FILENAME_METADATA = r'(?P\d{4}-\d{2}-\d{2}).*'
+
+ The regexp that will be used to extract any metadata from the filename. All
+ named groups that are matched will be set in the metadata object. The
+ default value will only extract the date from the filename.
+
+ For example, to extract both the date and the slug::
+
+ FILENAME_METADATA = r'(?P\d{4}-\d{2}-\d{2})_(?P.*)'
+
+ See also ``SLUGIFY_SOURCE``.
+
+.. data:: PATH_METADATA = ''
+
+ Like ``FILENAME_METADATA``, but parsed from a page's full path relative to
+ the content source directory.
+
+.. data:: EXTRA_PATH_METADATA = {}
+
+ Extra metadata dictionaries keyed by relative path. Relative paths require
+ correct OS-specific directory separators (i.e. / in UNIX and \\ in Windows)
+ unlike some other Pelican file settings. Paths to a directory apply to all
+ files under it. The most-specific path wins conflicts.
+
+Not all metadata needs to be :ref:`embedded in source file itself
+`. For example, blog posts are often named following a
+``YYYY-MM-DD-SLUG.rst`` pattern, or nested into ``YYYY/MM/DD-SLUG``
+directories. To extract metadata from the filename or path, set
+``FILENAME_METADATA`` or ``PATH_METADATA`` to regular expressions that use
+Python's `group name notation`_ ``(?P…)``. If you want to attach
+additional metadata but don't want to encode it in the path, you can set
+``EXTRA_PATH_METADATA``:
+
+.. parsed-literal::
+
+ EXTRA_PATH_METADATA = {
+ 'relative/path/to/file-1': {
+ 'key-1a': 'value-1a',
+ 'key-1b': 'value-1b',
+ },
+ 'relative/path/to/file-2': {
+ 'key-2': 'value-2',
+ },
+ }
+
+This can be a convenient way to shift the installed location of a particular
+file:
+
+.. parsed-literal::
+
+ # Take advantage of the following defaults
+ # STATIC_SAVE_AS = '{path}'
+ # STATIC_URL = '{path}'
+ STATIC_PATHS = [
+ 'static/robots.txt',
+ ]
+ EXTRA_PATH_METADATA = {
+ 'static/robots.txt': {'path': 'robots.txt'},
+ }
+
+.. _group name notation:
+ https://docs.python.org/3/library/re.html#regular-expression-syntax
+
+
+Feed settings
+=============
+
+By default, Pelican uses Atom feeds. However, it is also possible to use RSS
+feeds if you prefer.
+
+Pelican generates category feeds as well as feeds for all your articles. It
+does not generate feeds for tags by default, but it is possible to do so using
+the ``TAG_FEED_ATOM`` and ``TAG_FEED_RSS`` settings:
+
+.. data:: FEED_DOMAIN = None, i.e. base URL is "/"
+
+ The domain prepended to feed URLs. Since feed URLs should always be
+ absolute, it is highly recommended to define this (e.g.,
+ "https://feeds.example.com"). If you have already explicitly defined SITEURL
+ (see above) and want to use the same domain for your feeds, you can just
+ set: ``FEED_DOMAIN = SITEURL``.
+
+.. data:: FEED_ATOM = None, i.e. no Atom feed
+
+ The location to save the Atom feed.
+
+.. data:: FEED_ATOM_URL = None
+
+ Relative URL of the Atom feed. If not set, ``FEED_ATOM`` is used both for
+ save location and URL.
+
+.. data:: FEED_RSS = None, i.e. no RSS
+
+ The location to save the RSS feed.
+
+.. data:: FEED_RSS_URL = None
+
+ Relative URL of the RSS feed. If not set, ``FEED_RSS`` is used both for save
+ location and URL.
+
+.. data:: FEED_ALL_ATOM = 'feeds/all.atom.xml'
+
+ The location to save the all-posts Atom feed: this feed will contain all
+ posts regardless of their language.
+
+.. data:: FEED_ALL_ATOM_URL = None
+
+ Relative URL of the all-posts Atom feed. If not set, ``FEED_ALL_ATOM`` is
+ used both for save location and URL.
+
+.. data:: FEED_ALL_RSS = None, i.e. no all-posts RSS
+
+ The location to save the the all-posts RSS feed: this feed will contain all
+ posts regardless of their language.
+
+.. data:: FEED_ALL_RSS_URL = None
+
+ Relative URL of the all-posts RSS feed. If not set, ``FEED_ALL_RSS`` is used
+ both for save location and URL.
+
+.. data:: CATEGORY_FEED_ATOM = 'feeds/{slug}.atom.xml'
+
+ The location to save the category Atom feeds. [2]_
+
+.. data:: CATEGORY_FEED_ATOM_URL = None
+
+ Relative URL of the category Atom feeds, including the ``{slug}``
+ placeholder. [2]_ If not set, ``CATEGORY_FEED_ATOM`` is used both for save
+ location and URL.
+
+.. data:: CATEGORY_FEED_RSS = None, i.e. no RSS
+
+ The location to save the category RSS feeds, including the ``{slug}``
+ placeholder. [2]_
+
+.. data:: CATEGORY_FEED_RSS_URL = None
+
+ Relative URL of the category RSS feeds, including the ``{slug}``
+ placeholder. [2]_ If not set, ``CATEGORY_FEED_RSS`` is used both for save
+ location and URL.
+
+.. data:: AUTHOR_FEED_ATOM = 'feeds/{slug}.atom.xml'
+
+ The location to save the author Atom feeds. [2]_
+
+.. data:: AUTHOR_FEED_ATOM_URL = None
+
+ Relative URL of the author Atom feeds, including the ``{slug}`` placeholder.
+ [2]_ If not set, ``AUTHOR_FEED_ATOM`` is used both for save location and
+ URL.
+
+.. data:: AUTHOR_FEED_RSS = 'feeds/{slug}.rss.xml'
+
+ The location to save the author RSS feeds. [2]_
+
+.. data:: AUTHOR_FEED_RSS_URL = None
+
+ Relative URL of the author RSS feeds, including the ``{slug}`` placeholder.
+ [2]_ If not set, ``AUTHOR_FEED_RSS`` is used both for save location and URL.
+
+.. data:: TAG_FEED_ATOM = None, i.e. no tag feed
+
+ The location to save the tag Atom feed, including the ``{slug}``
+ placeholder. [2]_
+
+.. data:: TAG_FEED_ATOM_URL = None
+
+ Relative URL of the tag Atom feed, including the ``{slug}`` placeholder.
+ [2]_
+
+.. data:: TAG_FEED_RSS = None, i.e. no RSS tag feed
+
+ Relative URL to output the tag RSS feed, including the ``{slug}``
+ placeholder. If not set, ``TAG_FEED_RSS`` is used both for save location and
+ URL.
+
+.. data:: FEED_MAX_ITEMS
+
+ Maximum number of items allowed in a feed. Feed item quantity is
+ unrestricted by default.
+
+.. data:: RSS_FEED_SUMMARY_ONLY = True
+
+ Only include item summaries in the ``description`` tag of RSS feeds. If set
+ to ``False``, the full content will be included instead. This setting
+ doesn't affect Atom feeds, only RSS ones.
+
+If you don't want to generate some or any of these feeds, set the above
+variables to ``None``.
+
+.. [2] ``{slug}`` is replaced by name of the category / author / tag.
+
+
+Pagination
+==========
+
+The default behaviour of Pelican is to list all the article titles along with a
+short description on the index page. While this works well for small-to-medium
+sites, sites with a large quantity of articles will probably benefit from
+paginating this list.
+
+You can use the following settings to configure the pagination.
+
+.. data:: DEFAULT_ORPHANS = 0
+
+ The minimum number of articles allowed on the last page. Use this when you
+ don't want the last page to only contain a handful of articles.
+
+.. data:: DEFAULT_PAGINATION = False
+
+ The maximum number of articles to include on a page, not including orphans.
+ False to disable pagination.
+
+.. data:: PAGINATED_TEMPLATES = {'index': None, 'tag': None, 'category': None, 'author': None}
+
+ The templates to use pagination with, and the number of articles to include
+ on a page. If this value is ``None``, it defaults to ``DEFAULT_PAGINATION``.
+
+.. data:: PAGINATION_PATTERNS = (
+ (1, '{name}{extension}', '{name}{extension}'),
+ (2, '{name}{number}{extension}', '{name}{number}{extension}'),
+ )
+
+ A set of patterns that are used to determine advanced pagination output.
+
+
+Using Pagination Patterns
+-------------------------
+
+By default, pages subsequent to ``.../foo.html`` are created as
+``.../foo2.html``, etc. The ``PAGINATION_PATTERNS`` setting can be used to
+change this. It takes a sequence of triples, where each triple consists of::
+
+ (minimum_page, page_url, page_save_as,)
+
+For ``page_url`` and ``page_save_as``, you may use a number of variables.
+``{url}`` and ``{save_as}`` correspond respectively to the ``*_URL`` and
+``*_SAVE_AS`` values of the corresponding page type (e.g. ``ARTICLE_SAVE_AS``).
+If ``{save_as} == foo/bar.html``, then ``{name} == foo/bar`` and ``{extension}
+== .html``. ``{base_name}`` equals ``{name}`` except that it strips trailing
+``/index`` if present. ``{number}`` equals the page number.
+
+For example, if you want to leave the first page unchanged, but place
+subsequent pages at ``.../page/2/`` etc, you could set ``PAGINATION_PATTERNS``
+as follows::
+
+ PAGINATION_PATTERNS = (
+ (1, '{url}', '{save_as}'),
+ (2, '{base_name}/page/{number}/', '{base_name}/page/{number}/index.html'),
+ )
+
+
+Translations
+============
+
+Pelican offers a way to translate articles. See the :doc:`Content `
+section for more information.
+
+.. data:: DEFAULT_LANG = 'en'
+
+ The default language to use.
+
+.. data:: ARTICLE_TRANSLATION_ID = 'slug'
+
+ The metadata attribute(s) used to identify which articles are translations
+ of one another. May be a string or a collection of strings. Set to ``None``
+ or ``False`` to disable the identification of translations.
+
+.. data:: PAGE_TRANSLATION_ID = 'slug'
+
+ The metadata attribute(s) used to identify which pages are translations of
+ one another. May be a string or a collection of strings. Set to ``None`` or
+ ``False`` to disable the identification of translations.
+
+.. data:: TRANSLATION_FEED_ATOM = 'feeds/all-{lang}.atom.xml'
+
+ The location to save the Atom feed for translations. [3]_
+
+.. data:: TRANSLATION_FEED_ATOM_URL = None
+
+ Relative URL of the Atom feed for translations, including the ``{lang}``
+ placeholder. [3]_ If not set, ``TRANSLATION_FEED_ATOM`` is used both for
+ save location and URL.
+
+.. data:: TRANSLATION_FEED_RSS = None, i.e. no RSS
+
+ Where to put the RSS feed for translations.
+
+.. data:: TRANSLATION_FEED_RSS_URL = None
+
+ Relative URL of the RSS feed for translations, including the ``{lang}``
+ placeholder. [3]_ If not set, ``TRANSLATION_FEED_RSS`` is used both for save
+ location and URL.
+
+.. [3] {lang} is the language code
+
+
+Ordering content
+================
+
+.. data:: NEWEST_FIRST_ARCHIVES = True
+
+ Order archives by newest first by date. (False: orders by date with older
+ articles first.)
+
+.. data:: REVERSE_CATEGORY_ORDER = False
+
+ Reverse the category order. (True: lists by reverse alphabetical order;
+ default lists alphabetically.)
+
+.. data:: ARTICLE_ORDER_BY = 'reversed-date'
+
+ Defines how the articles (``articles_page.object_list`` in the template) are
+ sorted. Valid options are: metadata as a string (use ``reversed-`` prefix
+ the reverse the sort order), special option ``'basename'`` which will use
+ the basename of the file (without path) or a custom function to extract the
+ sorting key from articles. The default value, ``'reversed-date'``, will sort
+ articles by date in reverse order (i.e. newest article comes first).
+
+.. data:: PAGE_ORDER_BY = 'basename'
+
+ Defines how the pages (``pages`` variable in the template) are sorted.
+ Options are same as ``ARTICLE_ORDER_BY``. The default value, ``'basename'``
+ will sort pages by their basename.
+
+
+.. _settings/themes:
+
+Themes
+======
+
+Creating Pelican themes is addressed in a dedicated section (see
+:ref:`theming-pelican`). However, here are the settings that are related to
+themes.
+
+.. data:: THEME
+
+ Theme to use to produce the output. Can be a relative or absolute path to a
+ theme folder, or the name of a default theme or a theme installed via
+ :doc:`pelican-themes` (see below).
+
+.. data:: THEME_STATIC_DIR = 'theme'
+
+ Destination directory in the output path where Pelican will place the files
+ collected from `THEME_STATIC_PATHS`. Default is `theme`.
+
+.. data:: THEME_STATIC_PATHS = ['static']
+
+ Static theme paths you want to copy. Default value is `static`, but if your
+ theme has other static paths, you can put them here. If files or directories
+ with the same names are included in the paths defined in this settings, they
+ will be progressively overwritten.
+
+.. data:: THEME_TEMPLATES_OVERRIDES = []
+
+ A list of paths you want Jinja2 to search for templates before searching the
+ theme's ``templates/`` directory. Allows for overriding individual theme
+ template files without having to fork an existing theme. Jinja2 searches in
+ the following order: files in ``THEME_TEMPLATES_OVERRIDES`` first, then the
+ theme's ``templates/``.
+
+ You can also extend templates from the theme using the ``{% extends %}``
+ directive utilizing the ``!theme`` prefix as shown in the following example:
+
+ .. parsed-literal::
+
+ {% extends '!theme/article.html' %}
+
+.. data:: CSS_FILE = 'main.css'
+
+ Specify the CSS file you want to load.
+
+By default, two themes are available. You can specify them using the ``THEME``
+setting or by passing the ``-t`` option to the ``pelican`` command:
+
+* notmyidea
+* simple (a synonym for "plain text" :)
+
+There are a number of other themes available at
+https://github.com/getpelican/pelican-themes. Pelican comes with
+:doc:`pelican-themes`, a small script for managing themes.
+
+You can define your own theme, either by starting from scratch or by
+duplicating and modifying a pre-existing theme. Here is :doc:`a guide on how to
+create your theme `.
+
+Following are example ways to specify your preferred theme::
+
+ # Specify name of a built-in theme
+ THEME = "notmyidea"
+ # Specify name of a theme installed via the pelican-themes tool
+ THEME = "chunk"
+ # Specify a customized theme, via path relative to the settings file
+ THEME = "themes/mycustomtheme"
+ # Specify a customized theme, via absolute path
+ THEME = "/home/myuser/projects/mysite/themes/mycustomtheme"
+
+The built-in ``notmyidea`` theme can make good use of the following settings.
+Feel free to use them in your themes as well.
+
+.. data:: SITESUBTITLE
+
+ A subtitle to appear in the header.
+
+.. data:: DISQUS_SITENAME
+
+ Pelican can handle Disqus comments. Specify the Disqus sitename identifier
+ here.
+
+.. data:: GITHUB_URL
+
+ Your GitHub URL (if you have one). It will then use this information to
+ create a GitHub ribbon.
+
+.. data:: GOOGLE_ANALYTICS
+
+ Set to ``UA-XXXXX-Y`` Property's tracking ID to activate Google Analytics.
+
+.. data:: GA_COOKIE_DOMAIN
+
+ Set cookie domain field of Google Analytics tracking code. Defaults to
+ ``auto``.
+
+.. data:: GOSQUARED_SITENAME
+
+ Set to 'XXX-YYYYYY-X' to activate GoSquared.
+
+.. data:: MENUITEMS
+
+ A list of tuples (Title, URL) for additional menu items to appear at the
+ beginning of the main menu.
+
+.. data:: LINKS
+
+ A list of tuples (Title, URL) for links to appear on the header.
+
+.. data:: SOCIAL
+
+ A list of tuples (Title, URL) to appear in the "social" section.
+
+.. data:: TWITTER_USERNAME
+
+ Allows for adding a button to articles to encourage others to tweet about
+ them. Add your Twitter username if you want this button to appear.
+
+.. data:: LINKS_WIDGET_NAME
+
+ Allows override of the name of the links widget. If not specified, defaults
+ to "links".
+
+.. data:: SOCIAL_WIDGET_NAME
+
+ Allows override of the name of the "social" widget. If not specified,
+ defaults to "social".
+
+In addition, you can use the "wide" version of the ``notmyidea`` theme by
+adding the following to your configuration::
+
+ CSS_FILE = "wide.css"
+
+
+Logging
+=======
+
+Sometimes, a long list of warnings may appear during site generation. Finding
+the **meaningful** error message in the middle of tons of annoying log output
+can be quite tricky. In order to filter out redundant log messages, Pelican
+comes with the ``LOG_FILTER`` setting.
+
+``LOG_FILTER`` should be a list of tuples ``(level, msg)``, each of them being
+composed of the logging level (up to ``warning``) and the message to be
+ignored. Simply populate the list with the log messages you want to hide, and
+they will be filtered out.
+
+For example::
+
+ import logging
+ LOG_FILTER = [(logging.WARN, 'TAG_SAVE_AS is set to False')]
+
+It is possible to filter out messages by a template. Check out source code to
+obtain a template.
+
+For example::
+
+ import logging
+ LOG_FILTER = [(logging.WARN, 'Empty alt attribute for image %s in %s')]
+
+.. Warning::
+
+ Silencing messages by templates is a dangerous feature. It is possible to
+ unintentionally filter out multiple message types with the same template
+ (including messages from future Pelican versions). Proceed with caution.
+
+.. note::
+
+ This option does nothing if ``--debug`` is passed.
+
+.. _reading_only_modified_content:
+
+
+Reading only modified content
+=============================
+
+To speed up the build process, Pelican can optionally read only articles and
+pages with modified content.
+
+When Pelican is about to read some content source file:
+
+1. The hash or modification time information for the file from a
+ previous build are loaded from a cache file if ``LOAD_CONTENT_CACHE`` is
+ ``True``. These files are stored in the ``CACHE_PATH`` directory. If the
+ file has no record in the cache file, it is read as usual.
+2. The file is checked according to ``CHECK_MODIFIED_METHOD``:
+
+ - If set to ``'mtime'``, the modification time of the file is
+ checked.
+ - If set to a name of a function provided by the ``hashlib``
+ module, e.g. ``'md5'``, the file hash is checked.
+ - If set to anything else or the necessary information about the
+ file cannot be found in the cache file, the content is read as usual.
+
+3. If the file is considered unchanged, the content data saved in a
+ previous build corresponding to the file is loaded from the cache, and the
+ file is not read.
+4. If the file is considered changed, the file is read and the new
+ modification information and the content data are saved to the cache if
+ ``CACHE_CONTENT`` is ``True``.
+
+If ``CONTENT_CACHING_LAYER`` is set to ``'reader'`` (the default), the raw
+content and metadata returned by a reader are cached. If this setting is
+instead set to ``'generator'``, the processed content object is cached. Caching
+the processed content object may conflict with plugins (as some reading related
+signals may be skipped) and the ``WITH_FUTURE_DATES`` functionality (as the
+``draft`` status of the cached content objects would not change automatically
+over time).
+
+Checking modification times is faster than comparing file hashes, but it is not
+as reliable because ``mtime`` information can be lost, e.g., when copying
+content source files using the ``cp`` or ``rsync`` commands without the
+``mtime`` preservation mode (which for ``rsync`` can be invoked by passing the
+``--archive`` flag).
+
+The cache files are Python pickles, so they may not be readable by different
+versions of Python as the pickle format often changes. If such an error is
+encountered, it is caught and the cache file is rebuilt automatically in the
+new format. The cache files will also be rebuilt after the ``GZIP_CACHE``
+setting has been changed.
+
+The ``--ignore-cache`` command-line option is useful when the whole cache needs
+to be regenerated, such as when making modifications to the settings file that
+will affect the cached content, or just for debugging purposes. When Pelican
+runs in autoreload mode, modification of the settings file will make it ignore
+the cache automatically if ``AUTORELOAD_IGNORE_CACHE`` is ``True``.
+
+Note that even when using cached content, all output is always written, so the
+modification times of the generated ``*.html`` files will always change.
+Therefore, ``rsync``-based uploading may benefit from the ``--checksum``
+option.
+
+.. _writing_only_selected_content:
+
+
+Writing only selected content
+=============================
+
+When only working on a single article or page, or making tweaks to your theme,
+it is often desirable to generate and review your work as quickly as possible.
+In such cases, generating and writing the entire site output is often
+unnecessary. By specifying only the desired files as output paths in the
+``WRITE_SELECTED`` list, **only** those files will be written. This list can be
+also specified on the command line using the ``--write-selected`` option, which
+accepts a comma-separated list of output file paths. By default this list is
+empty, so all output is written. See :ref:`site_generation` for more details.
+
+
+Example settings
+================
+
+.. literalinclude:: ../samples/pelican.conf.py
+ :language: python
+
+
+.. _Jinja Environment documentation: https://jinja.palletsprojects.com/en/master/api/#jinja2.Environment
+.. _Docutils Configuration: http://docutils.sourceforge.net/docs/user/config.html
diff --git a/docs/themes.rst b/docs/themes.rst
new file mode 100644
index 00000000..86a754bc
--- /dev/null
+++ b/docs/themes.rst
@@ -0,0 +1,570 @@
+.. _theming-pelican:
+
+Creating themes
+###############
+
+To generate its HTML output, Pelican uses the `Jinja
+ `_ templating engine due to its flexibility and
+straightforward syntax. If you want to create your own theme, feel free to take
+inspiration from the `"simple" theme
+`_.
+
+To generate your site using a theme you have created (or downloaded manually
+and then modified), you can specify that theme via the ``-t`` flag::
+
+ pelican content -s pelicanconf.py -t /projects/your-site/themes/your-theme
+
+If you'd rather not specify the theme on every invocation, you can define
+``THEME`` in your settings to point to the location of your preferred theme.
+
+
+Structure
+=========
+
+To make your own theme, you must follow the following structure::
+
+ ├── static
+ │ ├── css
+ │ └── images
+ └── templates
+ ├── archives.html // to display archives
+ ├── period_archives.html // to display time-period archives
+ ├── article.html // processed for each article
+ ├── author.html // processed for each author
+ ├── authors.html // must list all the authors
+ ├── categories.html // must list all the categories
+ ├── category.html // processed for each category
+ ├── index.html // the index (list all the articles)
+ ├── page.html // processed for each page
+ ├── tag.html // processed for each tag
+ └── tags.html // must list all the tags. Can be a tag cloud.
+
+* `static` contains all the static assets, which will be copied to the output
+ `theme` folder. The above filesystem layout includes CSS and image folders,
+ but those are just examples. Put what you need here.
+
+* `templates` contains all the templates that will be used to generate the
+ content. The template files listed above are mandatory; you can add your own
+ templates if it helps you keep things organized while creating your theme.
+
+
+.. _templates-variables:
+
+Templates and variables
+=======================
+
+The idea is to use a simple syntax that you can embed into your HTML pages.
+This document describes which templates should exist in a theme, and which
+variables will be passed to each template at generation time.
+
+All templates will receive the variables defined in your settings file, as long
+as they are in all-caps. You can access them directly.
+
+
+Common variables
+----------------
+
+All of these settings will be available to all templates.
+
+============= ===================================================
+Variable Description
+============= ===================================================
+output_file The name of the file currently being generated. For
+ instance, when Pelican is rendering the home page,
+ output_file will be "index.html".
+articles The list of articles, ordered descending by date.
+ All the elements are `Article` objects, so you can
+ access their attributes (e.g. title, summary, author
+ etc.). Sometimes this is shadowed (for instance, in
+ the tags page). You will then find info about it
+ in the `all_articles` variable.
+dates The same list of articles, but ordered by date,
+ ascending.
+drafts The list of draft articles
+authors A list of (author, articles) tuples, containing all
+ the authors and corresponding articles (values)
+categories A list of (category, articles) tuples, containing
+ all the categories and corresponding articles (values)
+tags A list of (tag, articles) tuples, containing all
+ the tags and corresponding articles (values)
+pages The list of pages
+hidden_pages The list of hidden pages
+draft_pages The list of draft pages
+============= ===================================================
+
+
+Sorting
+-------
+
+URL wrappers (currently categories, tags, and authors), have comparison methods
+that allow them to be easily sorted by name::
+
+ {% for tag, articles in tags|sort %}
+
+If you want to sort based on different criteria, `Jinja's sort command`__ has a
+number of options.
+
+__ https://jinja.palletsprojects.com/en/master/templates/#sort
+
+
+Date Formatting
+---------------
+
+Pelican formats the date according to your settings and locale
+(``DATE_FORMATS``/``DEFAULT_DATE_FORMAT``) and provides a ``locale_date``
+attribute. On the other hand, the ``date`` attribute will be a `datetime`_
+object. If you need custom formatting for a date different than your settings,
+use the Jinja filter ``strftime`` that comes with Pelican. Usage is same as
+Python `strftime`_ format, but the filter will do the right thing and format
+your date according to the locale given in your settings::
+
+ {{ article.date|strftime('%d %B %Y') }}
+
+.. _datetime: https://docs.python.org/3/library/datetime.html#datetime-objects
+.. _strftime: https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior
+
+
+index.html
+----------
+
+This is the home page or index of your blog, generated at ``index.html``.
+
+If pagination is active, subsequent pages will reside in
+``index{number}.html``.
+
+====================== ===================================================
+Variable Description
+====================== ===================================================
+articles_paginator A paginator object for the list of articles
+articles_page The current page of articles
+articles_previous_page The previous page of articles (``None`` if page does
+ not exist)
+articles_next_page The next page of articles (``None`` if page does
+ not exist)
+dates_paginator A paginator object for the article list, ordered by
+ date, ascending.
+dates_page The current page of articles, ordered by date,
+ ascending.
+dates_previous_page The previous page of articles, ordered by date,
+ ascending (``None`` if page does not exist)
+dates_next_page The next page of articles, ordered by date,
+ ascending (``None`` if page does not exist)
+page_name 'index' -- useful for pagination links
+====================== ===================================================
+
+
+author.html
+-------------
+
+This template will be processed for each of the existing authors, with output
+generated according to the ``AUTHOR_SAVE_AS`` setting (`Default:`
+``author/{slug}.html``). If pagination is active, subsequent pages will by
+default reside at ``author/{slug}{number}.html``.
+
+====================== ===================================================
+Variable Description
+====================== ===================================================
+author The name of the author being processed
+articles Articles by this author
+dates Articles by this author, but ordered by date,
+ ascending
+articles_paginator A paginator object for the list of articles
+articles_page The current page of articles
+articles_previous_page The previous page of articles (``None`` if page does
+ not exist)
+articles_next_page The next page of articles (``None`` if page does
+ not exist)
+dates_paginator A paginator object for the article list, ordered by
+ date, ascending.
+dates_page The current page of articles, ordered by date,
+ ascending.
+dates_previous_page The previous page of articles, ordered by date,
+ ascending (``None`` if page does not exist)
+dates_next_page The next page of articles, ordered by date,
+ ascending (``None`` if page does not exist)
+page_name AUTHOR_URL where everything after `{slug}` is
+ removed -- useful for pagination links
+====================== ===================================================
+
+
+category.html
+-------------
+
+This template will be processed for each of the existing categories, with
+output generated according to the ``CATEGORY_SAVE_AS`` setting (`Default:`
+``category/{slug}.html``). If pagination is active, subsequent pages will by
+default reside at ``category/{slug}{number}.html``.
+
+====================== ===================================================
+Variable Description
+====================== ===================================================
+category The name of the category being processed
+articles Articles for this category
+dates Articles for this category, but ordered by date,
+ ascending
+articles_paginator A paginator object for the list of articles
+articles_page The current page of articles
+articles_previous_page The previous page of articles (``None`` if page does
+ not exist)
+articles_next_page The next page of articles (``None`` if page does
+ not exist)
+dates_paginator A paginator object for the list of articles,
+ ordered by date, ascending
+dates_page The current page of articles, ordered by date,
+ ascending
+dates_previous_page The previous page of articles, ordered by date,
+ ascending (``None`` if page does not exist)
+dates_next_page The next page of articles, ordered by date,
+ ascending (``None`` if page does not exist)
+page_name CATEGORY_URL where everything after `{slug}` is
+ removed -- useful for pagination links
+====================== ===================================================
+
+
+article.html
+-------------
+
+This template will be processed for each article, with output generated
+according to the ``ARTICLE_SAVE_AS`` setting (`Default:` ``{slug}.html``). The
+following variables are available when rendering.
+
+============= ===================================================
+Variable Description
+============= ===================================================
+article The article object to be displayed
+category The name of the category for the current article
+============= ===================================================
+
+Any metadata that you put in the header of the article source file will be
+available as fields on the ``article`` object. The field name will be the same
+as the name of the metadata field, except in all-lowercase characters.
+
+For example, you could add a field called `FacebookImage` to your article
+metadata, as shown below:
+
+.. code-block:: md
+
+ Title: I love Python more than music
+ Date: 2013-11-06 10:06
+ Tags: personal, python
+ Category: Tech
+ Slug: python-je-l-aime-a-mourir
+ Author: Francis Cabrel
+ FacebookImage: http://franciscabrel.com/images/pythonlove.png
+
+This new metadata will be made available as `article.facebookimage` in your
+`article.html` template. This would allow you, for example, to specify an image
+for the Facebook open graph tags that will change for each article:
+
+.. code-block:: html+jinja
+
+
+
+
+page.html
+---------
+
+This template will be processed for each page, with output generated according
+to the ``PAGE_SAVE_AS`` setting (`Default:` ``pages/{slug}.html``). The
+following variables are available when rendering.
+
+============= ===================================================
+Variable Description
+============= ===================================================
+page The page object to be displayed. You can access its
+ title, slug, and content.
+============= ===================================================
+
+
+tag.html
+--------
+
+This template will be processed for each tag, with output generated according
+to the ``TAG_SAVE_AS`` setting (`Default:` ``tag/{slug}.html``). If pagination
+is active, subsequent pages will by default reside at
+``tag/{slug}{number}.html``.
+
+====================== ===================================================
+Variable Description
+====================== ===================================================
+tag The name of the tag being processed
+articles Articles related to this tag
+dates Articles related to this tag, but ordered by date,
+ ascending
+articles_paginator A paginator object for the list of articles
+articles_page The current page of articles
+articles_previous_page The previous page of articles (``None`` if page does
+ not exist)
+articles_next_page The next page of articles (``None`` if page does
+ not exist)
+dates_paginator A paginator object for the list of articles,
+ ordered by date, ascending
+dates_page The current page of articles, ordered by date,
+ ascending
+dates_previous_page The previous page of articles, ordered by date,
+ ascending (``None`` if page does not exist)
+dates_next_page The next page of articles, ordered by date,
+ ascending (``None`` if page does not exist)
+page_name TAG_URL where everything after `{slug}` is removed
+ -- useful for pagination links
+====================== ===================================================
+
+
+period_archives.html
+--------------------
+
+This template will be processed for each year of your posts if a path for
+``YEAR_ARCHIVE_SAVE_AS`` is defined, each month if ``MONTH_ARCHIVE_SAVE_AS`` is
+defined, and each day if ``DAY_ARCHIVE_SAVE_AS`` is defined.
+
+=================== ===================================================
+Variable Description
+=================== ===================================================
+period A tuple of the form (`year`, `month`, `day`) that
+ indicates the current time period. `year` and `day`
+ are numbers while `month` is a string. This tuple
+ only contains `year` if the time period is a
+ given year. It contains both `year` and `month`
+ if the time period is over years and months and
+ so on.
+
+=================== ===================================================
+
+You can see an example of how to use `period` in the `"simple" theme
+period_archives.html template
+`_.
+
+
+Objects
+=======
+
+Detail objects attributes that are available and useful in templates. Not all
+attributes are listed here, this is a selection of attributes considered useful
+in a template.
+
+.. _object-article:
+
+Article
+-------
+
+The string representation of an Article is the `source_path` attribute.
+
+====================== ===================================================
+Attribute Description
+====================== ===================================================
+author The :ref:`Author ` of
+ this article.
+authors A list of :ref:`Authors `
+ of this article.
+category The :ref:`Category `
+ of this article.
+content The rendered content of the article.
+date Datetime object representing the article date.
+date_format Either default date format or locale date format.
+default_template Default template name.
+in_default_lang Boolean representing if the article is written
+ in the default language.
+lang Language of the article.
+locale_date Date formatted by the `date_format`.
+metadata Article header metadata `dict`.
+save_as Location to save the article page.
+slug Page slug.
+source_path Full system path of the article source file.
+relative_source_path Relative path from PATH_ to the article source file.
+status The article status, can be any of 'published' or
+ 'draft'.
+summary Rendered summary content.
+tags List of :ref:`Tag `
+ objects.
+template Template name to use for rendering.
+title Title of the article.
+translations List of translations
+ :ref:`Article ` objects.
+url URL to the article page.
+====================== ===================================================
+
+.. _PATH: settings.html#PATH
+
+
+.. _object-author_cat_tag:
+
+Author / Category / Tag
+-----------------------
+
+The string representation of those objects is the `name` attribute.
+
+=================== ===================================================
+Attribute Description
+=================== ===================================================
+name Name of this object [1]_.
+page_name Author page name.
+save_as Location to save the author page.
+slug Page slug.
+url URL to the author page.
+=================== ===================================================
+
+.. [1] for Author object, coming from `:authors:` or `AUTHOR`.
+
+.. _object-page:
+
+Page
+----
+
+The string representation of a Page is the `source_path` attribute.
+
+===================== ===================================================
+Attribute Description
+===================== ===================================================
+author The :ref:`Author ` of
+ this page.
+content The rendered content of the page.
+date Datetime object representing the page date.
+date_format Either default date format or locale date format.
+default_template Default template name.
+in_default_lang Boolean representing if the article is written
+ in the default language.
+lang Language of the article.
+locale_date Date formatted by the `date_format`.
+metadata Page header metadata `dict`.
+save_as Location to save the page.
+slug Page slug.
+source_path Full system path of the page source file.
+relative_source_path Relative path from PATH_ to the page source file.
+status The page status, can be any of 'published', 'hidden' or
+ 'draft'.
+summary Rendered summary content.
+tags List of :ref:`Tag `
+ objects.
+template Template name to use for rendering.
+title Title of the page.
+translations List of translations
+ :ref:`Article ` objects.
+url URL to the page.
+===================== ===================================================
+
+.. _PATH: settings.html#PATH
+
+
+Feeds
+=====
+
+The feed variables changed in 3.0. Each variable now explicitly lists ATOM or
+RSS in the name. ATOM is still the default. Old themes will need to be updated.
+Here is a complete list of the feed variables::
+
+ 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
+
+
+Inheritance
+===========
+
+Since version 3.0, Pelican supports inheritance from the ``simple`` theme, so
+you can re-use the ``simple`` theme templates in your own themes.
+
+If one of the mandatory files in the ``templates/`` directory of your theme is
+missing, it will be replaced by the matching template from the ``simple``
+theme. So if the HTML structure of a template in the ``simple`` theme is right
+for you, you don't have to write a new template from scratch.
+
+You can also extend templates from the ``simple`` theme in your own themes by
+using the ``{% extends %}`` directive as in the following example:
+
+.. code-block:: html+jinja
+
+ {% extends "!simple/index.html" %}
+
+ {% extends "index.html" %}
+
+
+Example
+-------
+
+With this system, it is possible to create a theme with just two files.
+
+base.html
+"""""""""
+
+The first file is the ``templates/base.html`` template:
+
+.. code-block:: html+jinja
+
+ {% extends "!simple/base.html" %}
+
+ {% block head %}
+ {{ super() }}
+
+ {% endblock %}
+
+1. On the first line, we extend the ``base.html`` template from the ``simple``
+ theme, so we don't have to rewrite the entire file.
+2. On the third line, we open the ``head`` block which has already been defined
+ in the ``simple`` theme.
+3. On the fourth line, the function ``super()`` keeps the content previously
+ inserted in the ``head`` block.
+4. On the fifth line, we append a stylesheet to the page.
+5. On the last line, we close the ``head`` block.
+
+This file will be extended by all the other templates, so the stylesheet will
+be linked from all pages.
+
+style.css
+"""""""""
+
+The second file is the ``static/css/style.css`` CSS stylesheet:
+
+.. code-block:: css
+
+ body {
+ font-family : monospace ;
+ font-size : 100% ;
+ background-color : white ;
+ color : #111 ;
+ width : 80% ;
+ min-width : 400px ;
+ min-height : 200px ;
+ padding : 1em ;
+ margin : 5% 10% ;
+ border : thin solid gray ;
+ border-radius : 5px ;
+ display : block ;
+ }
+
+ a:link { color : blue ; text-decoration : none ; }
+ a:hover { color : blue ; text-decoration : underline ; }
+ a:visited { color : blue ; }
+
+ h1 a { color : inherit !important }
+ h2 a { color : inherit !important }
+ h3 a { color : inherit !important }
+ h4 a { color : inherit !important }
+ h5 a { color : inherit !important }
+ h6 a { color : inherit !important }
+
+ pre {
+ margin : 2em 1em 2em 4em ;
+ }
+
+ #menu li {
+ display : inline ;
+ }
+
+ #post-list {
+ margin-bottom : 1em ;
+ margin-top : 1em ;
+ }
+
+Download
+""""""""
+
+You can download this example theme :download:`here <_static/theme-basic.zip>`.
diff --git a/docs/tips.rst b/docs/tips.rst
new file mode 100644
index 00000000..18a79ef0
--- /dev/null
+++ b/docs/tips.rst
@@ -0,0 +1,182 @@
+Tips
+####
+
+Here are some tips about Pelican that you might find useful.
+
+Custom 404 Pages
+================
+
+When a browser requests a resource that the web server cannot find, the web
+server usually displays a generic "File not found" (404) error page that can be
+stark and unsightly. One way to provide an error page that matches the theme of
+your site is to create a custom 404 page (*not* an article), such as this
+Markdown-formatted example stored in ``content/pages/404.md``::
+
+ Title: Not Found
+ Status: hidden
+ Save_as: 404.html
+
+ The requested item could not be located. Perhaps you might want to check
+ the [Archives](/archives.html)?
+
+The next step is to configure your web server to display this custom page
+instead of its default 404 page. For Nginx, add the following to your
+configuration file's ``location`` block::
+
+ error_page 404 /404.html;
+
+For Apache::
+
+ ErrorDocument 404 /404.html
+
+For Amazon S3, first navigate to the ``Static Site Hosting`` menu in the bucket
+settings on your AWS cosole. From there::
+
+ Error Document: 404.html
+
+Publishing to GitHub
+====================
+
+`GitHub Pages `_ offer an easy
+and convenient way to publish Pelican sites. There are `two types of GitHub
+Pages `_:
+*Project Pages* and *User Pages*. Pelican sites can be published as both
+Project Pages and User Pages.
+
+Project Pages
+-------------
+
+To publish a Pelican site as a Project Page you need to *push* the content of
+the ``output`` dir generated by Pelican to a repository's ``gh-pages`` branch
+on GitHub.
+
+The excellent `ghp-import `_, which can
+be installed with ``pip``, makes this process really easy.
+
+For example, if the source of your Pelican site is contained in a GitHub
+repository, and if you want to publish that Pelican site in the form of Project
+Pages to this repository, you can then use the following::
+
+ $ pelican content -o output -s pelicanconf.py
+ $ ghp-import output -b gh-pages
+ $ git push origin gh-pages
+
+The ``ghp-import output`` command updates the local ``gh-pages`` branch with
+the content of the ``output`` directory (creating the branch if it doesn't
+already exist). The ``git push origin gh-pages`` command updates the remote
+``gh-pages`` branch, effectively publishing the Pelican site.
+
+.. note::
+
+ The ``github`` target of the Makefile (and the ``gh_pages`` task of
+ ``tasks.py``) created by the ``pelican-quickstart`` command publishes the
+ Pelican site as Project Pages, as described above.
+
+User Pages
+----------
+
+To publish a Pelican site in the form of User Pages, you need to *push* the
+content of the ``output`` dir generated by Pelican to the ``master`` branch of
+your ``.github.io`` repository on GitHub.
+
+Again, you can take advantage of ``ghp-import``::
+
+ $ pelican content -o output -s pelicanconf.py
+ $ ghp-import output -b gh-pages
+ $ git push git@github.com:elemoine/elemoine.github.io.git gh-pages:master
+
+The ``git push`` command pushes the local ``gh-pages`` branch (freshly updated
+by the ``ghp-import`` command) to the ``elemoine.github.io`` repository's
+``master`` branch on GitHub.
+
+.. note::
+
+ To publish your Pelican site as User Pages, feel free to adjust the
+ ``github`` target of the Makefile.
+
+Another option for publishing to User Pages is to generate the output files in
+the root directory of the project.
+
+For example, your main project folder is ``.github.io`` and you can
+create the Pelican project in a subdirectory called ``Pelican``. Then from
+inside the ``Pelican`` folder you can run::
+
+ $ pelican content -o .. -s pelicanconf.py
+
+Now you can push the whole project ``.github.io`` to the master
+branch of your GitHub repository::
+
+ $ git push origin master
+
+(assuming origin is set to your remote repository).
+
+Custom 404 Pages
+----------------
+
+GitHub Pages will display the custom 404 page described above, as noted in the
+relevant `GitHub docs `_.
+
+Update your site on each commit
+-------------------------------
+
+To automatically update your Pelican site on each commit, you can create a
+post-commit hook. For example, you can add the following to
+``.git/hooks/post-commit``::
+
+ pelican content -o output -s pelicanconf.py && ghp-import output && git push origin gh-pages
+
+Copy static files to the root of your site
+------------------------------------------
+
+To use a `custom domain
+`_ with
+GitHub Pages, you need to put the domain of your site (e.g.,
+``blog.example.com``) inside a ``CNAME`` file at the root of your site. To do
+this, create the ``content/extra/`` directory and add a ``CNAME`` file to it.
+Then use the ``STATIC_PATHS`` setting to tell Pelican to copy this file to your
+output directory. For example::
+
+ STATIC_PATHS = ['images', 'extra/CNAME']
+ EXTRA_PATH_METADATA = {'extra/CNAME': {'path': 'CNAME'},}
+
+Note: use forward slashes, ``/``, even on Windows.
+
+You can also use the ``EXTRA_PATH_METADATA`` mechanism to place a
+``favicon.ico`` or ``robots.txt`` at the root of any site.
+
+How to add YouTube or Vimeo Videos
+==================================
+
+The easiest way is to paste the embed code of the video from these sites
+directly into your source content.
+
+Alternatively, you can also use Pelican plugins like ``liquid_tags``,
+``pelican_youtube``, or ``pelican_vimeo`` to embed videos in your content.
+
+Moreover, markup languages like reST and Markdown have plugins that let you
+embed videos in the markup. You can use `reST video directive
+`_ for reST or `mdx_video plugin
+`_ for Markdown.
+
+
+Develop Locally Using SSL
+==================================
+
+Here's how you can set up your local pelican server to support SSL.
+
+First, create a self-signed certificate and key using ``openssl`` (this creates ``cert.pem`` and ``key.pem``)::
+
+ $ openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
+
+And use this command to launch the server (the server starts within your ``output`` directory)::
+
+ python -m pelican.server 8443 --key=../key.pem --cert=../cert.pem
+
+If you are using ``develop-server.sh``, add this to the top::
+
+ CERT="$BASEDIR/cert.pem"
+ KEY="$BASEDIR/key.pem"
+
+and modify the ``pelican.server`` line as follows::
+
+ $PY -m pelican.server $port --ssl --cert="$CERT" --key="$KEY" &
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
deleted file mode 100644
index 6f5d9f44..00000000
--- a/package-lock.json
+++ /dev/null
@@ -1,1556 +0,0 @@
-{
- "name": "pelican-theme",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "devDependencies": {
- "@tailwindcss/typography": "^0.5.15",
- "tailwindcss": "^3.4.17"
- }
- },
- "node_modules/@alloc/quick-lru": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
- "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/@isaacs/cliui": {
- "version": "8.0.2",
- "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
- "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "string-width": "^5.1.2",
- "string-width-cjs": "npm:string-width@^4.2.0",
- "strip-ansi": "^7.0.1",
- "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
- "wrap-ansi": "^8.1.0",
- "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@jridgewell/gen-mapping": {
- "version": "0.3.8",
- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
- "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/set-array": "^1.2.1",
- "@jridgewell/sourcemap-codec": "^1.4.10",
- "@jridgewell/trace-mapping": "^0.3.24"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@jridgewell/resolve-uri": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
- "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@jridgewell/set-array": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
- "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
- "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.25",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
- "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/resolve-uri": "^3.1.0",
- "@jridgewell/sourcemap-codec": "^1.4.14"
- }
- },
- "node_modules/@nodelib/fs.scandir": {
- "version": "2.1.5",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
- "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@nodelib/fs.stat": "2.0.5",
- "run-parallel": "^1.1.9"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/@nodelib/fs.stat": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
- "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/@nodelib/fs.walk": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
- "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@nodelib/fs.scandir": "2.1.5",
- "fastq": "^1.6.0"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/@pkgjs/parseargs": {
- "version": "0.11.0",
- "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
- "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">=14"
- }
- },
- "node_modules/@tailwindcss/typography": {
- "version": "0.5.15",
- "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.15.tgz",
- "integrity": "sha512-AqhlCXl+8grUz8uqExv5OTtgpjuVIwFTSXTrh8y9/pw6q2ek7fJ+Y8ZEVw7EB2DCcuCOtEjf9w3+J3rzts01uA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "lodash.castarray": "^4.4.0",
- "lodash.isplainobject": "^4.0.6",
- "lodash.merge": "^4.6.2",
- "postcss-selector-parser": "6.0.10"
- },
- "peerDependencies": {
- "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20"
- }
- },
- "node_modules/ansi-regex": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
- "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
- }
- },
- "node_modules/ansi-styles": {
- "version": "6.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
- "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/any-promise": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
- "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/anymatch": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
- "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "normalize-path": "^3.0.0",
- "picomatch": "^2.0.4"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/arg": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
- "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/balanced-match": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/binary-extensions": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
- "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/brace-expansion": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
- "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0"
- }
- },
- "node_modules/braces": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
- "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fill-range": "^7.1.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/camelcase-css": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
- "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/chokidar": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
- "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "anymatch": "~3.1.2",
- "braces": "~3.0.2",
- "glob-parent": "~5.1.2",
- "is-binary-path": "~2.1.0",
- "is-glob": "~4.0.1",
- "normalize-path": "~3.0.0",
- "readdirp": "~3.6.0"
- },
- "engines": {
- "node": ">= 8.10.0"
- },
- "funding": {
- "url": "https://paulmillr.com/funding/"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.2"
- }
- },
- "node_modules/chokidar/node_modules/glob-parent": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
- "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "is-glob": "^4.0.1"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/commander": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
- "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/cross-spawn": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
- "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "path-key": "^3.1.0",
- "shebang-command": "^2.0.0",
- "which": "^2.0.1"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/cssesc": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
- "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "cssesc": "bin/cssesc"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/didyoumean": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
- "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
- "dev": true,
- "license": "Apache-2.0"
- },
- "node_modules/dlv": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
- "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/eastasianwidth": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
- "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/emoji-regex": {
- "version": "9.2.2",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
- "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/fast-glob": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
- "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@nodelib/fs.stat": "^2.0.2",
- "@nodelib/fs.walk": "^1.2.3",
- "glob-parent": "^5.1.2",
- "merge2": "^1.3.0",
- "micromatch": "^4.0.4"
- },
- "engines": {
- "node": ">=8.6.0"
- }
- },
- "node_modules/fast-glob/node_modules/glob-parent": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
- "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "is-glob": "^4.0.1"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/fastq": {
- "version": "1.17.1",
- "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
- "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "reusify": "^1.0.4"
- }
- },
- "node_modules/fill-range": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
- "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "to-regex-range": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/foreground-child": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
- "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "cross-spawn": "^7.0.0",
- "signal-exit": "^4.0.1"
- },
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/fsevents": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
- }
- },
- "node_modules/function-bind": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
- "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/glob": {
- "version": "10.4.5",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
- "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "foreground-child": "^3.1.0",
- "jackspeak": "^3.1.2",
- "minimatch": "^9.0.4",
- "minipass": "^7.1.2",
- "package-json-from-dist": "^1.0.0",
- "path-scurry": "^1.11.1"
- },
- "bin": {
- "glob": "dist/esm/bin.mjs"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/glob-parent": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
- "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "is-glob": "^4.0.3"
- },
- "engines": {
- "node": ">=10.13.0"
- }
- },
- "node_modules/hasown": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
- "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/is-binary-path": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
- "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "binary-extensions": "^2.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/is-core-module": {
- "version": "2.16.0",
- "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.0.tgz",
- "integrity": "sha512-urTSINYfAYgcbLb0yDQ6egFm6h3Mo1DcF9EkyXSRjjzdHbsulg01qhwWuXdOoUBuTkbQ80KDboXa0vFJ+BDH+g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "hasown": "^2.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-extglob": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
- "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/is-fullwidth-code-point": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
- "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/is-glob": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
- "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-extglob": "^2.1.1"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/is-number": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
- "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.12.0"
- }
- },
- "node_modules/isexe": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/jackspeak": {
- "version": "3.4.3",
- "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
- "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "@isaacs/cliui": "^8.0.2"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- },
- "optionalDependencies": {
- "@pkgjs/parseargs": "^0.11.0"
- }
- },
- "node_modules/jiti": {
- "version": "1.21.7",
- "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
- "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "jiti": "bin/jiti.js"
- }
- },
- "node_modules/lilconfig": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
- "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/antonk52"
- }
- },
- "node_modules/lines-and-columns": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
- "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/lodash.castarray": {
- "version": "4.4.0",
- "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
- "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/lodash.isplainobject": {
- "version": "4.0.6",
- "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
- "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/lodash.merge": {
- "version": "4.6.2",
- "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
- "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/lru-cache": {
- "version": "10.4.3",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
- "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/merge2": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
- "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/micromatch": {
- "version": "4.0.8",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
- "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "braces": "^3.0.3",
- "picomatch": "^2.3.1"
- },
- "engines": {
- "node": ">=8.6"
- }
- },
- "node_modules/minimatch": {
- "version": "9.0.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
- "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^2.0.1"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/minipass": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
- "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": ">=16 || 14 >=14.17"
- }
- },
- "node_modules/mz": {
- "version": "2.7.0",
- "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
- "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "any-promise": "^1.0.0",
- "object-assign": "^4.0.1",
- "thenify-all": "^1.0.0"
- }
- },
- "node_modules/nanoid": {
- "version": "3.3.8",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
- "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "bin": {
- "nanoid": "bin/nanoid.cjs"
- },
- "engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
- }
- },
- "node_modules/normalize-path": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
- "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/object-assign": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/object-hash": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
- "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/package-json-from-dist": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
- "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
- "dev": true,
- "license": "BlueOak-1.0.0"
- },
- "node_modules/path-key": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
- "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/path-parse": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
- "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/path-scurry": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
- "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "lru-cache": "^10.2.0",
- "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
- },
- "engines": {
- "node": ">=16 || 14 >=14.18"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/picocolors": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
- "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/picomatch": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/pify": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
- "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/pirates": {
- "version": "4.0.6",
- "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
- "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/postcss": {
- "version": "8.4.49",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
- "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/postcss"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "nanoid": "^3.3.7",
- "picocolors": "^1.1.1",
- "source-map-js": "^1.2.1"
- },
- "engines": {
- "node": "^10 || ^12 || >=14"
- }
- },
- "node_modules/postcss-import": {
- "version": "15.1.0",
- "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
- "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "postcss-value-parser": "^4.0.0",
- "read-cache": "^1.0.0",
- "resolve": "^1.1.7"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "peerDependencies": {
- "postcss": "^8.0.0"
- }
- },
- "node_modules/postcss-js": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
- "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "camelcase-css": "^2.0.1"
- },
- "engines": {
- "node": "^12 || ^14 || >= 16"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- "peerDependencies": {
- "postcss": "^8.4.21"
- }
- },
- "node_modules/postcss-load-config": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
- "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "lilconfig": "^3.0.0",
- "yaml": "^2.3.4"
- },
- "engines": {
- "node": ">= 14"
- },
- "peerDependencies": {
- "postcss": ">=8.0.9",
- "ts-node": ">=9.0.0"
- },
- "peerDependenciesMeta": {
- "postcss": {
- "optional": true
- },
- "ts-node": {
- "optional": true
- }
- }
- },
- "node_modules/postcss-nested": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
- "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "postcss-selector-parser": "^6.1.1"
- },
- "engines": {
- "node": ">=12.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.14"
- }
- },
- "node_modules/postcss-nested/node_modules/postcss-selector-parser": {
- "version": "6.1.2",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
- "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "cssesc": "^3.0.0",
- "util-deprecate": "^1.0.2"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/postcss-selector-parser": {
- "version": "6.0.10",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
- "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "cssesc": "^3.0.0",
- "util-deprecate": "^1.0.2"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/postcss-value-parser": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
- "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/queue-microtask": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
- "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT"
- },
- "node_modules/read-cache": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
- "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "pify": "^2.3.0"
- }
- },
- "node_modules/readdirp": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
- "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "picomatch": "^2.2.1"
- },
- "engines": {
- "node": ">=8.10.0"
- }
- },
- "node_modules/resolve": {
- "version": "1.22.9",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.9.tgz",
- "integrity": "sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-core-module": "^2.16.0",
- "path-parse": "^1.0.7",
- "supports-preserve-symlinks-flag": "^1.0.0"
- },
- "bin": {
- "resolve": "bin/resolve"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/reusify": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
- "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "iojs": ">=1.0.0",
- "node": ">=0.10.0"
- }
- },
- "node_modules/run-parallel": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
- "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "queue-microtask": "^1.2.2"
- }
- },
- "node_modules/shebang-command": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
- "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "shebang-regex": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/shebang-regex": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
- "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/signal-exit": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
- "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/source-map-js": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
- "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
- "dev": true,
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/string-width": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
- "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "eastasianwidth": "^0.2.0",
- "emoji-regex": "^9.2.2",
- "strip-ansi": "^7.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/string-width-cjs": {
- "name": "string-width",
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/string-width-cjs/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/string-width-cjs/node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/string-width-cjs/node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/strip-ansi": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
- "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^6.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
- }
- },
- "node_modules/strip-ansi-cjs": {
- "name": "strip-ansi",
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/sucrase": {
- "version": "3.35.0",
- "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
- "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.2",
- "commander": "^4.0.0",
- "glob": "^10.3.10",
- "lines-and-columns": "^1.1.6",
- "mz": "^2.7.0",
- "pirates": "^4.0.1",
- "ts-interface-checker": "^0.1.9"
- },
- "bin": {
- "sucrase": "bin/sucrase",
- "sucrase-node": "bin/sucrase-node"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- }
- },
- "node_modules/supports-preserve-symlinks-flag": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
- "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/tailwindcss": {
- "version": "3.4.17",
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
- "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@alloc/quick-lru": "^5.2.0",
- "arg": "^5.0.2",
- "chokidar": "^3.6.0",
- "didyoumean": "^1.2.2",
- "dlv": "^1.1.3",
- "fast-glob": "^3.3.2",
- "glob-parent": "^6.0.2",
- "is-glob": "^4.0.3",
- "jiti": "^1.21.6",
- "lilconfig": "^3.1.3",
- "micromatch": "^4.0.8",
- "normalize-path": "^3.0.0",
- "object-hash": "^3.0.0",
- "picocolors": "^1.1.1",
- "postcss": "^8.4.47",
- "postcss-import": "^15.1.0",
- "postcss-js": "^4.0.1",
- "postcss-load-config": "^4.0.2",
- "postcss-nested": "^6.2.0",
- "postcss-selector-parser": "^6.1.2",
- "resolve": "^1.22.8",
- "sucrase": "^3.35.0"
- },
- "bin": {
- "tailwind": "lib/cli.js",
- "tailwindcss": "lib/cli.js"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/tailwindcss/node_modules/postcss-selector-parser": {
- "version": "6.1.2",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
- "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "cssesc": "^3.0.0",
- "util-deprecate": "^1.0.2"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/thenify": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
- "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "any-promise": "^1.0.0"
- }
- },
- "node_modules/thenify-all": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
- "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "thenify": ">= 3.1.0 < 4"
- },
- "engines": {
- "node": ">=0.8"
- }
- },
- "node_modules/to-regex-range": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
- "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-number": "^7.0.0"
- },
- "engines": {
- "node": ">=8.0"
- }
- },
- "node_modules/ts-interface-checker": {
- "version": "0.1.13",
- "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
- "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
- "dev": true,
- "license": "Apache-2.0"
- },
- "node_modules/util-deprecate": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
- "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/which": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
- "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "isexe": "^2.0.0"
- },
- "bin": {
- "node-which": "bin/node-which"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/wrap-ansi": {
- "version": "8.1.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
- "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^6.1.0",
- "string-width": "^5.0.1",
- "strip-ansi": "^7.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
- "node_modules/wrap-ansi-cjs": {
- "name": "wrap-ansi",
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
- "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.0.0",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
- "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/wrap-ansi-cjs/node_modules/string-width": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/yaml": {
- "version": "2.6.1",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz",
- "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "yaml": "bin.mjs"
- },
- "engines": {
- "node": ">= 14"
- }
- }
- }
-}
diff --git a/package.json b/package.json
deleted file mode 100644
index 969943e4..00000000
--- a/package.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "devDependencies": {
- "@tailwindcss/typography": "^0.5.15",
- "tailwindcss": "^3.4.17"
- }
-}
diff --git a/pelican/__init__.py b/pelican/__init__.py
new file mode 100644
index 00000000..82366117
--- /dev/null
+++ b/pelican/__init__.py
@@ -0,0 +1,521 @@
+import argparse
+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 init as init_logging
+from pelican.generators import (ArticlesGenerator, # noqa: I100
+ PagesGenerator, SourceFileGenerator,
+ StaticGenerator, TemplatePagesGenerator)
+from pelican.plugins import signals
+from pelican.plugins._utils import load_plugins
+from pelican.readers import Readers
+from pelican.server import ComplexHTTPRequestHandler, RootedHTTPServer
+from pelican.settings import coerce_overrides, read_settings
+from pelican.utils import (FileSystemWatcher, clean_output_dir, maybe_pluralize)
+from pelican.writers import Writer
+
+try:
+ __version__ = __import__('pkg_resources') \
+ .get_distribution('pelican').version
+except Exception:
+ __version__ = "unknown"
+
+DEFAULT_CONFIG_NAME = 'pelicanconf.py'
+logger = logging.getLogger(__name__)
+
+
+class Pelican:
+
+ def __init__(self, settings):
+ """Pelican initialisation
+
+ 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 = load_plugins(self.settings)
+ for plugin in self.plugins:
+ logger.debug('Registering plugin `%s`', plugin.__name__)
+ try:
+ plugin.register()
+ except Exception as e:
+ logger.error('Cannot register plugin `%s`\n%s',
+ plugin.__name__, e)
+
+ 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()
+
+ for p in generators:
+ if hasattr(p, 'refresh_metadata_intersite_links'):
+ p.refresh_metadata_intersite_links()
+
+ signals.all_generators_finalized.send(generators)
+
+ 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_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')
+
+ print('Done: Processed {}, {}, {}, {} and {} in {:.2f} seconds.'
+ .format(
+ pluralized_articles,
+ pluralized_drafts,
+ pluralized_pages,
+ pluralized_hidden_pages,
+ pluralized_draft_pages,
+ time.time() - start_time))
+
+ def get_generator_classes(self):
+ generators = [ArticlesGenerator, PagesGenerator]
+
+ if self.settings['TEMPLATE_PAGES']:
+ generators.append(TemplatePagesGenerator)
+ if self.settings['OUTPUT_SOURCES']:
+ generators.append(SourceFileGenerator)
+
+ for pair in signals.get_generators.send(self):
+ (funct, value) = pair
+
+ if not isinstance(value, Iterable):
+ value = (value, )
+
+ for v in value:
+ if isinstance(v, type):
+ logger.debug('Found generator: %s', v)
+ generators.append(v)
+
+ # 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.
+ generators.append(StaticGenerator)
+ return generators
+
+ def get_writer(self):
+ writers = [w for (_, w) in signals.get_writer.send(self)
+ if isinstance(w, type)]
+ writers_found = len(writers)
+ if writers_found == 0:
+ return Writer(self.output_path, settings=self.settings)
+ else:
+ writer = writers[0]
+ if writers_found == 1:
+ logger.debug('Found writer: %s', writer)
+ else:
+ logger.warning(
+ '%s writers found, using only first one: %s',
+ writers_found, writer)
+ return writer(self.output_path, settings=self.settings)
+
+
+class PrintSettings(argparse.Action):
+ def __call__(self, parser, namespace, values, option_string):
+ instance, settings = get_instance(namespace)
+
+ 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):
+ setting_format = '\n{}:\n{}'
+ else:
+ setting_format = '\n{}: {}'
+ print(setting_format.format(
+ setting,
+ pprint.pformat(settings[setting])))
+ else:
+ print('\n{} is not a recognized setting.'.format(setting))
+ break
+ else:
+ # No argument was given to --print-settings, so print all settings
+ pprint.pprint(settings)
+
+ parser.exit()
+
+
+class ParseDict(argparse.Action):
+ def __call__(self, parser, namespace, values, option_string=None):
+ d = {}
+ if values:
+ for item in values:
+ split_items = item.split("=", 1)
+ key = split_items[0].strip()
+ value = split_items[1].strip()
+ d[key] = value
+ setattr(namespace, self.dest, d)
+
+
+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 '
+ 'automatically set to {} if a file exists with this '
+ 'name.'.format(DEFAULT_CONFIG_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('-w', '--write-selected', type=str,
+ dest='selected_paths', default=None,
+ help='Comma separated list of selected paths to write')
+
+ parser.add_argument('--fatal', metavar='errors|warnings',
+ choices=('errors', 'warnings'), default='',
+ help=('Exit the program with non-zero status if any '
+ 'errors/warnings encountered.'))
+
+ 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. If VALUE contains spaces, '
+ 'add quotes: SETTING="VALUE". Values other than '
+ 'integers and strings can be specified via JSON '
+ 'notation. (e.g., SETTING=none)',
+ nargs='*',
+ action=ParseDict
+ )
+
+ 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')
+
+ return args
+
+
+def get_config(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.selected_paths:
+ config['WRITE_SELECTED'] = args.selected_paths.split(',')
+ 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(coerce_overrides(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):
+ print(' --- AutoReload Mode: Monitoring `content`, `theme` and'
+ ' `settings` for changes. ---')
+ pelican, settings = get_instance(args)
+ watcher = FileSystemWatcher(args.settings, Readers, settings)
+ sleep = False
+ while True:
+ try:
+ # Don't sleep first time, but sleep afterwards to reduce cpu load
+ if sleep:
+ time.sleep(0.5)
+ else:
+ sleep = True
+
+ modified = watcher.check()
+
+ if modified['settings']:
+ pelican, settings = get_instance(args)
+ watcher.update_watchers(settings)
+
+ if any(modified.values()):
+ print('\n-> Modified: {}. re-generating...'.format(
+ ', '.join(k for k, v in modified.items() if v)))
+ pelican.run()
+
+ 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):
+ 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:
+ print("\nServing site at: {}:{} - Tap CTRL-C to stop".format(
+ server, port))
+ 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(args.verbosity, args.fatal,
+ 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))
+ p1.start()
+ p2.start()
+ exc = excqueue.get()
+ p1.terminate()
+ p2.terminate()
+ if exc is not None:
+ logger.critical(exc)
+ elif args.autoreload:
+ autoreload(args)
+ elif args.listen:
+ listen(settings.get('BIND'), settings.get('PORT'),
+ settings.get("OUTPUT_PATH"))
+ else:
+ watcher = FileSystemWatcher(args.settings, Readers, settings)
+ watcher.check()
+ pelican.run()
+ except KeyboardInterrupt:
+ logger.warning('Keyboard interrupt received. Exiting.')
+ except Exception as e:
+ logger.critical('%s', e)
+
+ if args.verbosity == logging.DEBUG:
+ raise
+ else:
+ sys.exit(getattr(e, 'exitcode', 1))
diff --git a/pelican/__main__.py b/pelican/__main__.py
new file mode 100644
index 00000000..69a5b95d
--- /dev/null
+++ b/pelican/__main__.py
@@ -0,0 +1,9 @@
+"""
+python -m pelican module entry point to run via python -m
+"""
+
+from . import main
+
+
+if __name__ == '__main__':
+ main()
diff --git a/pelican/cache.py b/pelican/cache.py
new file mode 100644
index 00000000..646f97bc
--- /dev/null
+++ b/pelican/cache.py
@@ -0,0 +1,134 @@
+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) 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 sublcass 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
diff --git a/pelican/contents.py b/pelican/contents.py
new file mode 100644
index 00000000..6470ee45
--- /dev/null
+++ b/pelican/contents.py
@@ -0,0 +1,607 @@
+import copy
+import datetime
+import locale
+import logging
+import os
+import re
+from urllib.parse import urljoin, urlparse, urlunparse
+
+import pytz
+
+from pelican.plugins import signals
+from pelican.settings import DEFAULT_CONFIG
+from pelican.utils import (deprecated_attribute, memoized, path_to_url,
+ posixize_path, sanitised_join, set_date_tzinfo,
+ slugify, truncate_html_words)
+
+# Import these so that they're avalaible when you import from pelican.contents.
+from pelican.urlwrappers import (Author, Category, Tag, URLWrapper) # NOQA
+
+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.
+
+ """
+ @deprecated_attribute(old='filename', new='source_path', since=(3, 2, 0))
+ def filename():
+ return None
+
+ def __init__(self, content, metadata=None, settings=None,
+ source_path=None, context=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 = dict()
+ 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 = pytz.timezone(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):
+ return self.source_path or repr(self)
+
+ def _has_valid_mandatory_properties(self):
+ """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):
+ """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):
+ if hasattr(self, 'allowed_statuses'):
+ if self.status not in self.allowed_statuses:
+ logger.error(
+ "Unknown status '%s' for file %s, skipping it.",
+ self.status,
+ self
+ )
+ return False
+
+ # if undefined we allow all
+ return True
+
+ def is_valid(self):
+ """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):
+ """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, klass=None):
+ if not klass:
+ klass = self.__class__.__name__
+ fq_key = ('{}_{}'.format(klass, key)).upper()
+ return self.settings[fq_key].format(**self.url_format)
+
+ def get_url_setting(self, key):
+ if hasattr(self, 'override_' + key):
+ return getattr(self, 'override_' + key)
+ key = key if self.in_default_lang else 'lang_%s' % key
+ return self._expand_settings(key)
+
+ def _link_replacer(self, siteurl, m):
+ 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'}:
+ 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)
+ )
+
+ key = 'static_content' if what in ('static', 'attach')\
+ else 'generated_content'
+
+ def _get_linked_content(key, path):
+ try:
+ return self._context[key][path]
+ except KeyError:
+ try:
+ # Markdown escapes spaces, try unescaping
+ return self._context[key][path.replace('%20', ' ')]
+ except KeyError:
+ if what == 'filename' and key == 'generated_content':
+ key = 'static_content'
+ linked_content = _get_linked_content(key, path)
+ if linked_content:
+ logger.warning(
+ '{filename} used for linking to static'
+ ' content %s in %s. Use {static} instead',
+ path,
+ self.get_relative_source_path())
+ return linked_content
+ return None
+
+ linked_content = _get_linked_content(key, path)
+ if linked_content:
+ if what == 'attach':
+ linked_content.attach_to(self)
+ 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):
+ intrasite_link_regex = self.settings['INTRASITE_LINK_REGEX']
+ regex = r"""
+ (?P<[^\>]+ # match tag with all url-value attributes
+ (?:href|src|poster|data|cite|formaction|action)\s*=\s*)
+
+ (?P["\']) # require value to be quoted
+ (?P{}(?P.*?)) # the url value
+ \2""".format(intrasite_link_regex)
+ return re.compile(regex, re.X)
+
+ def _update_content(self, content, siteurl):
+ """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):
+ 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', ' ')
+ static_links.add(path)
+ return static_links
+
+ def get_siteurl(self):
+ return self._context.get('localsiteurl', '')
+
+ @memoized
+ def get_content(self, siteurl):
+ if hasattr(self, '_get_content'):
+ content = self._get_content()
+ else:
+ content = self._content
+ return self._update_content(content, siteurl)
+
+ @property
+ def content(self):
+ return self.get_content(self.get_siteurl())
+
+ @memoized
+ def get_summary(self, siteurl):
+ """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']
+
+ if self.settings['SUMMARY_MAX_LENGTH'] is None:
+ return self.content
+
+ return truncate_html_words(self.content,
+ self.settings['SUMMARY_MAX_LENGTH'],
+ self.settings['SUMMARY_END_SUFFIX'])
+
+ @property
+ def summary(self):
+ return self.get_summary(self.get_siteurl())
+
+ def _get_summary(self):
+ """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):
+ """Dummy function"""
+ pass
+
+ @property
+ def status(self):
+ return self._status
+
+ @status.setter
+ def status(self, value):
+ # TODO maybe typecheck
+ self._status = value.lower()
+
+ @property
+ def url(self):
+ return self.get_url_setting('url')
+
+ @property
+ def save_as(self):
+ return self.get_url_setting('save_as')
+
+ def _get_template(self):
+ 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=None):
+ """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):
+ 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):
+ 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
+ if ('summary' in self.settings['FORMATTED_FIELDS'] and
+ 'summary' in self.metadata):
+ self._summary = self._update_content(
+ self._summary,
+ self.get_siteurl()
+ )
+ self.metadata['summary'] = self._summary
+
+
+class Page(Content):
+ mandatory_properties = ('title',)
+ allowed_statuses = ('published', 'hidden', 'draft')
+ default_status = 'published'
+ default_template = 'page'
+
+ def _expand_settings(self, key):
+ 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', 'draft')
+ 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=pytz.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):
+ 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):
+ 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):
+ # 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):
+ # 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):
+ """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):
+ 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
diff --git a/pelican/generators.py b/pelican/generators.py
new file mode 100644
index 00000000..63e20a0a
--- /dev/null
+++ b/pelican/generators.py
@@ -0,0 +1,934 @@
+import calendar
+import errno
+import fnmatch
+import logging
+import os
+from collections import defaultdict
+from functools import partial
+from itertools import chain, groupby
+from operator import attrgetter
+
+from jinja2 import (BaseLoader, ChoiceLoader, Environment, FileSystemLoader,
+ PrefixLoader, TemplateNotFound)
+
+from pelican.cache import FileStampDataCacher
+from pelican.contents import Article, Page, Static
+from pelican.plugins import signals
+from pelican.readers import Readers
+from pelican.utils import (DateFormatter, copy, mkdir_p, order_content,
+ posixize_path, process_translations)
+
+
+logger = logging.getLogger(__name__)
+
+
+class PelicanTemplateNotFound(Exception):
+ pass
+
+
+class Generator:
+ """Baseclass generator"""
+
+ def __init__(self, context, settings, path, theme, output_path,
+ readers_cache_name='', **kwargs):
+ self.context = context
+ self.settings = settings
+ self.path = path
+ self.theme = theme
+ self.output_path = output_path
+
+ for arg, value in kwargs.items():
+ setattr(self, arg, value)
+
+ self.readers = Readers(self.settings, readers_cache_name)
+
+ # templates cache
+ self._templates = {}
+ self._templates_path = list(self.settings['THEME_TEMPLATES_OVERRIDES'])
+
+ theme_templates_path = os.path.expanduser(
+ os.path.join(self.theme, 'templates'))
+ self._templates_path.append(theme_templates_path)
+ theme_loader = FileSystemLoader(theme_templates_path)
+
+ simple_theme_path = os.path.dirname(os.path.abspath(__file__))
+ simple_loader = FileSystemLoader(
+ os.path.join(simple_theme_path, "themes", "simple", "templates"))
+
+ self.env = Environment(
+ loader=ChoiceLoader([
+ FileSystemLoader(self._templates_path),
+ simple_loader, # implicit inheritance
+ PrefixLoader({
+ '!simple': simple_loader,
+ '!theme': theme_loader
+ }) # explicit ones
+ ]),
+ **self.settings['JINJA_ENVIRONMENT']
+ )
+
+ logger.debug('Template list: %s', self.env.list_templates())
+
+ # provide utils.strftime as a jinja filter
+ self.env.filters.update({'strftime': DateFormatter()})
+
+ # get custom Jinja filters from user settings
+ custom_filters = self.settings['JINJA_FILTERS']
+ self.env.filters.update(custom_filters)
+
+ # get custom Jinja globals from user settings
+ custom_globals = self.settings['JINJA_GLOBALS']
+ self.env.globals.update(custom_globals)
+
+ # get custom Jinja tests from user settings
+ custom_tests = self.settings['JINJA_TESTS']
+ self.env.tests.update(custom_tests)
+
+ signals.generator_init.send(self)
+
+ def get_template(self, name):
+ """Return the template by name.
+ Use self.theme to get the templates to use, and return a list of
+ templates ready to use with Jinja2.
+ """
+ if name not in self._templates:
+ for ext in self.settings['TEMPLATE_EXTENSIONS']:
+ try:
+ self._templates[name] = self.env.get_template(name + ext)
+ break
+ except TemplateNotFound:
+ continue
+
+ if name not in self._templates:
+ raise PelicanTemplateNotFound(
+ '[templates] unable to load {}[{}] from {}'.format(
+ name, ', '.join(self.settings['TEMPLATE_EXTENSIONS']),
+ self._templates_path))
+
+ return self._templates[name]
+
+ def _include_path(self, path, extensions=None):
+ """Inclusion logic for .get_files(), returns True/False
+
+ :param path: the path which might be including
+ :param extensions: the list of allowed extensions, or False if all
+ extensions are allowed
+ """
+ if extensions is None:
+ extensions = tuple(self.readers.extensions)
+ basename = os.path.basename(path)
+
+ # check IGNORE_FILES
+ ignores = self.settings['IGNORE_FILES']
+ if any(fnmatch.fnmatch(basename, ignore) for ignore in ignores):
+ return False
+
+ ext = os.path.splitext(basename)[1][1:]
+ if extensions is False or ext in extensions:
+ return True
+
+ return False
+
+ def get_files(self, paths, exclude=[], extensions=None):
+ """Return a list of files to use, based on rules
+
+ :param paths: the list pf paths to search (relative to self.path)
+ :param exclude: the list of path to exclude
+ :param extensions: the list of allowed extensions (if False, all
+ extensions are allowed)
+ """
+ # backward compatibility for older generators
+ if isinstance(paths, str):
+ paths = [paths]
+
+ # group the exclude dir names by parent path, for use with os.walk()
+ exclusions_by_dirpath = {}
+ for e in exclude:
+ parent_path, subdir = os.path.split(os.path.join(self.path, e))
+ exclusions_by_dirpath.setdefault(parent_path, set()).add(subdir)
+
+ files = set()
+ ignores = self.settings['IGNORE_FILES']
+ for path in paths:
+ # careful: os.path.join() will add a slash when path == ''.
+ root = os.path.join(self.path, path) if path else self.path
+
+ if os.path.isdir(root):
+ for dirpath, dirs, temp_files in os.walk(
+ root, topdown=True, followlinks=True):
+ excl = exclusions_by_dirpath.get(dirpath, ())
+ # We copy the `dirs` list as we will modify it in the loop:
+ for d in list(dirs):
+ if (d in excl or
+ any(fnmatch.fnmatch(d, ignore)
+ for ignore in ignores)):
+ if d in dirs:
+ dirs.remove(d)
+
+ reldir = os.path.relpath(dirpath, self.path)
+ for f in temp_files:
+ fp = os.path.join(reldir, f)
+ if self._include_path(fp, extensions):
+ files.add(fp)
+ elif os.path.exists(root) and self._include_path(path, extensions):
+ files.add(path) # can't walk non-directories
+ return files
+
+ def add_source_path(self, content, static=False):
+ """Record a source file path that a Generator found and processed.
+ Store a reference to its Content object, for url lookups later.
+ """
+ location = content.get_relative_source_path()
+ key = 'static_content' if static else 'generated_content'
+ self.context[key][location] = content
+
+ def _add_failed_source_path(self, path, static=False):
+ """Record a source file path that a Generator failed to process.
+ (For example, one that was missing mandatory metadata.)
+ The path argument is expected to be relative to self.path.
+ """
+ key = 'static_content' if static else 'generated_content'
+ self.context[key][posixize_path(os.path.normpath(path))] = None
+
+ def _is_potential_source_path(self, path, static=False):
+ """Return True if path was supposed to be used as a source file.
+ (This includes all source files that have been found by generators
+ before this method is called, even if they failed to process.)
+ The path argument is expected to be relative to self.path.
+ """
+ key = 'static_content' if static else 'generated_content'
+ return (posixize_path(os.path.normpath(path)) in self.context[key])
+
+ def add_static_links(self, content):
+ """Add file links in content to context to be processed as Static
+ content.
+ """
+ self.context['static_links'] |= content.get_static_links()
+
+ def _update_context(self, items):
+ """Update the context with the given items from the currrent
+ processor.
+ """
+ for item in items:
+ value = getattr(self, item)
+ if hasattr(value, 'items'):
+ value = list(value.items()) # py3k safeguard for iterators
+ self.context[item] = value
+
+ def __str__(self):
+ # return the name of the class for logging purposes
+ return self.__class__.__name__
+
+
+class CachingGenerator(Generator, FileStampDataCacher):
+ '''Subclass of Generator and FileStampDataCacher classes
+
+ enables content caching, either at the generator or reader level
+ '''
+
+ def __init__(self, *args, **kwargs):
+ '''Initialize the generator, then set up caching
+
+ note the multiple inheritance structure
+ '''
+ cls_name = self.__class__.__name__
+ Generator.__init__(self, *args,
+ readers_cache_name=(cls_name + '-Readers'),
+ **kwargs)
+
+ cache_this_level = \
+ self.settings['CONTENT_CACHING_LAYER'] == 'generator'
+ caching_policy = cache_this_level and self.settings['CACHE_CONTENT']
+ load_policy = cache_this_level and self.settings['LOAD_CONTENT_CACHE']
+ FileStampDataCacher.__init__(self, self.settings, cls_name,
+ caching_policy, load_policy
+ )
+
+ def _get_file_stamp(self, filename):
+ '''Get filestamp for path relative to generator.path'''
+ filename = os.path.join(self.path, filename)
+ return super()._get_file_stamp(filename)
+
+
+class _FileLoader(BaseLoader):
+
+ def __init__(self, path, basedir):
+ self.path = path
+ self.fullpath = os.path.join(basedir, path)
+
+ def get_source(self, environment, template):
+ if template != self.path or not os.path.exists(self.fullpath):
+ raise TemplateNotFound(template)
+ mtime = os.path.getmtime(self.fullpath)
+ with open(self.fullpath, encoding='utf-8') as f:
+ source = f.read()
+ return (source, self.fullpath,
+ lambda: mtime == os.path.getmtime(self.fullpath))
+
+
+class TemplatePagesGenerator(Generator):
+
+ def generate_output(self, writer):
+ for source, dest in self.settings['TEMPLATE_PAGES'].items():
+ self.env.loader.loaders.insert(0, _FileLoader(source, self.path))
+ try:
+ template = self.env.get_template(source)
+ rurls = self.settings['RELATIVE_URLS']
+ writer.write_file(dest, template, self.context, rurls,
+ override_output=True, url='')
+ finally:
+ del self.env.loader.loaders[0]
+
+
+class ArticlesGenerator(CachingGenerator):
+ """Generate blog articles"""
+
+ def __init__(self, *args, **kwargs):
+ """initialize properties"""
+ self.articles = [] # only articles in default language
+ self.translations = []
+ self.dates = {}
+ self.tags = defaultdict(list)
+ self.categories = defaultdict(list)
+ self.related_posts = []
+ self.authors = defaultdict(list)
+ self.drafts = [] # only drafts in default language
+ self.drafts_translations = []
+ super().__init__(*args, **kwargs)
+ signals.article_generator_init.send(self)
+
+ def generate_feeds(self, writer):
+ """Generate the feeds from the current context, and output files."""
+
+ if self.settings.get('FEED_ATOM'):
+ writer.write_feed(
+ self.articles,
+ self.context,
+ self.settings['FEED_ATOM'],
+ self.settings.get('FEED_ATOM_URL', self.settings['FEED_ATOM'])
+ )
+
+ if self.settings.get('FEED_RSS'):
+ writer.write_feed(
+ self.articles,
+ self.context,
+ self.settings['FEED_RSS'],
+ self.settings.get('FEED_RSS_URL', self.settings['FEED_RSS']),
+ feed_type='rss'
+ )
+
+ if (self.settings.get('FEED_ALL_ATOM') or
+ self.settings.get('FEED_ALL_RSS')):
+ all_articles = list(self.articles)
+ for article in self.articles:
+ all_articles.extend(article.translations)
+ order_content(all_articles,
+ order_by=self.settings['ARTICLE_ORDER_BY'])
+
+ if self.settings.get('FEED_ALL_ATOM'):
+ writer.write_feed(
+ all_articles,
+ self.context,
+ self.settings['FEED_ALL_ATOM'],
+ self.settings.get('FEED_ALL_ATOM_URL',
+ self.settings['FEED_ALL_ATOM'])
+ )
+
+ if self.settings.get('FEED_ALL_RSS'):
+ writer.write_feed(
+ all_articles,
+ self.context,
+ self.settings['FEED_ALL_RSS'],
+ self.settings.get('FEED_ALL_RSS_URL',
+ self.settings['FEED_ALL_RSS']),
+ feed_type='rss'
+ )
+
+ for cat, arts in self.categories:
+ if self.settings.get('CATEGORY_FEED_ATOM'):
+ writer.write_feed(
+ arts,
+ self.context,
+ self.settings['CATEGORY_FEED_ATOM'].format(slug=cat.slug),
+ self.settings.get(
+ 'CATEGORY_FEED_ATOM_URL',
+ self.settings['CATEGORY_FEED_ATOM']).format(
+ slug=cat.slug
+ ),
+ feed_title=cat.name
+ )
+
+ if self.settings.get('CATEGORY_FEED_RSS'):
+ writer.write_feed(
+ arts,
+ self.context,
+ self.settings['CATEGORY_FEED_RSS'].format(slug=cat.slug),
+ self.settings.get(
+ 'CATEGORY_FEED_RSS_URL',
+ self.settings['CATEGORY_FEED_RSS']).format(
+ slug=cat.slug
+ ),
+ feed_title=cat.name,
+ feed_type='rss'
+ )
+
+ for auth, arts in self.authors:
+ if self.settings.get('AUTHOR_FEED_ATOM'):
+ writer.write_feed(
+ arts,
+ self.context,
+ self.settings['AUTHOR_FEED_ATOM'].format(slug=auth.slug),
+ self.settings.get(
+ 'AUTHOR_FEED_ATOM_URL',
+ self.settings['AUTHOR_FEED_ATOM']
+ ).format(slug=auth.slug),
+ feed_title=auth.name
+ )
+
+ if self.settings.get('AUTHOR_FEED_RSS'):
+ writer.write_feed(
+ arts,
+ self.context,
+ self.settings['AUTHOR_FEED_RSS'].format(slug=auth.slug),
+ self.settings.get(
+ 'AUTHOR_FEED_RSS_URL',
+ self.settings['AUTHOR_FEED_RSS']
+ ).format(slug=auth.slug),
+ feed_title=auth.name,
+ feed_type='rss'
+ )
+
+ if (self.settings.get('TAG_FEED_ATOM') or
+ self.settings.get('TAG_FEED_RSS')):
+ for tag, arts in self.tags.items():
+ if self.settings.get('TAG_FEED_ATOM'):
+ writer.write_feed(
+ arts,
+ self.context,
+ self.settings['TAG_FEED_ATOM'].format(slug=tag.slug),
+ self.settings.get(
+ 'TAG_FEED_ATOM_URL',
+ self.settings['TAG_FEED_ATOM']
+ ).format(slug=tag.slug),
+ feed_title=tag.name
+ )
+
+ if self.settings.get('TAG_FEED_RSS'):
+ writer.write_feed(
+ arts,
+ self.context,
+ self.settings['TAG_FEED_RSS'].format(slug=tag.slug),
+ self.settings.get(
+ 'TAG_FEED_RSS_URL',
+ self.settings['TAG_FEED_RSS']
+ ).format(slug=tag.slug),
+ feed_title=tag.name,
+ feed_type='rss'
+ )
+
+ if (self.settings.get('TRANSLATION_FEED_ATOM') or
+ self.settings.get('TRANSLATION_FEED_RSS')):
+ translations_feeds = defaultdict(list)
+ for article in chain(self.articles, self.translations):
+ translations_feeds[article.lang].append(article)
+
+ for lang, items in translations_feeds.items():
+ items = order_content(
+ items, order_by=self.settings['ARTICLE_ORDER_BY'])
+ if self.settings.get('TRANSLATION_FEED_ATOM'):
+ writer.write_feed(
+ items,
+ self.context,
+ self.settings['TRANSLATION_FEED_ATOM']
+ .format(lang=lang),
+ self.settings.get(
+ 'TRANSLATION_FEED_ATOM_URL',
+ self.settings['TRANSLATION_FEED_ATOM']
+ ).format(lang=lang),
+ )
+ if self.settings.get('TRANSLATION_FEED_RSS'):
+ writer.write_feed(
+ items,
+ self.context,
+ self.settings['TRANSLATION_FEED_RSS']
+ .format(lang=lang),
+ self.settings.get(
+ 'TRANSLATION_FEED_RSS_URL',
+ self.settings['TRANSLATION_FEED_RSS']
+ ).format(lang=lang),
+ feed_type='rss'
+ )
+
+ def generate_articles(self, write):
+ """Generate the articles."""
+ for article in chain(self.translations, self.articles):
+ signals.article_generator_write_article.send(self, content=article)
+ write(article.save_as, self.get_template(article.template),
+ self.context, article=article, category=article.category,
+ override_output=hasattr(article, 'override_save_as'),
+ url=article.url, blog=True)
+
+ def generate_period_archives(self, write):
+ """Generate per-year, per-month, and per-day archives."""
+ try:
+ template = self.get_template('period_archives')
+ except PelicanTemplateNotFound:
+ template = self.get_template('archives')
+
+ period_save_as = {
+ 'year': self.settings['YEAR_ARCHIVE_SAVE_AS'],
+ 'month': self.settings['MONTH_ARCHIVE_SAVE_AS'],
+ 'day': self.settings['DAY_ARCHIVE_SAVE_AS'],
+ }
+
+ period_url = {
+ 'year': self.settings['YEAR_ARCHIVE_URL'],
+ 'month': self.settings['MONTH_ARCHIVE_URL'],
+ 'day': self.settings['DAY_ARCHIVE_URL'],
+ }
+
+ period_date_key = {
+ 'year': attrgetter('date.year'),
+ 'month': attrgetter('date.year', 'date.month'),
+ 'day': attrgetter('date.year', 'date.month', 'date.day')
+ }
+
+ def _generate_period_archives(dates, key, save_as_fmt, url_fmt):
+ """Generate period archives from `dates`, grouped by
+ `key` and written to `save_as`.
+ """
+ # `dates` is already sorted by date
+ for _period, group in groupby(dates, key=key):
+ archive = list(group)
+ articles = [a for a in self.articles if a in archive]
+ # arbitrarily grab the first date so that the usual
+ # format string syntax can be used for specifying the
+ # period archive dates
+ date = archive[0].date
+ save_as = save_as_fmt.format(date=date)
+ url = url_fmt.format(date=date)
+ context = self.context.copy()
+
+ if key == period_date_key['year']:
+ context["period"] = (_period,)
+ else:
+ month_name = calendar.month_name[_period[1]]
+ if key == period_date_key['month']:
+ context["period"] = (_period[0],
+ month_name)
+ else:
+ context["period"] = (_period[0],
+ month_name,
+ _period[2])
+
+ write(save_as, template, context, articles=articles,
+ dates=archive, template_name='period_archives',
+ blog=True, url=url, all_articles=self.articles)
+
+ for period in 'year', 'month', 'day':
+ save_as = period_save_as[period]
+ url = period_url[period]
+ if save_as:
+ key = period_date_key[period]
+ _generate_period_archives(self.dates, key, save_as, url)
+
+ def generate_direct_templates(self, write):
+ """Generate direct templates pages"""
+ for template in self.settings['DIRECT_TEMPLATES']:
+ save_as = self.settings.get("%s_SAVE_AS" % template.upper(),
+ '%s.html' % template)
+ url = self.settings.get("%s_URL" % template.upper(),
+ '%s.html' % template)
+ if not save_as:
+ continue
+
+ write(save_as, self.get_template(template), self.context,
+ articles=self.articles, dates=self.dates, blog=True,
+ template_name=template,
+ page_name=os.path.splitext(save_as)[0], url=url)
+
+ def generate_tags(self, write):
+ """Generate Tags pages."""
+ tag_template = self.get_template('tag')
+ for tag, articles in self.tags.items():
+ dates = [article for article in self.dates if article in articles]
+ write(tag.save_as, tag_template, self.context, tag=tag,
+ url=tag.url, articles=articles, dates=dates,
+ template_name='tag', blog=True, page_name=tag.page_name,
+ all_articles=self.articles)
+
+ def generate_categories(self, write):
+ """Generate category pages."""
+ category_template = self.get_template('category')
+ for cat, articles in self.categories:
+ dates = [article for article in self.dates if article in articles]
+ write(cat.save_as, category_template, self.context, url=cat.url,
+ category=cat, articles=articles, dates=dates,
+ template_name='category', blog=True, page_name=cat.page_name,
+ all_articles=self.articles)
+
+ def generate_authors(self, write):
+ """Generate Author pages."""
+ author_template = self.get_template('author')
+ for aut, articles in self.authors:
+ dates = [article for article in self.dates if article in articles]
+ write(aut.save_as, author_template, self.context,
+ url=aut.url, author=aut, articles=articles, dates=dates,
+ template_name='author', blog=True,
+ page_name=aut.page_name, all_articles=self.articles)
+
+ def generate_drafts(self, write):
+ """Generate drafts pages."""
+ for draft in chain(self.drafts_translations, self.drafts):
+ write(draft.save_as, self.get_template(draft.template),
+ self.context, article=draft, category=draft.category,
+ override_output=hasattr(draft, 'override_save_as'),
+ blog=True, all_articles=self.articles, url=draft.url)
+
+ def generate_pages(self, writer):
+ """Generate the pages on the disk"""
+ write = partial(writer.write_file,
+ relative_urls=self.settings['RELATIVE_URLS'])
+
+ # to minimize the number of relative path stuff modification
+ # in writer, articles pass first
+ self.generate_articles(write)
+ self.generate_period_archives(write)
+ self.generate_direct_templates(write)
+
+ # and subfolders after that
+ self.generate_tags(write)
+ self.generate_categories(write)
+ self.generate_authors(write)
+ self.generate_drafts(write)
+
+ def generate_context(self):
+ """Add the articles into the shared context"""
+
+ all_articles = []
+ all_drafts = []
+ for f in self.get_files(
+ self.settings['ARTICLE_PATHS'],
+ exclude=self.settings['ARTICLE_EXCLUDES']):
+ article = self.get_cached_data(f, None)
+ if article is None:
+ try:
+ article = self.readers.read_file(
+ base_path=self.path, path=f, content_class=Article,
+ context=self.context,
+ preread_signal=signals.article_generator_preread,
+ preread_sender=self,
+ context_signal=signals.article_generator_context,
+ context_sender=self)
+ except Exception as e:
+ logger.error(
+ 'Could not process %s\n%s', f, e,
+ exc_info=self.settings.get('DEBUG', False))
+ self._add_failed_source_path(f)
+ continue
+
+ if not article.is_valid():
+ self._add_failed_source_path(f)
+ continue
+
+ self.cache_data(f, article)
+
+ if article.status == "published":
+ all_articles.append(article)
+ elif article.status == "draft":
+ all_drafts.append(article)
+ self.add_source_path(article)
+ self.add_static_links(article)
+
+ def _process(arts):
+ origs, translations = process_translations(
+ arts, translation_id=self.settings['ARTICLE_TRANSLATION_ID'])
+ origs = order_content(origs, self.settings['ARTICLE_ORDER_BY'])
+ return origs, translations
+
+ self.articles, self.translations = _process(all_articles)
+ self.drafts, self.drafts_translations = _process(all_drafts)
+
+ signals.article_generator_pretaxonomy.send(self)
+
+ for article in self.articles:
+ # only main articles are listed in categories and tags
+ # not translations
+ self.categories[article.category].append(article)
+ if hasattr(article, 'tags'):
+ for tag in article.tags:
+ self.tags[tag].append(article)
+ for author in getattr(article, 'authors', []):
+ self.authors[author].append(article)
+
+ self.dates = list(self.articles)
+ self.dates.sort(key=attrgetter('date'),
+ reverse=self.context['NEWEST_FIRST_ARCHIVES'])
+
+ # and generate the output :)
+
+ # order the categories per name
+ self.categories = list(self.categories.items())
+ self.categories.sort(
+ reverse=self.settings['REVERSE_CATEGORY_ORDER'])
+
+ self.authors = list(self.authors.items())
+ self.authors.sort()
+
+ self._update_context(('articles', 'dates', 'tags', 'categories',
+ 'authors', 'related_posts', 'drafts'))
+ self.save_cache()
+ self.readers.save_cache()
+ signals.article_generator_finalized.send(self)
+
+ def generate_output(self, writer):
+ self.generate_feeds(writer)
+ self.generate_pages(writer)
+ signals.article_writer_finalized.send(self, writer=writer)
+
+ def refresh_metadata_intersite_links(self):
+ for e in chain(self.articles,
+ self.translations,
+ self.drafts,
+ self.drafts_translations):
+ if hasattr(e, 'refresh_metadata_intersite_links'):
+ e.refresh_metadata_intersite_links()
+
+
+class PagesGenerator(CachingGenerator):
+ """Generate pages"""
+
+ def __init__(self, *args, **kwargs):
+ self.pages = []
+ self.translations = []
+ self.hidden_pages = []
+ self.hidden_translations = []
+ self.draft_pages = []
+ self.draft_translations = []
+ super().__init__(*args, **kwargs)
+ signals.page_generator_init.send(self)
+
+ def generate_context(self):
+ all_pages = []
+ hidden_pages = []
+ draft_pages = []
+ for f in self.get_files(
+ self.settings['PAGE_PATHS'],
+ exclude=self.settings['PAGE_EXCLUDES']):
+ page = self.get_cached_data(f, None)
+ if page is None:
+ try:
+ page = self.readers.read_file(
+ base_path=self.path, path=f, content_class=Page,
+ context=self.context,
+ preread_signal=signals.page_generator_preread,
+ preread_sender=self,
+ context_signal=signals.page_generator_context,
+ context_sender=self)
+ except Exception as e:
+ logger.error(
+ 'Could not process %s\n%s', f, e,
+ exc_info=self.settings.get('DEBUG', False))
+ self._add_failed_source_path(f)
+ continue
+
+ if not page.is_valid():
+ self._add_failed_source_path(f)
+ continue
+
+ self.cache_data(f, page)
+
+ if page.status == "published":
+ all_pages.append(page)
+ elif page.status == "hidden":
+ hidden_pages.append(page)
+ elif page.status == "draft":
+ draft_pages.append(page)
+ self.add_source_path(page)
+ self.add_static_links(page)
+
+ def _process(pages):
+ origs, translations = process_translations(
+ pages, translation_id=self.settings['PAGE_TRANSLATION_ID'])
+ origs = order_content(origs, self.settings['PAGE_ORDER_BY'])
+ return origs, translations
+
+ self.pages, self.translations = _process(all_pages)
+ self.hidden_pages, self.hidden_translations = _process(hidden_pages)
+ self.draft_pages, self.draft_translations = _process(draft_pages)
+
+ self._update_context(('pages', 'hidden_pages', 'draft_pages'))
+
+ self.save_cache()
+ self.readers.save_cache()
+ signals.page_generator_finalized.send(self)
+
+ def generate_output(self, writer):
+ for page in chain(self.translations, self.pages,
+ self.hidden_translations, self.hidden_pages,
+ self.draft_translations, self.draft_pages):
+ signals.page_generator_write_page.send(self, content=page)
+ writer.write_file(
+ page.save_as, self.get_template(page.template),
+ self.context, page=page,
+ relative_urls=self.settings['RELATIVE_URLS'],
+ override_output=hasattr(page, 'override_save_as'),
+ url=page.url)
+ signals.page_writer_finalized.send(self, writer=writer)
+
+ def refresh_metadata_intersite_links(self):
+ for e in chain(self.pages,
+ self.hidden_pages,
+ self.hidden_translations,
+ self.draft_pages,
+ self.draft_translations):
+ if hasattr(e, 'refresh_metadata_intersite_links'):
+ e.refresh_metadata_intersite_links()
+
+
+class StaticGenerator(Generator):
+ """copy static paths (what you want to copy, like images, medias etc.
+ to output"""
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fallback_to_symlinks = False
+ signals.static_generator_init.send(self)
+
+ def generate_context(self):
+ self.staticfiles = []
+ linked_files = set(self.context['static_links'])
+ found_files = self.get_files(self.settings['STATIC_PATHS'],
+ exclude=self.settings['STATIC_EXCLUDES'],
+ extensions=False)
+ for f in linked_files | found_files:
+
+ # skip content source files unless the user explicitly wants them
+ if self.settings['STATIC_EXCLUDE_SOURCES']:
+ if self._is_potential_source_path(f):
+ continue
+
+ static = self.readers.read_file(
+ base_path=self.path, path=f, content_class=Static,
+ fmt='static', context=self.context,
+ preread_signal=signals.static_generator_preread,
+ preread_sender=self,
+ context_signal=signals.static_generator_context,
+ context_sender=self)
+ self.staticfiles.append(static)
+ self.add_source_path(static, static=True)
+ self._update_context(('staticfiles',))
+ signals.static_generator_finalized.send(self)
+
+ def generate_output(self, writer):
+ self._copy_paths(self.settings['THEME_STATIC_PATHS'], self.theme,
+ self.settings['THEME_STATIC_DIR'], self.output_path,
+ os.curdir)
+ for sc in self.context['staticfiles']:
+ if self._file_update_required(sc):
+ self._link_or_copy_staticfile(sc)
+ else:
+ logger.debug('%s is up to date, not copying', sc.source_path)
+
+ def _copy_paths(self, paths, source, destination, output_path,
+ final_path=None):
+ """Copy all the paths from source to destination"""
+ for path in paths:
+ source_path = os.path.join(source, path)
+
+ if final_path:
+ if os.path.isfile(source_path):
+ destination_path = os.path.join(output_path, destination,
+ final_path,
+ os.path.basename(path))
+ else:
+ destination_path = os.path.join(output_path, destination,
+ final_path)
+ else:
+ destination_path = os.path.join(output_path, destination, path)
+
+ copy(source_path, destination_path,
+ self.settings['IGNORE_FILES'])
+
+ def _file_update_required(self, staticfile):
+ source_path = os.path.join(self.path, staticfile.source_path)
+ save_as = os.path.join(self.output_path, staticfile.save_as)
+ if not os.path.exists(save_as):
+ return True
+ elif (self.settings['STATIC_CREATE_LINKS'] and
+ os.path.samefile(source_path, save_as)):
+ return False
+ elif (self.settings['STATIC_CREATE_LINKS'] and
+ os.path.realpath(save_as) == source_path):
+ return False
+ elif not self.settings['STATIC_CHECK_IF_MODIFIED']:
+ return True
+ else:
+ return self._source_is_newer(staticfile)
+
+ def _source_is_newer(self, staticfile):
+ source_path = os.path.join(self.path, staticfile.source_path)
+ save_as = os.path.join(self.output_path, staticfile.save_as)
+ s_mtime = os.path.getmtime(source_path)
+ d_mtime = os.path.getmtime(save_as)
+ return s_mtime - d_mtime > 0.000001
+
+ def _link_or_copy_staticfile(self, sc):
+ if self.settings['STATIC_CREATE_LINKS']:
+ self._link_staticfile(sc)
+ else:
+ self._copy_staticfile(sc)
+
+ def _copy_staticfile(self, sc):
+ source_path = os.path.join(self.path, sc.source_path)
+ save_as = os.path.join(self.output_path, sc.save_as)
+ self._mkdir(os.path.dirname(save_as))
+ copy(source_path, save_as)
+ logger.info('Copying %s to %s', sc.source_path, sc.save_as)
+
+ def _link_staticfile(self, sc):
+ source_path = os.path.join(self.path, sc.source_path)
+ save_as = os.path.join(self.output_path, sc.save_as)
+ self._mkdir(os.path.dirname(save_as))
+ try:
+ if os.path.lexists(save_as):
+ os.unlink(save_as)
+ logger.info('Linking %s and %s', sc.source_path, sc.save_as)
+ if self.fallback_to_symlinks:
+ os.symlink(source_path, save_as)
+ else:
+ os.link(source_path, save_as)
+ except OSError as err:
+ if err.errno == errno.EXDEV: # 18: Invalid cross-device link
+ logger.debug(
+ "Cross-device links not valid. "
+ "Creating symbolic links instead."
+ )
+ self.fallback_to_symlinks = True
+ self._link_staticfile(sc)
+ else:
+ raise err
+
+ def _mkdir(self, path):
+ if os.path.lexists(path) and not os.path.isdir(path):
+ os.unlink(path)
+ mkdir_p(path)
+
+
+class SourceFileGenerator(Generator):
+
+ def generate_context(self):
+ self.output_extension = self.settings['OUTPUT_SOURCES_EXTENSION']
+
+ def _create_source(self, obj):
+ output_path, _ = os.path.splitext(obj.save_as)
+ dest = os.path.join(self.output_path,
+ output_path + self.output_extension)
+ copy(obj.source_path, dest)
+
+ def generate_output(self, writer=None):
+ logger.info('Generating source files...')
+ for obj in chain(self.context['articles'], self.context['pages']):
+ self._create_source(obj)
+ for obj_trans in obj.translations:
+ self._create_source(obj_trans)
diff --git a/pelican/log.py b/pelican/log.py
new file mode 100644
index 00000000..325ac3ea
--- /dev/null
+++ b/pelican/log.py
@@ -0,0 +1,228 @@
+import logging
+import os
+import sys
+from collections import defaultdict
+from collections.abc import Mapping
+
+__all__ = [
+ 'init'
+]
+
+
+class BaseFormatter(logging.Formatter):
+ def __init__(self, fmt=None, datefmt=None):
+ FORMAT = '%(customlevelname)s %(message)s'
+ super().__init__(fmt=FORMAT, datefmt=datefmt)
+
+ def format(self, record):
+ customlevel = self._get_levelname(record.levelname)
+ record.__dict__['customlevelname'] = customlevel
+ # format multiline messages 'nicely' to make it clear they are together
+ record.msg = record.msg.replace('\n', '\n | ')
+ if not isinstance(record.args, Mapping):
+ record.args = tuple(arg.replace('\n', '\n | ') if
+ isinstance(arg, str) else
+ arg for arg in record.args)
+ return super().format(record)
+
+ def formatException(self, ei):
+ ''' prefix traceback info for better representation '''
+ s = super().formatException(ei)
+ # fancy format traceback
+ s = '\n'.join(' | ' + line for line in s.splitlines())
+ # separate the traceback from the preceding lines
+ s = ' |___\n{}'.format(s)
+ return s
+
+ def _get_levelname(self, name):
+ ''' NOOP: overridden by subclasses '''
+ return name
+
+
+class ANSIFormatter(BaseFormatter):
+ ANSI_CODES = {
+ 'red': '\033[1;31m',
+ 'yellow': '\033[1;33m',
+ 'cyan': '\033[1;36m',
+ 'white': '\033[1;37m',
+ 'bgred': '\033[1;41m',
+ 'bggrey': '\033[1;100m',
+ 'reset': '\033[0;m'}
+
+ LEVEL_COLORS = {
+ 'INFO': 'cyan',
+ 'WARNING': 'yellow',
+ 'ERROR': 'red',
+ 'CRITICAL': 'bgred',
+ 'DEBUG': 'bggrey'}
+
+ def _get_levelname(self, name):
+ color = self.ANSI_CODES[self.LEVEL_COLORS.get(name, 'white')]
+ if name == 'INFO':
+ fmt = '{0}->{2}'
+ else:
+ fmt = '{0}{1}{2}:'
+ return fmt.format(color, name, self.ANSI_CODES['reset'])
+
+
+class TextFormatter(BaseFormatter):
+ """
+ Convert a `logging.LogRecord' object into text.
+ """
+
+ def _get_levelname(self, name):
+ if name == 'INFO':
+ return '->'
+ else:
+ return name + ':'
+
+
+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, **kwargs):
+ super().warning(*args, **kwargs)
+ if FatalLogger.warnings_fatal:
+ raise RuntimeError('Warning encountered')
+
+ def error(self, *args, **kwargs):
+ super().error(*args, **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
+
+
+def supports_color():
+ """
+ Returns True if the running system's terminal supports color,
+ and False otherwise.
+
+ from django.core.management.color
+ """
+ plat = sys.platform
+ supported_platform = plat != 'Pocket PC' and \
+ (plat != 'win32' or 'ANSICON' in os.environ)
+
+ # isatty is not always implemented, #6223.
+ is_a_tty = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
+ if not supported_platform or not is_a_tty:
+ return False
+ return True
+
+
+def get_formatter():
+ if supports_color():
+ return ANSIFormatter()
+ else:
+ return TextFormatter()
+
+
+def init(level=None, fatal='', handler=logging.StreamHandler(), name=None,
+ logs_dedup_min_level=None):
+ FatalLogger.warnings_fatal = fatal.startswith('warning')
+ FatalLogger.errors_fatal = bool(fatal)
+
+ logger = logging.getLogger(name)
+
+ handler.setFormatter(get_formatter())
+ logger.addHandler(handler)
+
+ 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)
+
+ root_logger = logging.getLogger()
+ root_logger.debug('debug')
+ root_logger.info('info')
+ root_logger.warning('warning')
+ root_logger.error('error')
+ root_logger.critical('critical')
diff --git a/pelican/paginator.py b/pelican/paginator.py
new file mode 100644
index 00000000..9d169045
--- /dev/null
+++ b/pelican/paginator.py
@@ -0,0 +1,158 @@
+import functools
+import logging
+import os
+from collections import namedtuple
+from math import ceil
+
+logger = logging.getLogger(__name__)
+PaginationRule = namedtuple(
+ '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 ''.format(self.number, 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 <= 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[0] == '/':
+ ret = ret[1:]
+ return ret
+
+ url = property(functools.partial(_from_settings, key='URL'))
+ save_as = property(functools.partial(_from_settings, key='SAVE_AS'))
diff --git a/pelican/plugins/_utils.py b/pelican/plugins/_utils.py
new file mode 100644
index 00000000..78c19e54
--- /dev/null
+++ b/pelican/plugins/_utils.py
@@ -0,0 +1,101 @@
+import importlib
+import importlib.machinery
+import importlib.util
+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 load_legacy_plugin(plugin, 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('Cannot import plugin `{}`'.format(plugin))
+ else:
+ # 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 'pelican.plugins.{}'.format(plugin) in namespace_plugins:
+ plugin = namespace_plugins['pelican.plugins.{}'.format(
+ 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
diff --git a/pelican/plugins/signals.py b/pelican/plugins/signals.py
new file mode 100644
index 00000000..4013360f
--- /dev/null
+++ b/pelican/plugins/signals.py
@@ -0,0 +1,49 @@
+from blinker import signal
+
+# 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')
diff --git a/pelican/readers.py b/pelican/readers.py
new file mode 100644
index 00000000..15d09908
--- /dev/null
+++ b/pelican/readers.py
@@ -0,0 +1,747 @@
+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, Tag
+from pelican.plugins import signals
+from pelican.utils import get_date, pelican_open, posixize_path
+
+try:
+ from markdown import Markdown
+except ImportError:
+ Markdown = False # NOQA
+
+# 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
+
+
+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('')
+
+ 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)
+
+ for docinfo in document.traverse(docutils.nodes.docinfo):
+ 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
+
+
+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 += '{}>'.format(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 += ''.format(data)
+
+ def handle_data(self, data):
+ self._data_buffer += data
+
+ def handle_entityref(self, data):
+ self._data_buffer += '&{};'.format(data)
+
+ def handle_charref(self, data):
+ self._data_buffer += '{};'.format(data)
+
+ def build_tag(self, tag, attrs, close_tag):
+ result = '<{}'.format(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 += "='{}'".format(escape(v, quote=False))
+ else:
+ result += '="{}"'.format(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 = ['{}="{}"'.format(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.reader_classes = {}
+
+ for cls in [BaseReader] + BaseReader.__subclasses__():
+ if not cls.enabled:
+ logger.debug('Missing dependencies for %s',
+ ', '.join(cls.file_extensions))
+ continue
+
+ for ext in cls.file_extensions:
+ self.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)
+
+ # 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):
+ return self.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:
+ _, ext = os.path.splitext(os.path.basename(path))
+ fmt = ext[1:]
+
+ 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)
+ self.cache_data(path, (content, reader_metadata))
+ metadata.update(_filter_discardable_metadata(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']:
+ from typogrify.filters import typogrify
+ import smartypants
+
+ 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 " 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, )',
+ context_signal.name,
+ context_sender)
+ context_signal.send(context_sender, metadata=metadata)
+
+ return content_class(content=content, metadata=metadata,
+ settings=self.settings, source_path=path,
+ context=context)
+
+
+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
+ ]*
+ src=(['"])(.*?)\1
+ [^\>]*
+ alt=(['"])\3
+ )|(?:
+ # alt before src
+ ]*
+ 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[^.]*).*',
+ ... 'PATH_METADATA':
+ ... r'(?P[^/]*)/(?P\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': ,
+ '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.*)', 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
diff --git a/pelican/rstdirectives.py b/pelican/rstdirectives.py
new file mode 100644
index 00000000..500c8578
--- /dev/null
+++ b/pelican/rstdirectives.py
@@ -0,0 +1,90 @@
+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={}, content=[]):
+ 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)
diff --git a/pelican/server.py b/pelican/server.py
new file mode 100644
index 00000000..6ebce876
--- /dev/null
+++ b/pelican/server.py
@@ -0,0 +1,138 @@
+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 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', '/', '']
+
+ 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
+ original_path = original_path.rstrip('/')
+ # Try to detect file by applying various suffixes
+ tries = []
+ for suffix in self.SUFFIXES:
+ 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
+
+
+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()
diff --git a/pelican/settings.py b/pelican/settings.py
new file mode 100644
index 00000000..ea3ee8eb
--- /dev/null
+++ b/pelican/settings.py
@@ -0,0 +1,683 @@
+import copy
+import importlib.util
+import inspect
+import json
+import locale
+import logging
+import os
+import re
+from os.path import isabs
+
+from pelican.log import LimitFilter
+
+
+def load_source(name, path):
+ spec = importlib.util.spec_from_file_location(name, path)
+ mod = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(mod)
+ return mod
+
+
+logger = logging.getLogger(__name__)
+
+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': '',
+ 'RSS_FEED_SUMMARY_ONLY': True,
+ '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\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.*?)[|}]',
+ '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,
+ 'WRITE_SELECTED': [],
+ 'FORMATTED_FIELDS': ['summary'],
+ 'PORT': 8000,
+ 'BIND': '127.0.0.1',
+}
+
+PYGMENTS_RST_OPTIONS = None
+
+
+def read_settings(path=None, override=None):
+ 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
+ PYGMENTS_RST_OPTIONS = settings.get('PYGMENTS_RST_OPTIONS', None)
+ return settings
+
+
+def get_settings_from_module(module=None):
+ """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):
+ """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):
+ """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, format_field):
+ """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('}', '}}') \
+ % '{{{}}}'.format(format_field)
+ if result.format(**{format_field: TEST_STRING}) != expected:
+ raise ValueError('Failed to safely replace %s with {{{}}}'.format(
+ format_field))
+
+ return result
+
+
+def handle_deprecated_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 ('THEME_TEMPLATES_OVERRIDES' in settings and
+ settings['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/master/docs/settings.rst#path-metadata'),
+ ]:
+ if old in settings:
+ message = 'The {} setting has been removed in favor of {}'.format(
+ old, new)
+ if doc:
+ message += ', see {} for details'.format(doc)
+ 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 '%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 '%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]
+
+ return settings
+
+
+def configure_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 %s"
+ % settings['THEME'])
+
+ # make paths selected for writing absolute if necessary
+ settings['WRITE_SELECTED'] = [
+ os.path.abspath(path) for path in
+ settings.get('WRITE_SELECTED', DEFAULT_CONFIG['WRITE_SELECTED'])
+ ]
+
+ # 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 \
+ settings.get('WITH_FUTURE_DATES', False):
+ 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
+
+
+def coerce_overrides(overrides):
+ if overrides is None:
+ return {}
+ coerced = {}
+ types_to_cast = {int, str, bool}
+ for k, v in overrides.items():
+ if k not in DEFAULT_CONFIG:
+ logger.warning('Override for unknown setting %s, ignoring', k)
+ continue
+ setting_type = type(DEFAULT_CONFIG[k])
+ if setting_type not in types_to_cast:
+ coerced[k] = json.loads(v)
+ else:
+ try:
+ coerced[k] = setting_type(v)
+ except ValueError:
+ logger.debug('ValueError for %s override with %s, try to '
+ 'load as json', k, v)
+ coerced[k] = json.loads(v)
+ return coerced
diff --git a/pelican/tests/TestPages/bad_page.rst b/pelican/tests/TestPages/bad_page.rst
new file mode 100644
index 00000000..bc62948b
--- /dev/null
+++ b/pelican/tests/TestPages/bad_page.rst
@@ -0,0 +1,8 @@
+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.
diff --git a/pelican/tests/TestPages/draft_page.rst b/pelican/tests/TestPages/draft_page.rst
new file mode 100644
index 00000000..19ca0646
--- /dev/null
+++ b/pelican/tests/TestPages/draft_page.rst
@@ -0,0 +1,8 @@
+This is a test draft page
+##########################
+
+:status: draft
+
+The quick brown fox .
+
+This page is a draft.
diff --git a/pelican/tests/TestPages/draft_page_markdown.md b/pelican/tests/TestPages/draft_page_markdown.md
new file mode 100644
index 00000000..fda71868
--- /dev/null
+++ b/pelican/tests/TestPages/draft_page_markdown.md
@@ -0,0 +1,12 @@
+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
\ No newline at end of file
diff --git a/pelican/tests/TestPages/draft_page_with_template.rst b/pelican/tests/TestPages/draft_page_with_template.rst
new file mode 100644
index 00000000..9d46e717
--- /dev/null
+++ b/pelican/tests/TestPages/draft_page_with_template.rst
@@ -0,0 +1,11 @@
+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
diff --git a/pelican/tests/TestPages/hidden_page.rst b/pelican/tests/TestPages/hidden_page.rst
new file mode 100644
index 00000000..57ca329c
--- /dev/null
+++ b/pelican/tests/TestPages/hidden_page.rst
@@ -0,0 +1,8 @@
+This is a test hidden page
+##########################
+
+:status: hidden
+
+The quick brown fox jumped over the lazy dog's back.
+
+This page is hidden
diff --git a/pelican/tests/TestPages/hidden_page_markdown.md b/pelican/tests/TestPages/hidden_page_markdown.md
new file mode 100644
index 00000000..1e532fe7
--- /dev/null
+++ b/pelican/tests/TestPages/hidden_page_markdown.md
@@ -0,0 +1,12 @@
+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
diff --git a/pelican/tests/TestPages/hidden_page_with_template.rst b/pelican/tests/TestPages/hidden_page_with_template.rst
new file mode 100644
index 00000000..36104a09
--- /dev/null
+++ b/pelican/tests/TestPages/hidden_page_with_template.rst
@@ -0,0 +1,11 @@
+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
diff --git a/pelican/tests/TestPages/page.rst b/pelican/tests/TestPages/page.rst
new file mode 100644
index 00000000..2d13976d
--- /dev/null
+++ b/pelican/tests/TestPages/page.rst
@@ -0,0 +1,4 @@
+This is a test page
+###################
+
+The quick brown fox jumped over the lazy dog's back.
diff --git a/pelican/tests/TestPages/page_markdown.md b/pelican/tests/TestPages/page_markdown.md
new file mode 100644
index 00000000..d5416a6f
--- /dev/null
+++ b/pelican/tests/TestPages/page_markdown.md
@@ -0,0 +1,9 @@
+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.
diff --git a/pelican/tests/TestPages/page_used_for_sorting_test.rst b/pelican/tests/TestPages/page_used_for_sorting_test.rst
new file mode 100644
index 00000000..40cdc7ea
--- /dev/null
+++ b/pelican/tests/TestPages/page_used_for_sorting_test.rst
@@ -0,0 +1,6 @@
+A Page (Test) for sorting
+#########################
+
+:slug: zzzz
+
+When using title, should be first. When using slug, should be last.
diff --git a/pelican/tests/TestPages/page_with_category_and_tag_links.md b/pelican/tests/TestPages/page_with_category_and_tag_links.md
new file mode 100644
index 00000000..6806a570
--- /dev/null
+++ b/pelican/tests/TestPages/page_with_category_and_tag_links.md
@@ -0,0 +1,7 @@
+Title: Page with a bunch of links
+
+My links:
+
+[Link 1]({tag}マック)
+
+[Link 2]({category}Yeah)
diff --git a/pelican/tests/TestPages/page_with_static_links.md b/pelican/tests/TestPages/page_with_static_links.md
new file mode 100644
index 00000000..1967cfcf
--- /dev/null
+++ b/pelican/tests/TestPages/page_with_static_links.md
@@ -0,0 +1,7 @@
+Title: Page with static links
+
+My links:
+
+[Link 0]({static}image0.jpg)
+
+[Link 1]({attach}image1.jpg)
diff --git a/pelican/tests/TestPages/page_with_template.rst b/pelican/tests/TestPages/page_with_template.rst
new file mode 100644
index 00000000..9388dc2f
--- /dev/null
+++ b/pelican/tests/TestPages/page_with_template.rst
@@ -0,0 +1,8 @@
+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
diff --git a/pelican/tests/__init__.py b/pelican/tests/__init__.py
new file mode 100644
index 00000000..4605a02b
--- /dev/null
+++ b/pelican/tests/__init__.py
@@ -0,0 +1,15 @@
+import logging
+import warnings
+
+from pelican.log import log_warnings
+
+# redirect warnings modulole 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())
diff --git a/pelican/tests/content/2012-11-29_rst_w_filename_meta#foo-bar.rst b/pelican/tests/content/2012-11-29_rst_w_filename_meta#foo-bar.rst
new file mode 100644
index 00000000..43f05a15
--- /dev/null
+++ b/pelican/tests/content/2012-11-29_rst_w_filename_meta#foo-bar.rst
@@ -0,0 +1,6 @@
+
+Rst with filename metadata
+##########################
+
+:category: yeah
+:author: Alexis Métaireau
diff --git a/pelican/tests/content/2012-11-30_md_w_filename_meta#foo-bar.md b/pelican/tests/content/2012-11-30_md_w_filename_meta#foo-bar.md
new file mode 100644
index 00000000..cdccfc8a
--- /dev/null
+++ b/pelican/tests/content/2012-11-30_md_w_filename_meta#foo-bar.md
@@ -0,0 +1,6 @@
+category: yeah
+author: Alexis Métaireau
+
+Markdown with filename metadata
+===============================
+
diff --git a/pelican/tests/content/TestCategory/article_with_category.rst b/pelican/tests/content/TestCategory/article_with_category.rst
new file mode 100644
index 00000000..69ffa98b
--- /dev/null
+++ b/pelican/tests/content/TestCategory/article_with_category.rst
@@ -0,0 +1,7 @@
+This is an article with category !
+##################################
+
+:category: yeah
+:date: 1970-01-01
+
+This article should be in 'yeah' category.
diff --git a/pelican/tests/content/TestCategory/article_without_category.rst b/pelican/tests/content/TestCategory/article_without_category.rst
new file mode 100644
index 00000000..4bc5d78d
--- /dev/null
+++ b/pelican/tests/content/TestCategory/article_without_category.rst
@@ -0,0 +1,4 @@
+This is an article without category !
+#####################################
+
+This article should be in 'TestCategory' category.
diff --git a/pelican/tests/content/article.rst b/pelican/tests/content/article.rst
new file mode 100644
index 00000000..793e6869
--- /dev/null
+++ b/pelican/tests/content/article.rst
@@ -0,0 +1,6 @@
+Article title
+#############
+
+THIS is some content. With some stuff to "typogrify"...
+
+Now with added support for :abbr:`TLA (three letter acronym)`.
diff --git a/pelican/tests/content/article_with_attributes_containing_double_quotes.html b/pelican/tests/content/article_with_attributes_containing_double_quotes.html
new file mode 100644
index 00000000..7daa5801
--- /dev/null
+++ b/pelican/tests/content/article_with_attributes_containing_double_quotes.html
@@ -0,0 +1,11 @@
+
+
+
+
+ Ensure that if an attribute value contains a double quote, it is
+ surrounded with single quotes, otherwise with double quotes.
+ Span content
+ Span content
+ Span content
+
+
diff --git a/pelican/tests/content/article_with_capitalized_metadata.rst b/pelican/tests/content/article_with_capitalized_metadata.rst
new file mode 100644
index 00000000..93ed5b15
--- /dev/null
+++ b/pelican/tests/content/article_with_capitalized_metadata.rst
@@ -0,0 +1,16 @@
+
+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"...
diff --git a/pelican/tests/content/article_with_code_block.rst b/pelican/tests/content/article_with_code_block.rst
new file mode 100644
index 00000000..586878cf
--- /dev/null
+++ b/pelican/tests/content/article_with_code_block.rst
@@ -0,0 +1,15 @@
+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
diff --git a/pelican/tests/content/article_with_comments.html b/pelican/tests/content/article_with_comments.html
new file mode 100644
index 00000000..289e4a66
--- /dev/null
+++ b/pelican/tests/content/article_with_comments.html
@@ -0,0 +1,8 @@
+
+
+
+
+ Body content
+
+
+
diff --git a/pelican/tests/content/article_with_duplicate_tags_authors.md b/pelican/tests/content/article_with_duplicate_tags_authors.md
new file mode 100644
index 00000000..7ab046f9
--- /dev/null
+++ b/pelican/tests/content/article_with_duplicate_tags_authors.md
@@ -0,0 +1,15 @@
+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.
diff --git a/pelican/tests/content/article_with_inline_svg.html b/pelican/tests/content/article_with_inline_svg.html
new file mode 100644
index 00000000..07f97a8a
--- /dev/null
+++ b/pelican/tests/content/article_with_inline_svg.html
@@ -0,0 +1,17 @@
+
+
+ Article with an inline SVG
+
+
+ Ensure that the title attribute in an inline svg is not handled as an HTML title.
+
+
+ A different title inside the inline SVG
+
+
+
+
+
+
+
+
diff --git a/pelican/tests/content/article_with_keywords.html b/pelican/tests/content/article_with_keywords.html
new file mode 100644
index 00000000..0744c754
--- /dev/null
+++ b/pelican/tests/content/article_with_keywords.html
@@ -0,0 +1,6 @@
+
+
+ This is a super article !
+
+
+
diff --git a/pelican/tests/content/article_with_markdown_and_footnote.md b/pelican/tests/content/article_with_markdown_and_footnote.md
new file mode 100644
index 00000000..6fea2d6e
--- /dev/null
+++ b/pelican/tests/content/article_with_markdown_and_footnote.md
@@ -0,0 +1,15 @@
+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
diff --git a/pelican/tests/content/article_with_markdown_and_nested_metadata.md b/pelican/tests/content/article_with_markdown_and_nested_metadata.md
new file mode 100644
index 00000000..3968027b
--- /dev/null
+++ b/pelican/tests/content/article_with_markdown_and_nested_metadata.md
@@ -0,0 +1,5 @@
+Title: Article with markdown and nested summary metadata
+Date: 2012-10-30
+Summary: Test: This metadata value looks like metadata
+
+This is some content.
diff --git a/pelican/tests/content/article_with_markdown_and_nonascii_summary.md b/pelican/tests/content/article_with_markdown_and_nonascii_summary.md
new file mode 100644
index 00000000..d76ed4a1
--- /dev/null
+++ b/pelican/tests/content/article_with_markdown_and_nonascii_summary.md
@@ -0,0 +1,19 @@
+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.
diff --git a/pelican/tests/content/article_with_markdown_and_summary_metadata_multi.md b/pelican/tests/content/article_with_markdown_and_summary_metadata_multi.md
new file mode 100644
index 00000000..c5f62a0a
--- /dev/null
+++ b/pelican/tests/content/article_with_markdown_and_summary_metadata_multi.md
@@ -0,0 +1,10 @@
+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.
diff --git a/pelican/tests/content/article_with_markdown_and_summary_metadata_single.md b/pelican/tests/content/article_with_markdown_and_summary_metadata_single.md
new file mode 100644
index 00000000..a7d6f09b
--- /dev/null
+++ b/pelican/tests/content/article_with_markdown_and_summary_metadata_single.md
@@ -0,0 +1,5 @@
+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.
diff --git a/pelican/tests/content/article_with_markdown_extension.markdown b/pelican/tests/content/article_with_markdown_extension.markdown
new file mode 100644
index 00000000..94e92871
--- /dev/null
+++ b/pelican/tests/content/article_with_markdown_extension.markdown
@@ -0,0 +1,10 @@
+title: Test markdown File
+category: test
+
+Test Markdown File Header
+=========================
+
+Used for pelican test
+---------------------
+
+This is another markdown test file. Uses the markdown extension.
diff --git a/pelican/tests/content/article_with_markdown_markup_extensions.md b/pelican/tests/content/article_with_markdown_markup_extensions.md
new file mode 100644
index 00000000..6cf56403
--- /dev/null
+++ b/pelican/tests/content/article_with_markdown_markup_extensions.md
@@ -0,0 +1,8 @@
+Title: Test Markdown extensions
+
+[TOC]
+
+## Level1
+
+### Level2
+
diff --git a/pelican/tests/content/article_with_md_extension.md b/pelican/tests/content/article_with_md_extension.md
new file mode 100644
index 00000000..89b6980c
--- /dev/null
+++ b/pelican/tests/content/article_with_md_extension.md
@@ -0,0 +1,14 @@
+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.
diff --git a/pelican/tests/content/article_with_mdown_extension.mdown b/pelican/tests/content/article_with_mdown_extension.mdown
new file mode 100644
index 00000000..bdaf74cd
--- /dev/null
+++ b/pelican/tests/content/article_with_mdown_extension.mdown
@@ -0,0 +1,10 @@
+title: Test mdown File
+category: test
+
+Test Markdown File Header
+=========================
+
+Used for pelican test
+---------------------
+
+This is another markdown test file. Uses the mdown extension.
diff --git a/pelican/tests/content/article_with_metadata.html b/pelican/tests/content/article_with_metadata.html
new file mode 100644
index 00000000..b501ea29
--- /dev/null
+++ b/pelican/tests/content/article_with_metadata.html
@@ -0,0 +1,15 @@
+
+
+ This is a super article !
+
+
+
+
+
+
+
+
+ Multi-line metadata should be supported
+ as well as inline markup .
+
+
diff --git a/pelican/tests/content/article_with_metadata.rst b/pelican/tests/content/article_with_metadata.rst
new file mode 100644
index 00000000..5f3d77bc
--- /dev/null
+++ b/pelican/tests/content/article_with_metadata.rst
@@ -0,0 +1,16 @@
+
+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"...
diff --git a/pelican/tests/content/article_with_metadata.unknownextension b/pelican/tests/content/article_with_metadata.unknownextension
new file mode 100644
index 00000000..d4bac1c0
--- /dev/null
+++ b/pelican/tests/content/article_with_metadata.unknownextension
@@ -0,0 +1,12 @@
+
+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
diff --git a/pelican/tests/content/article_with_metadata_and_contents.html b/pelican/tests/content/article_with_metadata_and_contents.html
new file mode 100644
index 00000000..b108ac8a
--- /dev/null
+++ b/pelican/tests/content/article_with_metadata_and_contents.html
@@ -0,0 +1,15 @@
+
+
+ This is a super article !
+
+
+
+
+
+
+
+
+ Multi-line metadata should be supported
+ as well as inline markup .
+
+
diff --git a/pelican/tests/content/article_with_metadata_explicit_date_implicit_modified.html b/pelican/tests/content/article_with_metadata_explicit_date_implicit_modified.html
new file mode 100644
index 00000000..b501ea29
--- /dev/null
+++ b/pelican/tests/content/article_with_metadata_explicit_date_implicit_modified.html
@@ -0,0 +1,15 @@
+
+
+ This is a super article !
+
+
+
+
+
+
+
+
+ Multi-line metadata should be supported
+ as well as inline markup .
+
+
diff --git a/pelican/tests/content/article_with_metadata_explicit_dates.html b/pelican/tests/content/article_with_metadata_explicit_dates.html
new file mode 100644
index 00000000..1c74f40e
--- /dev/null
+++ b/pelican/tests/content/article_with_metadata_explicit_dates.html
@@ -0,0 +1,16 @@
+
+
+ This is a super article !
+
+
+
+
+
+
+
+
+
+ Multi-line metadata should be supported
+ as well as inline markup .
+
+
diff --git a/pelican/tests/content/article_with_metadata_implicit_date_explicit_modified.html b/pelican/tests/content/article_with_metadata_implicit_date_explicit_modified.html
new file mode 100644
index 00000000..8cd2885b
--- /dev/null
+++ b/pelican/tests/content/article_with_metadata_implicit_date_explicit_modified.html
@@ -0,0 +1,15 @@
+
+
+ This is a super article !
+
+
+
+
+
+
+
+
+ Multi-line metadata should be supported
+ as well as inline markup .
+
+
diff --git a/pelican/tests/content/article_with_metadata_implicit_dates.html b/pelican/tests/content/article_with_metadata_implicit_dates.html
new file mode 100644
index 00000000..14d7e1e0
--- /dev/null
+++ b/pelican/tests/content/article_with_metadata_implicit_dates.html
@@ -0,0 +1,14 @@
+
+
+ This is a super article !
+
+
+
+
+
+
+
+ Multi-line metadata should be supported
+ as well as inline markup .
+
+
diff --git a/pelican/tests/content/article_with_mkd_extension.mkd b/pelican/tests/content/article_with_mkd_extension.mkd
new file mode 100644
index 00000000..c946cb87
--- /dev/null
+++ b/pelican/tests/content/article_with_mkd_extension.mkd
@@ -0,0 +1,10 @@
+title: Test mkd File
+category: test
+
+Test Markdown File Header
+=========================
+
+Used for pelican test
+---------------------
+
+This is another markdown test file. Uses the mkd extension.
diff --git a/pelican/tests/content/article_with_multiple_authors.html b/pelican/tests/content/article_with_multiple_authors.html
new file mode 100644
index 00000000..a74442c9
--- /dev/null
+++ b/pelican/tests/content/article_with_multiple_authors.html
@@ -0,0 +1,6 @@
+
+
+ This is an article with multiple authors!
+
+
+
diff --git a/pelican/tests/content/article_with_multiple_authors.rst b/pelican/tests/content/article_with_multiple_authors.rst
new file mode 100644
index 00000000..04af017c
--- /dev/null
+++ b/pelican/tests/content/article_with_multiple_authors.rst
@@ -0,0 +1,6 @@
+This is an article with multiple authors!
+#########################################
+
+:date: 2014-02-09 02:20
+:modified: 2014-02-09 02:20
+:authors: First Author, Second Author
diff --git a/pelican/tests/content/article_with_multiple_authors_list.rst b/pelican/tests/content/article_with_multiple_authors_list.rst
new file mode 100644
index 00000000..7da5fae2
--- /dev/null
+++ b/pelican/tests/content/article_with_multiple_authors_list.rst
@@ -0,0 +1,10 @@
+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.
diff --git a/pelican/tests/content/article_with_multiple_authors_semicolon.rst b/pelican/tests/content/article_with_multiple_authors_semicolon.rst
new file mode 100644
index 00000000..fa76ac4e
--- /dev/null
+++ b/pelican/tests/content/article_with_multiple_authors_semicolon.rst
@@ -0,0 +1,6 @@
+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
diff --git a/pelican/tests/content/article_with_multiple_metadata_tags.html b/pelican/tests/content/article_with_multiple_metadata_tags.html
new file mode 100644
index 00000000..b52fbcf7
--- /dev/null
+++ b/pelican/tests/content/article_with_multiple_metadata_tags.html
@@ -0,0 +1,11 @@
+
+
+ Metadata tags as list!
+
+
+
+
+ When custom metadata tags are specified more than once
+ they are collected into a list!
+
+
diff --git a/pelican/tests/content/article_with_nonconformant_meta_tags.html b/pelican/tests/content/article_with_nonconformant_meta_tags.html
new file mode 100644
index 00000000..5ed44bbd
--- /dev/null
+++ b/pelican/tests/content/article_with_nonconformant_meta_tags.html
@@ -0,0 +1,12 @@
+
+
+
+
+ Article with Nonconformant HTML meta tags
+
+
+
+ Multi-line metadata should be supported
+ as well as inline markup .
+
+
diff --git a/pelican/tests/content/article_with_null_attributes.html b/pelican/tests/content/article_with_null_attributes.html
new file mode 100644
index 00000000..68da704c
--- /dev/null
+++ b/pelican/tests/content/article_with_null_attributes.html
@@ -0,0 +1,8 @@
+
+
+
+
+ Ensure that empty attributes are copied properly.
+
+
+
diff --git a/pelican/tests/content/article_with_template.rst b/pelican/tests/content/article_with_template.rst
new file mode 100644
index 00000000..eb55738c
--- /dev/null
+++ b/pelican/tests/content/article_with_template.rst
@@ -0,0 +1,8 @@
+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".
diff --git a/pelican/tests/content/article_with_typogrify_dashes.md b/pelican/tests/content/article_with_typogrify_dashes.md
new file mode 100644
index 00000000..2914b2df
--- /dev/null
+++ b/pelican/tests/content/article_with_typogrify_dashes.md
@@ -0,0 +1,3 @@
+Title: One -, two --, three --- dashes!
+
+One: -; Two: --; Three: ---
diff --git a/pelican/tests/content/article_with_typogrify_dashes.rst b/pelican/tests/content/article_with_typogrify_dashes.rst
new file mode 100644
index 00000000..90064c5d
--- /dev/null
+++ b/pelican/tests/content/article_with_typogrify_dashes.rst
@@ -0,0 +1,4 @@
+One -, two --, three --- dashes!
+################################
+
+One: -; Two: --; Three: ---
diff --git a/pelican/tests/content/article_with_uppercase_metadata.html b/pelican/tests/content/article_with_uppercase_metadata.html
new file mode 100644
index 00000000..b4cedf39
--- /dev/null
+++ b/pelican/tests/content/article_with_uppercase_metadata.html
@@ -0,0 +1,6 @@
+
+
+ This is a super article !
+
+
+
diff --git a/pelican/tests/content/article_with_uppercase_metadata.rst b/pelican/tests/content/article_with_uppercase_metadata.rst
new file mode 100644
index 00000000..e26cdd13
--- /dev/null
+++ b/pelican/tests/content/article_with_uppercase_metadata.rst
@@ -0,0 +1,6 @@
+
+This is a super article !
+#########################
+
+:Category: Yeah
+
diff --git a/pelican/tests/content/article_without_category.rst b/pelican/tests/content/article_without_category.rst
new file mode 100644
index 00000000..ff47f6ef
--- /dev/null
+++ b/pelican/tests/content/article_without_category.rst
@@ -0,0 +1,6 @@
+
+This is an article without category !
+#####################################
+
+This article should be in the DEFAULT_CATEGORY.
+
diff --git a/pelican/tests/content/bad_extension.mmd b/pelican/tests/content/bad_extension.mmd
new file mode 100644
index 00000000..d282a749
--- /dev/null
+++ b/pelican/tests/content/bad_extension.mmd
@@ -0,0 +1,3 @@
+Title: Bad Extension
+
+This file shouldn't be included because its file extension is `.mmd`.
diff --git a/pelican/tests/content/bloggerexport.xml b/pelican/tests/content/bloggerexport.xml
new file mode 100644
index 00000000..4bc0985a
--- /dev/null
+++ b/pelican/tests/content/bloggerexport.xml
@@ -0,0 +1,1067 @@
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.archive
+
+ 2018-08-02T12:38:27.320-07:00
+
+ Notes of a Young Doctor
+
+
+
+
+
+ Mikhail Afanasyevich Bulgakov
+
+ https://www.blogger.com/profile/000082957
+
+ noreply@blogger.com
+
+
+
+ Blogger
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.layout
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Template: Notes of a Young Doctor
+ [Over 2000 lines of mostly css that we don't need here.]
+
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_PUBLISHING_MODE
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Het type publicatie voor deze blog.
+ PUBLISH_MODE_BLOGSPOT
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_ADMIN_PERMISSION
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ De lijst van e-mails van beheerders voor de blog.
+ mikhail.afanasyevich.bulgakov@gmail.com
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_ADULT_CONTENT
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Hier wordt aangegeven of deze blog content voor volwassenen bevat
+ false
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_ALTERNATE_JSRENDER_ALLOWED
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Hier wordt aangegeven of alternatieve weergaven in JavaScript zijn toegestaan
+ true
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_ANALYTICS_ACCOUNT_NUMBER
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Google Analytics-accountnummer voor een blog
+
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_ARCHIVE_DATE_FORMAT
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Het getal van de datumnotatie voor de archiefindex
+ 9
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_ARCHIVE_FREQUENCY
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Hoe vaak deze blog moet worden gearchiveerd
+ MONTHLY
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_AUTHOR_PERMISSION
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ De lijst van e-mails van auteurs die toestemming hebben om te publiceren.
+
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_BACKLINKS_ALLOWED
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Hier wordt aangegeven of reactiebacklinks op de blog moeten worden getoond
+ false
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_BY_POST_ARCHIVING
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Hier wordt aangegeven of iedere post moet worden voorzien van een archiefpagina
+ true
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_COMMENT_ACCESS
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Wie kan reacties achterlaten
+ BLOGGERS
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_COMMENT_CAPTCHA
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Hier wordt aangegeven of personen die reacties geven, een Captcha (woordverificatie) moeten invullen
+ true
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_COMMENT_EMAIL
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Lijst met e-mailadressen om meldingen van nieuwe reacties naar te sturen
+ mikhail.afanasyevich.bulgakov@gmail.com
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_COMMENT_FEED
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Het type feed dat voor blogreacties moet worden gegeven
+ FULL
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_COMMENT_FORM_LOCATION
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Locatie van formulier voor blogreacties
+ EMBEDDED_IFRAME
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_COMMENT_MESSAGE
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Bericht bij blogreactie
+
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_COMMENT_MODERATION
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Hier wordt aangegeven of het modereren van reacties moet worden ingeschakeld
+ DISABLED
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_COMMENT_MODERATION_DELAY
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Aantal dagen waarna nieuwe reacties in aanmerking komen voor moderaten
+ 14
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_COMMENT_MODERATION_EMAIL
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ E-mailadres waar meldingen binnenkomen over welke nieuwe reacties bewerkt of verwijderd moeten worden
+
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_COMMENT_PROFILE_IMAGES
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Hier wordt aangegeven of profielafbeeldingen in reacties moeten worden getoond
+ true
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_COMMENTS_ALLOWED
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Hier wordt aangegeven of er reacties moeten worden weergegeven
+ true
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_COMMENTS_TIME_STAMP_FORMAT
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Getal van de tijdstempelnotatie voor reacties
+ 29
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_CONVERT_LINE_BREAKS
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Hier wordt aangegeven of de regelscheidingen moeten worden omgezet in <br />-tags in de posteditor
+ true
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_CUSTOM_ADS_TXT
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ De aangepaste ads.txt-content van de blog die aan advertentiezoekmachines wordt getoond.
+
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_CUSTOM_ADS_TXT_ENABLED
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Bepaalt of deze blog aangepaste ads.txt-content aan advertentiezoekmachines toont.
+ false
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_CUSTOM_PAGE_NOT_FOUND
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ De content die wordt weergegeven wanneer een post of pagina niet is gevonden.
+
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_CUSTOM_ROBOTS_TXT
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ De aangepaste robots.txt-content van de blog wordt aan zoekmachines getoond.
+
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_CUSTOM_ROBOTS_TXT_ENABLED
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Bepaalt of deze blog aangepaste robots.txt-content aan zoekmachines toont.
+ false
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_DATE_FORMAT
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Het getal van de datumnotatie voor koppen
+ 26
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_DEFAULT_BACKLINKS_MODE
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Standaardbacklinks voor posts
+ DEFAULT_HAVE_BACKLINKS
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_DEFAULT_COMMENTS_MODE
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Standaardreactie voor posts
+ DEFAULT_HAVE_COMMENTS
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_DESCRIPTION
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Een beschrijving van de blog
+
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_EMAIL_POST_LINKS
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Hier wordt aangegeven of er een link moet worden weergegeven waarmee gebruikers posts kunnen e-mailen
+ true
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_FEED_REDIRECT_URL
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ URL waar verzoeken om postfeed naartoe worden geleid
+
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_FLOAT_ALIGNMENT
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Hier wordt aangegeven of zwevende uitlijning is ingeschakeld voor de blog
+ true
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_LOCALE
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Taal voor deze blog
+ nl
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_M2B_WHITELIST_EMAIL
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Lijst met e-mailadressen die via e-mail posts op de blog kunnen plaatsen.
+
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_MAX_NUM
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Maximaal aantal items voor weergave op de hoofdpagina"
+ 100
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_MAX_UNIT
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Eenheid van items voor weergave op de hoofdpagina
+ POSTS
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_META_DESCRIPTION
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ De metabeschrijving van de blog die wordt gebruikt door zoekmachines.
+
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_META_DESCRIPTION_ENABLED
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Of deze blog wordt weergegeven met metabeschrijvingen.
+ false
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_NAME
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ De naam van de blog
+ Notes of a Young Doctor
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_PER_POST_FEED
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Het type feed dat voor reacties op afzonderlijke posts moet worden gegeven
+ FULL
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_POST_FEED
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Het type feed dat voor blogposts moet worden gegeven
+ FULL
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_POST_FEED_FOOTER
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Voettekst om aan het einde van iedere vermelding in de postfeed toe te voegen
+
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_POST_TEMPLATE
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ De template voor blogposts
+
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_PROMOTED
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Hier wordt aangegeven of deze blog op Blogger kan worden aangeprezen
+ true
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_QUICK_EDITING
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Hier wordt aangegeven of Snel bewerken is ingeschakeld
+ true
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_READ_ACCESS_MODE
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Het type toegang voor de lezers van de blog.
+ PUBLIC
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_READER_PERMISSION
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ De e-maillijst voor gebruikers die toestemming hebben om de blog te lezen.
+
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_SEARCHABLE
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Hier wordt aangegeven of deze blog door zoekmachines moet worden geïndexeerd
+ true
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_SEND_EMAIL
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Door komma's gescheiden lijst met e-mailadressen om nieuwe blogposts naar te sturen
+
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_SHOW_TITLE
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Hier wordt aangegeven of het titelveld moet worden weergegeven
+ true
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_SHOW_URL
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Hier wordt aangegeven of er een verwante link in de postopsteller moet worden weergegeven
+ false
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_SUBDOMAIN
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Het subdomein van BlogSpot om je blog op te publiceren
+ youngdoctornotes
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_TIME_STAMP_FORMAT
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Het getal van de tijdstempelnotatie
+ 27
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_TIME_ZONE
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ De tijdzone voor deze blog
+ America/Los_Angeles
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.settings.BLOG_USE_LIGHTBOX
+ 2010-11-27T07:08:20.877-08:00
+ 2018-08-02T12:38:27.320-07:00
+
+ Of afbeeldingen worden weergegeven in de lightbox wanneer erop wordt geklikt
+ true
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.post-1276418104709695660
+ 2010-11-27T08:21:00.000-08:00
+ 2018-08-02T12:22:48.286-07:00
+
+ yes
+
+
+
+
+ Black as Egypt's Night
+ Write next story here
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+ 0
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.post-1858599377741856733
+ 2010-11-27T07:12:00.000-08:00
+ 2010-11-27T07:56:43.964-08:00
+
+ The Steel Windpipe
+ It was a cold Winter's night.<br /><br /><ul><li>Very cold indeed.</li><br /><li>Note to self: pad out ending</li></ul>
+
+
+
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+ 1
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.page-4386962582497458967
+ 2018-08-02T12:38:00.001-07:00
+ 2018-08-02T12:38:27.171-07:00
+
+ yes
+
+
+ Test page 2
+ <div dir="ltr" style="text-align: left;" trbidi="on">This is a second test</div>
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.page-1406163839769953231
+ 2018-08-02T12:37:00.004-07:00
+ 2018-08-02T12:37:47.424-07:00
+
+ Test page
+ <div dir="ltr" style="text-align: left;" trbidi="on">This is a test.</div>
+
+
+
+
+ Mikhail Afanasyevich Bulgakov
+ https://www.blogger.com/profile/000082957
+ noreply@blogger.com
+
+
+
+
+ tag:blogger.com,1999:blog-6303278419262689239.post-5590533389087749201
+ 2010-11-29T12:35:44.027-08:00
+ 2010-11-29T12:35:44.027-08:00
+
+ Mishka, always a pleasure to read your adventures!...
+ Mishka, always a pleasure to read your adventures!<br /><br />It's a shame you don't get more time for writing.
+
+
+
+
+ Thomas Isidore Noël Sankara
+ https://www.blogger.com/profile/0617349827
+ noreply@blogger.com
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/content/empty.md b/pelican/tests/content/empty.md
new file mode 100644
index 00000000..e69de29b
diff --git a/pelican/tests/content/empty_with_bom.md b/pelican/tests/content/empty_with_bom.md
new file mode 100644
index 00000000..5f282702
--- /dev/null
+++ b/pelican/tests/content/empty_with_bom.md
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/pelican/tests/content/wordpress_content_decoded b/pelican/tests/content/wordpress_content_decoded
new file mode 100644
index 00000000..6e91338c
--- /dev/null
+++ b/pelican/tests/content/wordpress_content_decoded
@@ -0,0 +1,48 @@
+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.
+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.
+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.
+
+
+
+ a = [1, 2, 3]
+ b = [4, 5, 6]
+ for i in zip(a, b):
+ print i
+
+
+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.
diff --git a/pelican/tests/content/wordpress_content_encoded b/pelican/tests/content/wordpress_content_encoded
new file mode 100644
index 00000000..da35de3b
--- /dev/null
+++ b/pelican/tests/content/wordpress_content_encoded
@@ -0,0 +1,55 @@
+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.
+
+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.
+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.
+
+
+
+
+ a = [1, 2, 3]
+ b = [4, 5, 6]
+ for i in zip(a, b):
+ print i
+
+
+
+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.
+
diff --git a/pelican/tests/content/wordpressexport.xml b/pelican/tests/content/wordpressexport.xml
new file mode 100644
index 00000000..9b194e8f
--- /dev/null
+++ b/pelican/tests/content/wordpressexport.xml
@@ -0,0 +1,957 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Pelican test channel
+ http://thisisa.test
+ Not a real feed, just for test
+ Sun, 13 May 2012 01:13:52 +0000
+ en
+ 1.1
+ http://thisisa.test
+ http://thisisa.test
+
+ 2 Bob bob@thisisa.test
+ 3 Jonh jonh@thisisa.test
+
+ 7 categ-1
+ 11 categ-2
+ 1 uncategorized
+ 15 categ-3
+ 25 tag-1
+ 122 tag2
+ 68 tag-3
+
+ http://wordpress.org/?v=3.3.1
+
+ -
+
Empty post
+ http://thisisa.test/?attachment_id=24
+ Sat, 04 Feb 2012 03:17:33 +0000
+ bob
+ https://upload.wikimedia.org/wikipedia/commons/thumb/2/2c/Pelican_lakes_entrance02.jpg/240px-Pelican_lakes_entrance02.jpg
+
+
+
+ 24
+ 2012-02-04 03:17:33
+ 2012-02-04 03:17:33
+ open
+ open
+ empty-post
+ inherit
+ 0
+ 0
+ attachment
+
+ 0
+ https://upload.wikimedia.org/wikipedia/commons/thumb/2/2c/Pelican_lakes_entrance02.jpg/240px-Pelican_lakes_entrance02.jpg
+
+ _wp_attachment_metadata
+
+
+
+ _wp_attached_file
+
+
+
+ _wp_attachment_image_alt
+
+
+
+ -
+
+ http://thisisa.test/?p=168
+ Thu, 01 Jan 1970 00:00:00 +0000
+ bob
+ http://thisisa.test/?p=168
+
+
+
+ 168
+ 2012-02-15 21:23:57
+ 0000-00-00 00:00:00
+ open
+ open
+
+ draft
+ 0
+ 0
+ post
+
+ 0
+
+
+ _edit_last
+
+
+
+ -
+
A normal post
+ http://thisisa.test/?p=174
+ Thu, 01 Jan 1970 00:00:00 +0000
+ bob
+ http://thisisa.test/?p=174
+
+
+ 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.
+
+
+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.]]>
+
+ 174
+ 2012-02-16 15:52:55
+ 0000-00-00 00:00:00
+ open
+ open
+
+ draft
+ 0
+ 0
+ post
+
+ 0
+
+
+ _edit_last
+
+
+
+ -
+
Complete draft
+ http://thisisa.test/?p=176
+ Thu, 01 Jan 1970 00:00:00 +0000
+ bob
+ http://thisisa.test/?p=176
+
+
+
+ 176
+ 2012-02-17 15:11:55
+ 0000-00-00 00:00:00
+ open
+ open
+
+ draft
+ 0
+ 0
+ post
+
+ 0
+
+
+ _edit_last
+
+
+
+ -
+
Page
+ http://thisisa.test/contact/
+ Wed, 11 Apr 2012 11:38:08 +0000
+ bob
+ http://thisisa.test/?page_id=334
+
+
+
+ 334
+ 2012-04-11 06:38:08
+ 2012-04-11 11:38:08
+ open
+ open
+ contact
+ publish
+ 0
+ 0
+ page
+
+ 0
+
+ sharing_disabled
+
+
+
+ _wp_page_template
+
+
+
+ _edit_last
+
+
+
+ -
+
Empty Page
+ http://thisisa.test/empty/
+ Wed, 11 Apr 2012 11:38:08 +0000
+ bob
+ http://thisisa.test/?page_id=334
+
+
+
+ 334
+ 2012-04-11 06:38:08
+ 2012-04-11 11:38:08
+ open
+ open
+ empty
+ publish
+ 0
+ 0
+ page
+
+ 0
+
+ sharing_disabled
+
+
+
+ _wp_page_template
+
+
+
+ _edit_last
+
+
+
+ -
+
Special chars: l'é
+ http://thisisa.test/?p=471
+ Thu, 01 Jan 1970 00:00:00 +0000
+ bob
+ http://thisisa.test/?p=471
+
+
+
+ 471
+ 2012-04-29 09:44:27
+ 0000-00-00 00:00:00
+ open
+ open
+
+ draft
+ 0
+ 0
+ post
+
+ 0
+
+
+ _edit_last
+
+
+
+
+ -
+
With excerpt
+ http://thisisa.test/with-excerpt/
+ Sat, 04 Feb 2012 02:03:06 +0000
+ bob
+ http://thisisa.test/?p=8
+
+
+
+ 8
+ 2012-02-04 02:03:06
+ 2012-02-04 02:03:06
+ open
+ open
+ with-excerpt
+ publish
+ 0
+ 0
+ post
+
+ 0
+
+
+
+
+ _edit_last
+
+
+
+ et_bigpost
+
+
+
+ _thumbnail_id
+
+
+
+ -
+
With tags
+ http://thisisa.test/tags/
+ Sat, 04 Feb 2012 21:05:25 +0000
+ bob
+ http://thisisa.test/?p=25
+
+
+
+ 25
+ 2012-02-04 21:05:25
+ 2012-02-04 21:05:25
+ open
+ open
+ with-tags
+ publish
+ 0
+ 0
+ post
+
+ 0
+
+
+
+
+
+ _edit_last
+
+
+
+ et_bigpost
+
+
+
+ _thumbnail_id
+
+
+
+ -
+
With comments
+ http://thisisa.test/with-comments/
+ Wed, 18 Apr 2012 08:36:26 +0000
+ john
+ http://thisisa.test/?p=422
+
+
+
+ 422
+ 2012-04-18 03:36:26
+ 2012-04-18 08:36:26
+ open
+ open
+ with-comments
+ publish
+ 0
+ 0
+ post
+
+ 0
+
+
+ _edit_last
+
+
+
+ _thumbnail_id
+
+
+
+ 116
+
+ User2@mail.test
+
+ 127.0.0.1
+ 2012-05-06 15:46:06
+ 2012-05-06 20:46:06
+
+ 1
+
+ 0
+ 0
+
+ akismet_result
+
+
+
+ akismet_history
+
+
+
+ akismet_as_submitted
+
+
+
+
+ 117
+
+ bob@thisisa.test
+
+ 127.0.0.1
+ 2012-05-06 17:44:06
+ 2012-05-06 22:44:06
+
+ 1
+
+ 116
+ 3
+
+ akismet_result
+
+
+
+ akismet_history
+
+
+
+ akismet_as_submitted
+
+
+
+
+ 156
+
+
+ http://thisisa.test/to-article-you-ping-back/
+ 127.0.0.1
+ 2012-05-09 19:30:19
+ 2012-05-10 00:30:19
+
+ trash
+ pingback
+ 0
+ 0
+
+ akismet_history
+
+
+
+ _wp_trash_meta_status
+
+
+
+ _wp_trash_meta_time
+
+
+
+
+ 122
+
+ bob@thisisa.test
+
+ 127.0.0.1
+ 2012-05-07 14:11:34
+ 2012-05-07 19:11:34
+
+ 1
+
+ 121
+ 3
+
+ akismet_result
+
+
+
+ akismet_history
+
+
+
+ akismet_as_submitted
+
+
+
+
+ -
+
Post with raw data
+ http://thisisa.test/?p=173
+ Thu, 01 Jan 1970 00:00:00 +0000
+ bob
+ http://thisisa.test/?p=173
+
+ Pelicans are scary
+
+Pelicans are supposed to eat fish, damn it!
+
+VIDEO
+
+Bottom line: don't mess up with birds
+
+"That's a 'wonderful' shoe."
+
+“That’s a ‘magic’ sock.”]]>
+
+ 173
+ 2012-02-16 15:52:55
+ 0000-00-00 00:00:00
+ open
+ open
+ post-with-raw-data
+ publish
+ 0
+ 0
+ post
+
+ 0
+
+
+ _edit_last
+
+
+
+ -
+
A normal post with some <html> entities in the title. You can't miss them.
+ http://thisisa.test/?p=175
+ Thu, 01 Jan 1970 00:00:00 +0000
+ bob
+ http://thisisa.test/?p=175
+
+
+ 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.
+
+
+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.]]>
+
+ 175
+ 2012-02-16 15:52:55
+ 0000-00-00 00:00:00
+ open
+ open
+ html-entity-test
+ publish
+ 0
+ 0
+ post
+
+ 0
+
+
+ _edit_last
+
+
+
+ -
+
Code in List
+ http://thisisa.test/?p=175
+ Thu, 01 Jan 1970 00:00:00 +0000
+ bob
+ http://thisisa.test/?p=175
+
+
+ List Item One!
+ List Item Two!
+This is a code sample
+
+
+ a = [1, 2, 3]
+ b = [4, 5, 6]
+ for i in zip(a, b):
+ print i
+
+
+ List Item Four!
+
+
+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.]]>
+
+ 175
+ 2012-02-16 15:52:55
+ 0000-00-00 00:00:00
+ open
+ open
+ code-in-list-test
+ publish
+ 0
+ 0
+ post
+
+ 0
+
+
+ _edit_last
+
+
+
+ -
+
A custom post in category 4
+ http://thisisa.test/?p=175
+ Thu, 01 Jan 1970 00:00:00 +0000
+ bob
+ http://thisisa.test/?p=175
+
+
+ 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.
+
+
+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.]]>
+
+ 175
+ 2012-02-16 15:52:55
+ 0000-00-00 00:00:00
+ open
+ open
+ custpost1cat4
+ publish
+ 0
+ 0
+ custom1
+
+ 0
+
+
+ _edit_last
+
+
+
+ -
+
A custom post in category 5
+ http://thisisa.test/?p=176
+ Thu, 01 Jan 1970 00:00:00 +0000
+ bob
+ http://thisisa.test/?p=176
+
+
+ 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.
+
+
+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.]]>
+
+ 176
+ 2012-02-16 15:52:55
+ 0000-00-00 00:00:00
+ open
+ open
+ custpost1cat5
+ publish
+ 0
+ 0
+ custom1
+
+ 0
+
+
+ _edit_last
+
+
+
+ -
+
A 2nd custom post type also in category 5
+ http://thisisa.test/?p=177
+ Thu, 01 Jan 1970 00:00:00 +0000
+ bob
+ http://thisisa.test/?p=177
+
+
+ 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.
+
+
+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.]]>
+
+ 177
+ 2012-02-16 15:52:55
+ 0000-00-00 00:00:00
+ open
+ open
+ custpost2cat5
+ publish
+ 0
+ 0
+ custom2
+
+ 0
+
+
+ _edit_last
+
+
+
+ -
+
Attachment with a parent
+ http://thisisa.test/?attachment_id=24
+ Sat, 04 Feb 2012 03:17:33 +0000
+ bob
+ http://thisurlisinvalid.notarealdomain/not_an_image.jpg
+
+
+
+ 25
+ 2012-02-04 03:17:33
+ 2012-02-04 03:17:33
+ open
+ open
+ attachment-with-a-parent
+ inherit
+ 8
+ 0
+ attachment
+
+ 0
+ http://thisurlisinvalid.notarealdomain/not_an_image.jpg
+
+ _wp_attachment_metadata
+
+
+
+ _wp_attached_file
+
+
+
+ _wp_attachment_image_alt
+
+
+
+ -
+
2nd Attachment to same parent
+ http://thisisa.test/?attachment_id=25
+ Sat, 04 Feb 2012 03:17:33 +0000
+ bob
+ http://en.wikipedia.org/wiki/File:Pelikan_Walvis_Bay.jpg
+
+
+
+ 25
+ 2012-02-04 03:17:33
+ 2012-02-04 03:17:33
+ open
+ open
+ 2nd[attachment-to-same-parent
+ inherit
+ 8
+ 0
+ attachment
+
+ 0
+ http://en.wikipedia.org/wiki/File:Pelikan_Walvis_Bay.jpg
+
+ _wp_attachment_metadata
+
+
+
+ _wp_attached_file
+
+
+
+ _wp_attachment_image_alt
+
+
+
+ -
+
Attachment with a different parent
+ http://thisisa.test/?attachment_id=26
+ Sat, 04 Feb 2012 03:17:33 +0000
+ bob
+ http://thisurlisinvalid.notarealdomain
+
+
+
+ 25
+ 2012-02-04 03:17:33
+ 2012-02-04 03:17:33
+ open
+ open
+ attachment-with-a-different-parent
+ inherit
+ 25
+ 0
+ attachment
+
+ 0
+ http://thisurlisinvalid.notarealdomain
+
+ _wp_attachment_metadata
+
+
+
+ _wp_attached_file
+
+
+
+ _wp_attachment_image_alt
+
+
+
+
+
diff --git a/pelican/tests/cyclic_intersite_links/first-article.rst b/pelican/tests/cyclic_intersite_links/first-article.rst
new file mode 100644
index 00000000..b9eb3f93
--- /dev/null
+++ b/pelican/tests/cyclic_intersite_links/first-article.rst
@@ -0,0 +1,7 @@
+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>`_.
diff --git a/pelican/tests/cyclic_intersite_links/second-article.rst b/pelican/tests/cyclic_intersite_links/second-article.rst
new file mode 100644
index 00000000..acb87d47
--- /dev/null
+++ b/pelican/tests/cyclic_intersite_links/second-article.rst
@@ -0,0 +1,7 @@
+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>`_.
diff --git a/pelican/tests/cyclic_intersite_links/third-article.rst b/pelican/tests/cyclic_intersite_links/third-article.rst
new file mode 100644
index 00000000..4c7c1b76
--- /dev/null
+++ b/pelican/tests/cyclic_intersite_links/third-article.rst
@@ -0,0 +1,7 @@
+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>`_.
diff --git a/pelican/tests/default_conf.py b/pelican/tests/default_conf.py
new file mode 100644
index 00000000..99f3b6cf
--- /dev/null
+++ b/pelican/tests/default_conf.py
@@ -0,0 +1,44 @@
+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"
diff --git a/pelican/tests/dummy_plugins/namespace_plugin/pelican/plugins/ns_plugin/__init__.py b/pelican/tests/dummy_plugins/namespace_plugin/pelican/plugins/ns_plugin/__init__.py
new file mode 100644
index 00000000..c514861d
--- /dev/null
+++ b/pelican/tests/dummy_plugins/namespace_plugin/pelican/plugins/ns_plugin/__init__.py
@@ -0,0 +1,5 @@
+NAME = 'namespace plugin'
+
+
+def register():
+ pass
diff --git a/pelican/tests/dummy_plugins/normal_plugin/normal_plugin/__init__.py b/pelican/tests/dummy_plugins/normal_plugin/normal_plugin/__init__.py
new file mode 100644
index 00000000..5838a835
--- /dev/null
+++ b/pelican/tests/dummy_plugins/normal_plugin/normal_plugin/__init__.py
@@ -0,0 +1,7 @@
+from .submodule import noop # noqa: F401
+
+NAME = 'normal plugin'
+
+
+def register():
+ pass
diff --git a/pelican/tests/dummy_plugins/normal_plugin/normal_plugin/submodule.py b/pelican/tests/dummy_plugins/normal_plugin/normal_plugin/submodule.py
new file mode 100644
index 00000000..ba9c8f94
--- /dev/null
+++ b/pelican/tests/dummy_plugins/normal_plugin/normal_plugin/submodule.py
@@ -0,0 +1,2 @@
+def noop():
+ pass
diff --git a/pelican/tests/mixed_content/fake_image.jpg b/pelican/tests/mixed_content/fake_image.jpg
new file mode 100644
index 00000000..e69de29b
diff --git a/pelican/tests/mixed_content/short_page.md b/pelican/tests/mixed_content/short_page.md
new file mode 100644
index 00000000..46ca45ac
--- /dev/null
+++ b/pelican/tests/mixed_content/short_page.md
@@ -0,0 +1,3 @@
+Title: Short Page
+
+This is a page with little text.
diff --git a/pelican/tests/mixed_content/subdir/subdir_fake_image.jpg b/pelican/tests/mixed_content/subdir/subdir_fake_image.jpg
new file mode 100644
index 00000000..e69de29b
diff --git a/pelican/tests/nested_content/maindir/maindir.md b/pelican/tests/nested_content/maindir/maindir.md
new file mode 100644
index 00000000..443e1827
--- /dev/null
+++ b/pelican/tests/nested_content/maindir/maindir.md
@@ -0,0 +1,3 @@
+Title: Main Dir Page
+
+This page lives in maindir.
diff --git a/pelican/tests/nested_content/maindir/subdir/subdir.md b/pelican/tests/nested_content/maindir/subdir/subdir.md
new file mode 100644
index 00000000..32e73617
--- /dev/null
+++ b/pelican/tests/nested_content/maindir/subdir/subdir.md
@@ -0,0 +1,3 @@
+Title: Subdir Page
+
+This page lives in maindir/subdir.
diff --git a/pelican/tests/output/basic/a-markdown-powered-article.html b/pelican/tests/output/basic/a-markdown-powered-article.html
new file mode 100644
index 00000000..894fb8c9
--- /dev/null
+++ b/pelican/tests/output/basic/a-markdown-powered-article.html
@@ -0,0 +1,67 @@
+
+
+
+
+
+ A markdown powered article
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Wed 20 April 2011
+
+
+In cat1 .
+
+ You're mutually oblivious.
+
a root-relative link to unbelievable
+a file-relative link to unbelievable
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/archives.html b/pelican/tests/output/basic/archives.html
new file mode 100644
index 00000000..27cbb73e
--- /dev/null
+++ b/pelican/tests/output/basic/archives.html
@@ -0,0 +1,69 @@
+
+
+
+
+
+ A Pelican Blog
+
+
+
+
+
+
+
+Archives for A Pelican Blog
+
+
+ Fri 30 November 2012
+ FILENAME_METADATA example
+ Wed 29 February 2012
+ Second article
+ Wed 20 April 2011
+ A markdown powered article
+ Thu 17 February 2011
+ Article 1
+ Thu 17 February 2011
+ Article 2
+ Thu 17 February 2011
+ Article 3
+ Thu 02 December 2010
+ This is a super article !
+ Wed 20 October 2010
+ Oh yeah !
+ Fri 15 October 2010
+ Unbelievable !
+ Sun 14 March 2010
+ The baz tag
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/article-1.html b/pelican/tests/output/basic/article-1.html
new file mode 100644
index 00000000..5e1196b7
--- /dev/null
+++ b/pelican/tests/output/basic/article-1.html
@@ -0,0 +1,66 @@
+
+
+
+
+
+ Article 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Thu 17 February 2011
+
+
+In cat1 .
+
+ Article 1
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/article-2.html b/pelican/tests/output/basic/article-2.html
new file mode 100644
index 00000000..7dab6907
--- /dev/null
+++ b/pelican/tests/output/basic/article-2.html
@@ -0,0 +1,66 @@
+
+
+
+
+
+ Article 2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Thu 17 February 2011
+
+
+In cat1 .
+
+ Article 2
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/article-3.html b/pelican/tests/output/basic/article-3.html
new file mode 100644
index 00000000..ba934652
--- /dev/null
+++ b/pelican/tests/output/basic/article-3.html
@@ -0,0 +1,66 @@
+
+
+
+
+
+ Article 3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Thu 17 February 2011
+
+
+In cat1 .
+
+ Article 3
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/author/alexis-metaireau.html b/pelican/tests/output/basic/author/alexis-metaireau.html
new file mode 100644
index 00000000..a4d7a279
--- /dev/null
+++ b/pelican/tests/output/basic/author/alexis-metaireau.html
@@ -0,0 +1,111 @@
+
+
+
+
+
+ A Pelican Blog - Alexis Métaireau
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Thu 02 December 2010
+
+
+
+ Updated: Sun 17 November 2013
+
+
+
+ By Alexis Métaireau
+
+In yeah .
+tags: foo bar foobar
+ Some content here !
+
+
This is a simple title
+
And here comes the cool stuff .
+
+
+
+>>> from ipdb import set_trace
+>>> set_trace()
+
+
→ And now try with some utf8 hell: ééé
+
+
+
+
+ Other articles
+
+
+
+
+
+
+
+
+
+ Published: Wed 20 October 2010
+
+
+
+ By Alexis Métaireau
+
+In bar .
+tags: oh bar yeah
Translations:
+ fr
+
+
+
Why not ?
+
After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !
+
+
+
+
read more
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/authors.html b/pelican/tests/output/basic/authors.html
new file mode 100644
index 00000000..f062ccf6
--- /dev/null
+++ b/pelican/tests/output/basic/authors.html
@@ -0,0 +1,51 @@
+
+
+
+
+
+ A Pelican Blog - Authors
+
+
+
+
+
+
+
+
+ Authors on A Pelican Blog
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/categories.html b/pelican/tests/output/basic/categories.html
new file mode 100644
index 00000000..e9d981c8
--- /dev/null
+++ b/pelican/tests/output/basic/categories.html
@@ -0,0 +1,50 @@
+
+
+
+
+
+ A Pelican Blog - Categories
+
+
+
+
+
+
+ Categories on A Pelican Blog
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/category/bar.html b/pelican/tests/output/basic/category/bar.html
new file mode 100644
index 00000000..7e272b78
--- /dev/null
+++ b/pelican/tests/output/basic/category/bar.html
@@ -0,0 +1,67 @@
+
+
+
+
+
+ A Pelican Blog - bar
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Wed 20 October 2010
+
+
+
+ By Alexis Métaireau
+
+In bar .
+tags: oh bar yeah
Translations:
+ fr
+
+
+
Why not ?
+
After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/category/cat1.html b/pelican/tests/output/basic/category/cat1.html
new file mode 100644
index 00000000..eccf5587
--- /dev/null
+++ b/pelican/tests/output/basic/category/cat1.html
@@ -0,0 +1,124 @@
+
+
+
+
+
+ A Pelican Blog - cat1
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Wed 20 April 2011
+
+
+In cat1 .
+
+ You're mutually oblivious.
+a root-relative link to unbelievable
+a file-relative link to unbelievable
+
+
+ Other articles
+
+
+
+
+
+
+
+
+
+ Published: Thu 17 February 2011
+
+
+In cat1 .
+
+ Article 1
+
+
read more
+
+
+
+
+
+
+
+
+
+ Published: Thu 17 February 2011
+
+
+In cat1 .
+
+ Article 2
+
+
read more
+
+
+
+
+
+
+
+
+
+ Published: Thu 17 February 2011
+
+
+In cat1 .
+
+ Article 3
+
+
read more
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/category/misc.html b/pelican/tests/output/basic/category/misc.html
new file mode 100644
index 00000000..4f597888
--- /dev/null
+++ b/pelican/tests/output/basic/category/misc.html
@@ -0,0 +1,135 @@
+
+
+
+
+
+ A Pelican Blog - misc
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Fri 30 November 2012
+
+
+In misc .
+
+ Some cool stuff!
+
+
+
+ Other articles
+
+
+
+
+
+
+
+
+
+ Published: Wed 29 February 2012
+
+
+In misc .
+tags: foo bar baz
Translations:
+ fr
+
+ This is some article, in english
+
+
read more
+
+
+
+
+
+
+
+
+
+ Published: Fri 15 October 2010
+
+
+In misc .
+
+ Or completely awesome. Depends the needs.
+
a root-relative link to markdown-article
+a file-relative link to markdown-article
+
+
Testing sourcecode directive
+
formatter = self . options and VARIANTS [ self . options . keys ()[ 0 ]]
+
+
+
+
Testing another case
+
This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will …
+
read more
+
+
+
+
+
+
+
+
+
+ Published: Sun 14 March 2010
+
+
+In misc .
+
+ This article overrides the listening of the articles under the baz tag.
+
+
read more
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/category/yeah.html b/pelican/tests/output/basic/category/yeah.html
new file mode 100644
index 00000000..31a95629
--- /dev/null
+++ b/pelican/tests/output/basic/category/yeah.html
@@ -0,0 +1,75 @@
+
+
+
+
+
+ A Pelican Blog - yeah
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Thu 02 December 2010
+
+
+
+ Updated: Sun 17 November 2013
+
+
+
+ By Alexis Métaireau
+
+In yeah .
+tags: foo bar foobar
+ Some content here !
+
+
This is a simple title
+
And here comes the cool stuff .
+
+
+
+>>> from ipdb import set_trace
+>>> set_trace()
+
+
→ And now try with some utf8 hell: ééé
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/drafts/a-draft-article-without-date.html b/pelican/tests/output/basic/drafts/a-draft-article-without-date.html
new file mode 100644
index 00000000..039f2187
--- /dev/null
+++ b/pelican/tests/output/basic/drafts/a-draft-article-without-date.html
@@ -0,0 +1,67 @@
+
+
+
+
+
+ A draft article without date
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published:
+
+
+In misc .
+
+ This is a draft article, it should live under the /drafts/ folder and not be
+listed anywhere else.
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/drafts/a-draft-article.html b/pelican/tests/output/basic/drafts/a-draft-article.html
new file mode 100644
index 00000000..068b414a
--- /dev/null
+++ b/pelican/tests/output/basic/drafts/a-draft-article.html
@@ -0,0 +1,67 @@
+
+
+
+
+
+ A draft article
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Sun 08 May 2011
+
+
+In misc .
+
+ This is a draft article, it should live under the /drafts/ folder and not be
+listed anywhere else.
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/feeds/alexis-metaireau.atom.xml b/pelican/tests/output/basic/feeds/alexis-metaireau.atom.xml
new file mode 100644
index 00000000..8f9a85fa
--- /dev/null
+++ b/pelican/tests/output/basic/feeds/alexis-metaireau.atom.xml
@@ -0,0 +1,22 @@
+
+A Pelican Blog - Alexis Métaireau / 2013-11-17T23:29:00+00:00 This is a super article ! 2010-12-02T10:14:00+00:00 2013-11-17T23:29:00+00:00 Alexis Métaireau tag:None,2010-12-02:/this-is-a-super-article.html <p class="first last">Multi-line metadata should be supported
+as well as <strong>inline markup</strong>.</p>
+ <p>Some content here !</p>
+<div class="section" id="this-is-a-simple-title">
+<h2>This is a simple title</h2>
+<p>And here comes the cool <a class="reference external" href="http://books.couchdb.org/relax/design-documents/views">stuff</a>.</p>
+<img alt="alternate text" src="/pictures/Sushi.jpg" style="width: 600px; height: 450px;" />
+<img alt="alternate text" src="/pictures/Sushi_Macro.jpg" style="width: 600px; height: 450px;" />
+<pre class="literal-block">
+>>> from ipdb import set_trace
+>>> set_trace()
+</pre>
+<p>→ And now try with some utf8 hell: ééé</p>
+</div>
+ Oh yeah ! 2010-10-20T10:14:00+00:00 2010-10-20T10:14:00+00:00 Alexis Métaireau tag:None,2010-10-20:/oh-yeah.html <div class="section" id="why-not">
+<h2>Why not ?</h2>
+<p>After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !</p>
+<img alt="alternate text" src="/pictures/Sushi.jpg" style="width: 600px; height: 450px;" />
+</div>
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/feeds/alexis-metaireau.rss.xml b/pelican/tests/output/basic/feeds/alexis-metaireau.rss.xml
new file mode 100644
index 00000000..1af41d47
--- /dev/null
+++ b/pelican/tests/output/basic/feeds/alexis-metaireau.rss.xml
@@ -0,0 +1,10 @@
+
+A Pelican Blog - Alexis Métaireau /Sun, 17 Nov 2013 23:29:00 +0000 This is a super article ! /this-is-a-super-article.html<p class="first last">Multi-line metadata should be supported
+as well as <strong>inline markup</strong>.</p>
+ Alexis Métaireau Thu, 02 Dec 2010 10:14:00 +0000 tag:None,2010-12-02:/this-is-a-super-article.html yeah foo bar foobar Oh yeah ! /oh-yeah.html<div class="section" id="why-not">
+<h2>Why not ?</h2>
+<p>After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !</p>
+<img alt="alternate text" src="/pictures/Sushi.jpg" style="width: 600px; height: 450px;" />
+</div>
+ Alexis Métaireau Wed, 20 Oct 2010 10:14:00 +0000 tag:None,2010-10-20:/oh-yeah.html bar oh bar yeah
\ No newline at end of file
diff --git a/pelican/tests/output/basic/feeds/all-en.atom.xml b/pelican/tests/output/basic/feeds/all-en.atom.xml
new file mode 100644
index 00000000..f59ea651
--- /dev/null
+++ b/pelican/tests/output/basic/feeds/all-en.atom.xml
@@ -0,0 +1,74 @@
+
+A Pelican Blog / 2013-11-17T23:29:00+00:00 FILENAME_METADATA example 2012-11-30T00:00:00+00:00 2012-11-30T00:00:00+00:00 tag:None,2012-11-30:/filename_metadata-example.html <p>Some cool stuff!</p>
+ Second article 2012-02-29T00:00:00+00:00 2012-02-29T00:00:00+00:00 tag:None,2012-02-29:/second-article.html <p>This is some article, in english</p>
+ A markdown powered article 2011-04-20T00:00:00+00:00 2011-04-20T00:00:00+00:00 tag:None,2011-04-20:/a-markdown-powered-article.html <p>You're mutually oblivious.</p>
+<p><a href="/unbelievable.html">a root-relative link to unbelievable</a>
+<a href="/unbelievable.html">a file-relative link to unbelievable</a></p> Article 1 2011-02-17T00:00:00+00:00 2011-02-17T00:00:00+00:00 tag:None,2011-02-17:/article-1.html <p>Article 1</p>
+ Article 2 2011-02-17T00:00:00+00:00 2011-02-17T00:00:00+00:00 tag:None,2011-02-17:/article-2.html <p>Article 2</p>
+ Article 3 2011-02-17T00:00:00+00:00 2011-02-17T00:00:00+00:00 tag:None,2011-02-17:/article-3.html <p>Article 3</p>
+ This is a super article ! 2010-12-02T10:14:00+00:00 2013-11-17T23:29:00+00:00 Alexis Métaireau tag:None,2010-12-02:/this-is-a-super-article.html <p class="first last">Multi-line metadata should be supported
+as well as <strong>inline markup</strong>.</p>
+ <p>Some content here !</p>
+<div class="section" id="this-is-a-simple-title">
+<h2>This is a simple title</h2>
+<p>And here comes the cool <a class="reference external" href="http://books.couchdb.org/relax/design-documents/views">stuff</a>.</p>
+<img alt="alternate text" src="/pictures/Sushi.jpg" style="width: 600px; height: 450px;" />
+<img alt="alternate text" src="/pictures/Sushi_Macro.jpg" style="width: 600px; height: 450px;" />
+<pre class="literal-block">
+>>> from ipdb import set_trace
+>>> set_trace()
+</pre>
+<p>→ And now try with some utf8 hell: ééé</p>
+</div>
+ Oh yeah ! 2010-10-20T10:14:00+00:00 2010-10-20T10:14:00+00:00 Alexis Métaireau tag:None,2010-10-20:/oh-yeah.html <div class="section" id="why-not">
+<h2>Why not ?</h2>
+<p>After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !</p>
+<img alt="alternate text" src="/pictures/Sushi.jpg" style="width: 600px; height: 450px;" />
+</div>
+ Unbelievable ! 2010-10-15T20:30:00+00:00 2010-10-15T20:30:00+00:00 tag:None,2010-10-15:/unbelievable.html <p>Or completely awesome. Depends the needs.</p>
+<p><a class="reference external" href="/a-markdown-powered-article.html">a root-relative link to markdown-article</a>
+<a class="reference external" href="/a-markdown-powered-article.html">a file-relative link to markdown-article</a></p>
+<div class="section" id="testing-sourcecode-directive">
+<h2>Testing sourcecode directive</h2>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table></div>
+<div class="section" id="testing-another-case">
+<h2>Testing another case</h2>
+<p>This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will …</p></div> <p>Or completely awesome. Depends the needs.</p>
+<p><a class="reference external" href="/a-markdown-powered-article.html">a root-relative link to markdown-article</a>
+<a class="reference external" href="/a-markdown-powered-article.html">a file-relative link to markdown-article</a></p>
+<div class="section" id="testing-sourcecode-directive">
+<h2>Testing sourcecode directive</h2>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table></div>
+<div class="section" id="testing-another-case">
+<h2>Testing another case</h2>
+<p>This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will have nothing in default.</p>
+<div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+<p>Lovely.</p>
+</div>
+<div class="section" id="testing-more-sourcecode-directives">
+<h2>Testing more sourcecode directives</h2>
+<div class="highlight"><pre><span></span><span id="foo-8"><a name="foo-8"></a><span class="lineno special"> 8 </span><span class="testingk">def</span> <span class="testingnf">run</span><span class="testingp">(</span><span class="testingbp">self</span><span class="testingp">):</span><br></span><span id="foo-9"><a name="foo-9"></a><span class="lineno"> </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">assert_has_content</span><span class="testingp">()</span><br></span><span id="foo-10"><a name="foo-10"></a><span class="lineno special">10 </span> <span class="testingk">try</span><span class="testingp">:</span><br></span><span id="foo-11"><a name="foo-11"></a><span class="lineno"> </span> <span class="testingn">lexer</span> <span class="testingo">=</span> <span class="testingn">get_lexer_by_name</span><span class="testingp">(</span><span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">arguments</span><span class="testingp">[</span><span class="testingmi">0</span><span class="testingp">])</span><br></span><span id="foo-12"><a name="foo-12"></a><span class="lineno special">12 </span> <span class="testingk">except</span> <span class="testingne">ValueError</span><span class="testingp">:</span><br></span><span id="foo-13"><a name="foo-13"></a><span class="lineno"> </span> <span class="testingc1"># no lexer found - use the text one instead of an exception</span><br></span><span id="foo-14"><a name="foo-14"></a><span class="lineno special">14 </span> <span class="testingn">lexer</span> <span class="testingo">=</span> <span class="testingn">TextLexer</span><span class="testingp">()</span><br></span><span id="foo-15"><a name="foo-15"></a><span class="lineno"> </span><br></span><span id="foo-16"><a name="foo-16"></a><span class="lineno special">16 </span> <span class="testingk">if</span> <span class="testingp">(</span><span class="testings1">'linenos'</span> <span class="testingow">in</span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span> <span class="testingow">and</span><br></span><span id="foo-17"><a name="foo-17"></a><span class="lineno"> </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">[</span><span class="testings1">'linenos'</span><span class="testingp">]</span> <span class="testingow">not</span> <span class="testingow">in</span> <span class="testingp">(</span><span class="testings1">'table'</span><span class="testingp">,</span> <span class="testings1">'inline'</span><span class="testingp">)):</span><br></span><span id="foo-18"><a name="foo-18"></a><span class="lineno special">18 </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">[</span><span class="testings1">'linenos'</span><span class="testingp">]</span> <span class="testingo">=</span> <span class="testings1">'table'</span><br></span><span id="foo-19"><a name="foo-19"></a><span class="lineno"> </span><br></span><span id="foo-20"><a name="foo-20"></a><span class="lineno special">20 </span> <span class="testingk">for</span> <span class="testingn">flag</span> <span class="testingow">in</span> <span class="testingp">(</span><span class="testings1">'nowrap'</span><span class="testingp">,</span> <span class="testings1">'nobackground'</span><span class="testingp">,</span> <span class="testings1">'anchorlinenos'</span><span class="testingp">):</span><br></span><span id="foo-21"><a name="foo-21"></a><span class="lineno"> </span> <span class="testingk">if</span> <span class="testingn">flag</span> <span class="testingow">in</span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">:</span><br></span><span id="foo-22"><a name="foo-22"></a><span class="lineno special">22 </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">[</span><span class="testingn">flag</span><span class="testingp">]</span> <span class="testingo">=</span> <span class="testingkc">True</span><br></span><span id="foo-23"><a name="foo-23"></a><span class="lineno"> </span><br></span><span id="foo-24"><a name="foo-24"></a><span class="lineno special">24 </span> <span class="testingc1"># noclasses should already default to False, but just in case...</span><br></span><span id="foo-25"><a name="foo-25"></a><span class="lineno"> </span> <span class="testingn">formatter</span> <span class="testingo">=</span> <span class="testingn">HtmlFormatter</span><span class="testingp">(</span><span class="testingn">noclasses</span><span class="testingo">=</span><span class="testingkc">False</span><span class="testingp">,</span> <span class="testingo">**</span><span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">)</span><br></span><span id="foo-26"><a name="foo-26"></a><span class="lineno special">26 </span> <span class="testingn">parsed</span> <span class="testingo">=</span> <span class="testingn">highlight</span><span class="testingp">(</span><span class="testings1">'</span><span class="testingse">\n</span><span class="testings1">'</span><span class="testingo">.</span><span class="testingn">join</span><span class="testingp">(</span><span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">content</span><span class="testingp">),</span> <span class="testingn">lexer</span><span class="testingp">,</span> <span class="testingn">formatter</span><span class="testingp">)</span><br></span><span id="foo-27"><a name="foo-27"></a><span class="lineno"> </span> <span class="testingk">return</span> <span class="testingp">[</span><span class="testingn">nodes</span><span class="testingo">.</span><span class="testingn">raw</span><span class="testingp">(</span><span class="testings1">''</span><span class="testingp">,</span> <span class="testingn">parsed</span><span class="testingp">,</span> <span class="testingnb">format</span><span class="testingo">=</span><span class="testings1">'html'</span><span class="testingp">)]</span><br></span></pre></div>
+<p>Lovely.</p>
+</div>
+<div class="section" id="testing-even-more-sourcecode-directives">
+<h2>Testing even more sourcecode directives</h2>
+<span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+<p>Lovely.</p>
+</div>
+<div class="section" id="testing-overriding-config-defaults">
+<h2>Testing overriding config defaults</h2>
+<p>Even if the default is line numbers, we can override it here</p>
+<div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+<p>Lovely.</p>
+</div>
+ The baz tag 2010-03-14T00:00:00+00:00 2010-03-14T00:00:00+00:00 tag:None,2010-03-14:/tag/baz.html <p>This article overrides the listening of the articles under the <em>baz</em> tag.</p>
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/feeds/all-fr.atom.xml b/pelican/tests/output/basic/feeds/all-fr.atom.xml
new file mode 100644
index 00000000..4ecf7534
--- /dev/null
+++ b/pelican/tests/output/basic/feeds/all-fr.atom.xml
@@ -0,0 +1,4 @@
+
+A Pelican Blog / 2012-02-29T00:00:00+00:00 Deuxième article 2012-02-29T00:00:00+00:00 2012-02-29T00:00:00+00:00 tag:None,2012-02-29:/second-article-fr.html <p>Ceci est un article, en français.</p>
+ Trop bien ! 2010-10-20T10:14:00+00:00 2010-10-20T10:14:00+00:00 tag:None,2010-10-20:/oh-yeah-fr.html <p>Et voila du contenu en français</p>
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/feeds/all.atom.xml b/pelican/tests/output/basic/feeds/all.atom.xml
new file mode 100644
index 00000000..7407d2ff
--- /dev/null
+++ b/pelican/tests/output/basic/feeds/all.atom.xml
@@ -0,0 +1,76 @@
+
+A Pelican Blog / 2013-11-17T23:29:00+00:00 FILENAME_METADATA example 2012-11-30T00:00:00+00:00 2012-11-30T00:00:00+00:00 tag:None,2012-11-30:/filename_metadata-example.html <p>Some cool stuff!</p>
+ Second article 2012-02-29T00:00:00+00:00 2012-02-29T00:00:00+00:00 tag:None,2012-02-29:/second-article.html <p>This is some article, in english</p>
+ Deuxième article 2012-02-29T00:00:00+00:00 2012-02-29T00:00:00+00:00 tag:None,2012-02-29:/second-article-fr.html <p>Ceci est un article, en français.</p>
+ A markdown powered article 2011-04-20T00:00:00+00:00 2011-04-20T00:00:00+00:00 tag:None,2011-04-20:/a-markdown-powered-article.html <p>You're mutually oblivious.</p>
+<p><a href="/unbelievable.html">a root-relative link to unbelievable</a>
+<a href="/unbelievable.html">a file-relative link to unbelievable</a></p> Article 1 2011-02-17T00:00:00+00:00 2011-02-17T00:00:00+00:00 tag:None,2011-02-17:/article-1.html <p>Article 1</p>
+ Article 2 2011-02-17T00:00:00+00:00 2011-02-17T00:00:00+00:00 tag:None,2011-02-17:/article-2.html <p>Article 2</p>
+ Article 3 2011-02-17T00:00:00+00:00 2011-02-17T00:00:00+00:00 tag:None,2011-02-17:/article-3.html <p>Article 3</p>
+ This is a super article ! 2010-12-02T10:14:00+00:00 2013-11-17T23:29:00+00:00 Alexis Métaireau tag:None,2010-12-02:/this-is-a-super-article.html <p class="first last">Multi-line metadata should be supported
+as well as <strong>inline markup</strong>.</p>
+ <p>Some content here !</p>
+<div class="section" id="this-is-a-simple-title">
+<h2>This is a simple title</h2>
+<p>And here comes the cool <a class="reference external" href="http://books.couchdb.org/relax/design-documents/views">stuff</a>.</p>
+<img alt="alternate text" src="/pictures/Sushi.jpg" style="width: 600px; height: 450px;" />
+<img alt="alternate text" src="/pictures/Sushi_Macro.jpg" style="width: 600px; height: 450px;" />
+<pre class="literal-block">
+>>> from ipdb import set_trace
+>>> set_trace()
+</pre>
+<p>→ And now try with some utf8 hell: ééé</p>
+</div>
+ Oh yeah ! 2010-10-20T10:14:00+00:00 2010-10-20T10:14:00+00:00 Alexis Métaireau tag:None,2010-10-20:/oh-yeah.html <div class="section" id="why-not">
+<h2>Why not ?</h2>
+<p>After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !</p>
+<img alt="alternate text" src="/pictures/Sushi.jpg" style="width: 600px; height: 450px;" />
+</div>
+ Trop bien ! 2010-10-20T10:14:00+00:00 2010-10-20T10:14:00+00:00 tag:None,2010-10-20:/oh-yeah-fr.html <p>Et voila du contenu en français</p>
+ Unbelievable ! 2010-10-15T20:30:00+00:00 2010-10-15T20:30:00+00:00 tag:None,2010-10-15:/unbelievable.html <p>Or completely awesome. Depends the needs.</p>
+<p><a class="reference external" href="/a-markdown-powered-article.html">a root-relative link to markdown-article</a>
+<a class="reference external" href="/a-markdown-powered-article.html">a file-relative link to markdown-article</a></p>
+<div class="section" id="testing-sourcecode-directive">
+<h2>Testing sourcecode directive</h2>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table></div>
+<div class="section" id="testing-another-case">
+<h2>Testing another case</h2>
+<p>This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will …</p></div> <p>Or completely awesome. Depends the needs.</p>
+<p><a class="reference external" href="/a-markdown-powered-article.html">a root-relative link to markdown-article</a>
+<a class="reference external" href="/a-markdown-powered-article.html">a file-relative link to markdown-article</a></p>
+<div class="section" id="testing-sourcecode-directive">
+<h2>Testing sourcecode directive</h2>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table></div>
+<div class="section" id="testing-another-case">
+<h2>Testing another case</h2>
+<p>This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will have nothing in default.</p>
+<div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+<p>Lovely.</p>
+</div>
+<div class="section" id="testing-more-sourcecode-directives">
+<h2>Testing more sourcecode directives</h2>
+<div class="highlight"><pre><span></span><span id="foo-8"><a name="foo-8"></a><span class="lineno special"> 8 </span><span class="testingk">def</span> <span class="testingnf">run</span><span class="testingp">(</span><span class="testingbp">self</span><span class="testingp">):</span><br></span><span id="foo-9"><a name="foo-9"></a><span class="lineno"> </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">assert_has_content</span><span class="testingp">()</span><br></span><span id="foo-10"><a name="foo-10"></a><span class="lineno special">10 </span> <span class="testingk">try</span><span class="testingp">:</span><br></span><span id="foo-11"><a name="foo-11"></a><span class="lineno"> </span> <span class="testingn">lexer</span> <span class="testingo">=</span> <span class="testingn">get_lexer_by_name</span><span class="testingp">(</span><span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">arguments</span><span class="testingp">[</span><span class="testingmi">0</span><span class="testingp">])</span><br></span><span id="foo-12"><a name="foo-12"></a><span class="lineno special">12 </span> <span class="testingk">except</span> <span class="testingne">ValueError</span><span class="testingp">:</span><br></span><span id="foo-13"><a name="foo-13"></a><span class="lineno"> </span> <span class="testingc1"># no lexer found - use the text one instead of an exception</span><br></span><span id="foo-14"><a name="foo-14"></a><span class="lineno special">14 </span> <span class="testingn">lexer</span> <span class="testingo">=</span> <span class="testingn">TextLexer</span><span class="testingp">()</span><br></span><span id="foo-15"><a name="foo-15"></a><span class="lineno"> </span><br></span><span id="foo-16"><a name="foo-16"></a><span class="lineno special">16 </span> <span class="testingk">if</span> <span class="testingp">(</span><span class="testings1">'linenos'</span> <span class="testingow">in</span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span> <span class="testingow">and</span><br></span><span id="foo-17"><a name="foo-17"></a><span class="lineno"> </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">[</span><span class="testings1">'linenos'</span><span class="testingp">]</span> <span class="testingow">not</span> <span class="testingow">in</span> <span class="testingp">(</span><span class="testings1">'table'</span><span class="testingp">,</span> <span class="testings1">'inline'</span><span class="testingp">)):</span><br></span><span id="foo-18"><a name="foo-18"></a><span class="lineno special">18 </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">[</span><span class="testings1">'linenos'</span><span class="testingp">]</span> <span class="testingo">=</span> <span class="testings1">'table'</span><br></span><span id="foo-19"><a name="foo-19"></a><span class="lineno"> </span><br></span><span id="foo-20"><a name="foo-20"></a><span class="lineno special">20 </span> <span class="testingk">for</span> <span class="testingn">flag</span> <span class="testingow">in</span> <span class="testingp">(</span><span class="testings1">'nowrap'</span><span class="testingp">,</span> <span class="testings1">'nobackground'</span><span class="testingp">,</span> <span class="testings1">'anchorlinenos'</span><span class="testingp">):</span><br></span><span id="foo-21"><a name="foo-21"></a><span class="lineno"> </span> <span class="testingk">if</span> <span class="testingn">flag</span> <span class="testingow">in</span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">:</span><br></span><span id="foo-22"><a name="foo-22"></a><span class="lineno special">22 </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">[</span><span class="testingn">flag</span><span class="testingp">]</span> <span class="testingo">=</span> <span class="testingkc">True</span><br></span><span id="foo-23"><a name="foo-23"></a><span class="lineno"> </span><br></span><span id="foo-24"><a name="foo-24"></a><span class="lineno special">24 </span> <span class="testingc1"># noclasses should already default to False, but just in case...</span><br></span><span id="foo-25"><a name="foo-25"></a><span class="lineno"> </span> <span class="testingn">formatter</span> <span class="testingo">=</span> <span class="testingn">HtmlFormatter</span><span class="testingp">(</span><span class="testingn">noclasses</span><span class="testingo">=</span><span class="testingkc">False</span><span class="testingp">,</span> <span class="testingo">**</span><span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">)</span><br></span><span id="foo-26"><a name="foo-26"></a><span class="lineno special">26 </span> <span class="testingn">parsed</span> <span class="testingo">=</span> <span class="testingn">highlight</span><span class="testingp">(</span><span class="testings1">'</span><span class="testingse">\n</span><span class="testings1">'</span><span class="testingo">.</span><span class="testingn">join</span><span class="testingp">(</span><span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">content</span><span class="testingp">),</span> <span class="testingn">lexer</span><span class="testingp">,</span> <span class="testingn">formatter</span><span class="testingp">)</span><br></span><span id="foo-27"><a name="foo-27"></a><span class="lineno"> </span> <span class="testingk">return</span> <span class="testingp">[</span><span class="testingn">nodes</span><span class="testingo">.</span><span class="testingn">raw</span><span class="testingp">(</span><span class="testings1">''</span><span class="testingp">,</span> <span class="testingn">parsed</span><span class="testingp">,</span> <span class="testingnb">format</span><span class="testingo">=</span><span class="testings1">'html'</span><span class="testingp">)]</span><br></span></pre></div>
+<p>Lovely.</p>
+</div>
+<div class="section" id="testing-even-more-sourcecode-directives">
+<h2>Testing even more sourcecode directives</h2>
+<span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+<p>Lovely.</p>
+</div>
+<div class="section" id="testing-overriding-config-defaults">
+<h2>Testing overriding config defaults</h2>
+<p>Even if the default is line numbers, we can override it here</p>
+<div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+<p>Lovely.</p>
+</div>
+ The baz tag 2010-03-14T00:00:00+00:00 2010-03-14T00:00:00+00:00 tag:None,2010-03-14:/tag/baz.html <p>This article overrides the listening of the articles under the <em>baz</em> tag.</p>
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/feeds/bar.atom.xml b/pelican/tests/output/basic/feeds/bar.atom.xml
new file mode 100644
index 00000000..edd1170a
--- /dev/null
+++ b/pelican/tests/output/basic/feeds/bar.atom.xml
@@ -0,0 +1,8 @@
+
+A Pelican Blog - bar / 2010-10-20T10:14:00+00:00 Oh yeah ! 2010-10-20T10:14:00+00:00 2010-10-20T10:14:00+00:00 Alexis Métaireau tag:None,2010-10-20:/oh-yeah.html <div class="section" id="why-not">
+<h2>Why not ?</h2>
+<p>After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !</p>
+<img alt="alternate text" src="/pictures/Sushi.jpg" style="width: 600px; height: 450px;" />
+</div>
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/feeds/cat1.atom.xml b/pelican/tests/output/basic/feeds/cat1.atom.xml
new file mode 100644
index 00000000..8516b95c
--- /dev/null
+++ b/pelican/tests/output/basic/feeds/cat1.atom.xml
@@ -0,0 +1,7 @@
+
+A Pelican Blog - cat1 / 2011-04-20T00:00:00+00:00 A markdown powered article 2011-04-20T00:00:00+00:00 2011-04-20T00:00:00+00:00 tag:None,2011-04-20:/a-markdown-powered-article.html <p>You're mutually oblivious.</p>
+<p><a href="/unbelievable.html">a root-relative link to unbelievable</a>
+<a href="/unbelievable.html">a file-relative link to unbelievable</a></p> Article 1 2011-02-17T00:00:00+00:00 2011-02-17T00:00:00+00:00 tag:None,2011-02-17:/article-1.html <p>Article 1</p>
+ Article 2 2011-02-17T00:00:00+00:00 2011-02-17T00:00:00+00:00 tag:None,2011-02-17:/article-2.html <p>Article 2</p>
+ Article 3 2011-02-17T00:00:00+00:00 2011-02-17T00:00:00+00:00 tag:None,2011-02-17:/article-3.html <p>Article 3</p>
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/feeds/misc.atom.xml b/pelican/tests/output/basic/feeds/misc.atom.xml
new file mode 100644
index 00000000..a8243b48
--- /dev/null
+++ b/pelican/tests/output/basic/feeds/misc.atom.xml
@@ -0,0 +1,49 @@
+
+A Pelican Blog - misc / 2012-11-30T00:00:00+00:00 FILENAME_METADATA example 2012-11-30T00:00:00+00:00 2012-11-30T00:00:00+00:00 tag:None,2012-11-30:/filename_metadata-example.html <p>Some cool stuff!</p>
+ Second article 2012-02-29T00:00:00+00:00 2012-02-29T00:00:00+00:00 tag:None,2012-02-29:/second-article.html <p>This is some article, in english</p>
+ Unbelievable ! 2010-10-15T20:30:00+00:00 2010-10-15T20:30:00+00:00 tag:None,2010-10-15:/unbelievable.html <p>Or completely awesome. Depends the needs.</p>
+<p><a class="reference external" href="/a-markdown-powered-article.html">a root-relative link to markdown-article</a>
+<a class="reference external" href="/a-markdown-powered-article.html">a file-relative link to markdown-article</a></p>
+<div class="section" id="testing-sourcecode-directive">
+<h2>Testing sourcecode directive</h2>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table></div>
+<div class="section" id="testing-another-case">
+<h2>Testing another case</h2>
+<p>This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will …</p></div> <p>Or completely awesome. Depends the needs.</p>
+<p><a class="reference external" href="/a-markdown-powered-article.html">a root-relative link to markdown-article</a>
+<a class="reference external" href="/a-markdown-powered-article.html">a file-relative link to markdown-article</a></p>
+<div class="section" id="testing-sourcecode-directive">
+<h2>Testing sourcecode directive</h2>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table></div>
+<div class="section" id="testing-another-case">
+<h2>Testing another case</h2>
+<p>This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will have nothing in default.</p>
+<div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+<p>Lovely.</p>
+</div>
+<div class="section" id="testing-more-sourcecode-directives">
+<h2>Testing more sourcecode directives</h2>
+<div class="highlight"><pre><span></span><span id="foo-8"><a name="foo-8"></a><span class="lineno special"> 8 </span><span class="testingk">def</span> <span class="testingnf">run</span><span class="testingp">(</span><span class="testingbp">self</span><span class="testingp">):</span><br></span><span id="foo-9"><a name="foo-9"></a><span class="lineno"> </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">assert_has_content</span><span class="testingp">()</span><br></span><span id="foo-10"><a name="foo-10"></a><span class="lineno special">10 </span> <span class="testingk">try</span><span class="testingp">:</span><br></span><span id="foo-11"><a name="foo-11"></a><span class="lineno"> </span> <span class="testingn">lexer</span> <span class="testingo">=</span> <span class="testingn">get_lexer_by_name</span><span class="testingp">(</span><span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">arguments</span><span class="testingp">[</span><span class="testingmi">0</span><span class="testingp">])</span><br></span><span id="foo-12"><a name="foo-12"></a><span class="lineno special">12 </span> <span class="testingk">except</span> <span class="testingne">ValueError</span><span class="testingp">:</span><br></span><span id="foo-13"><a name="foo-13"></a><span class="lineno"> </span> <span class="testingc1"># no lexer found - use the text one instead of an exception</span><br></span><span id="foo-14"><a name="foo-14"></a><span class="lineno special">14 </span> <span class="testingn">lexer</span> <span class="testingo">=</span> <span class="testingn">TextLexer</span><span class="testingp">()</span><br></span><span id="foo-15"><a name="foo-15"></a><span class="lineno"> </span><br></span><span id="foo-16"><a name="foo-16"></a><span class="lineno special">16 </span> <span class="testingk">if</span> <span class="testingp">(</span><span class="testings1">'linenos'</span> <span class="testingow">in</span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span> <span class="testingow">and</span><br></span><span id="foo-17"><a name="foo-17"></a><span class="lineno"> </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">[</span><span class="testings1">'linenos'</span><span class="testingp">]</span> <span class="testingow">not</span> <span class="testingow">in</span> <span class="testingp">(</span><span class="testings1">'table'</span><span class="testingp">,</span> <span class="testings1">'inline'</span><span class="testingp">)):</span><br></span><span id="foo-18"><a name="foo-18"></a><span class="lineno special">18 </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">[</span><span class="testings1">'linenos'</span><span class="testingp">]</span> <span class="testingo">=</span> <span class="testings1">'table'</span><br></span><span id="foo-19"><a name="foo-19"></a><span class="lineno"> </span><br></span><span id="foo-20"><a name="foo-20"></a><span class="lineno special">20 </span> <span class="testingk">for</span> <span class="testingn">flag</span> <span class="testingow">in</span> <span class="testingp">(</span><span class="testings1">'nowrap'</span><span class="testingp">,</span> <span class="testings1">'nobackground'</span><span class="testingp">,</span> <span class="testings1">'anchorlinenos'</span><span class="testingp">):</span><br></span><span id="foo-21"><a name="foo-21"></a><span class="lineno"> </span> <span class="testingk">if</span> <span class="testingn">flag</span> <span class="testingow">in</span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">:</span><br></span><span id="foo-22"><a name="foo-22"></a><span class="lineno special">22 </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">[</span><span class="testingn">flag</span><span class="testingp">]</span> <span class="testingo">=</span> <span class="testingkc">True</span><br></span><span id="foo-23"><a name="foo-23"></a><span class="lineno"> </span><br></span><span id="foo-24"><a name="foo-24"></a><span class="lineno special">24 </span> <span class="testingc1"># noclasses should already default to False, but just in case...</span><br></span><span id="foo-25"><a name="foo-25"></a><span class="lineno"> </span> <span class="testingn">formatter</span> <span class="testingo">=</span> <span class="testingn">HtmlFormatter</span><span class="testingp">(</span><span class="testingn">noclasses</span><span class="testingo">=</span><span class="testingkc">False</span><span class="testingp">,</span> <span class="testingo">**</span><span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">)</span><br></span><span id="foo-26"><a name="foo-26"></a><span class="lineno special">26 </span> <span class="testingn">parsed</span> <span class="testingo">=</span> <span class="testingn">highlight</span><span class="testingp">(</span><span class="testings1">'</span><span class="testingse">\n</span><span class="testings1">'</span><span class="testingo">.</span><span class="testingn">join</span><span class="testingp">(</span><span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">content</span><span class="testingp">),</span> <span class="testingn">lexer</span><span class="testingp">,</span> <span class="testingn">formatter</span><span class="testingp">)</span><br></span><span id="foo-27"><a name="foo-27"></a><span class="lineno"> </span> <span class="testingk">return</span> <span class="testingp">[</span><span class="testingn">nodes</span><span class="testingo">.</span><span class="testingn">raw</span><span class="testingp">(</span><span class="testings1">''</span><span class="testingp">,</span> <span class="testingn">parsed</span><span class="testingp">,</span> <span class="testingnb">format</span><span class="testingo">=</span><span class="testings1">'html'</span><span class="testingp">)]</span><br></span></pre></div>
+<p>Lovely.</p>
+</div>
+<div class="section" id="testing-even-more-sourcecode-directives">
+<h2>Testing even more sourcecode directives</h2>
+<span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+<p>Lovely.</p>
+</div>
+<div class="section" id="testing-overriding-config-defaults">
+<h2>Testing overriding config defaults</h2>
+<p>Even if the default is line numbers, we can override it here</p>
+<div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+<p>Lovely.</p>
+</div>
+ The baz tag 2010-03-14T00:00:00+00:00 2010-03-14T00:00:00+00:00 tag:None,2010-03-14:/tag/baz.html <p>This article overrides the listening of the articles under the <em>baz</em> tag.</p>
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/feeds/yeah.atom.xml b/pelican/tests/output/basic/feeds/yeah.atom.xml
new file mode 100644
index 00000000..6f871915
--- /dev/null
+++ b/pelican/tests/output/basic/feeds/yeah.atom.xml
@@ -0,0 +1,16 @@
+
+A Pelican Blog - yeah / 2013-11-17T23:29:00+00:00 This is a super article ! 2010-12-02T10:14:00+00:00 2013-11-17T23:29:00+00:00 Alexis Métaireau tag:None,2010-12-02:/this-is-a-super-article.html <p class="first last">Multi-line metadata should be supported
+as well as <strong>inline markup</strong>.</p>
+ <p>Some content here !</p>
+<div class="section" id="this-is-a-simple-title">
+<h2>This is a simple title</h2>
+<p>And here comes the cool <a class="reference external" href="http://books.couchdb.org/relax/design-documents/views">stuff</a>.</p>
+<img alt="alternate text" src="/pictures/Sushi.jpg" style="width: 600px; height: 450px;" />
+<img alt="alternate text" src="/pictures/Sushi_Macro.jpg" style="width: 600px; height: 450px;" />
+<pre class="literal-block">
+>>> from ipdb import set_trace
+>>> set_trace()
+</pre>
+<p>→ And now try with some utf8 hell: ééé</p>
+</div>
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/filename_metadata-example.html b/pelican/tests/output/basic/filename_metadata-example.html
new file mode 100644
index 00000000..6a6ee389
--- /dev/null
+++ b/pelican/tests/output/basic/filename_metadata-example.html
@@ -0,0 +1,66 @@
+
+
+
+
+
+ FILENAME_METADATA example
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Fri 30 November 2012
+
+
+In misc .
+
+ Some cool stuff!
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/index.html b/pelican/tests/output/basic/index.html
new file mode 100644
index 00000000..f215dc44
--- /dev/null
+++ b/pelican/tests/output/basic/index.html
@@ -0,0 +1,274 @@
+
+
+
+
+
+ A Pelican Blog
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Fri 30 November 2012
+
+
+In misc .
+
+ Some cool stuff!
+
+
+
+ Other articles
+
+
+
+
+
+
+
+
+
+ Published: Wed 29 February 2012
+
+
+In misc .
+tags: foo bar baz
Translations:
+ fr
+
+ This is some article, in english
+
+
read more
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Thu 17 February 2011
+
+
+In cat1 .
+
+ Article 1
+
+
read more
+
+
+
+
+
+
+
+
+
+ Published: Thu 17 February 2011
+
+
+In cat1 .
+
+ Article 2
+
+
read more
+
+
+
+
+
+
+
+
+
+ Published: Thu 17 February 2011
+
+
+In cat1 .
+
+ Article 3
+
+
read more
+
+
+
+
+
+
+
+
+
+ Published: Thu 02 December 2010
+
+
+
+ Updated: Sun 17 November 2013
+
+
+
+ By Alexis Métaireau
+
+In yeah .
+tags: foo bar foobar
+ Multi-line metadata should be supported
+as well as inline markup .
+
+
read more
+
+
+
+
+
+
+
+
+
+ Published: Wed 20 October 2010
+
+
+
+ By Alexis Métaireau
+
+In bar .
+tags: oh bar yeah
Translations:
+ fr
+
+
+
Why not ?
+
After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !
+
+
+
+
read more
+
+
+
+
+
+
+
+
+
+ Published: Fri 15 October 2010
+
+
+In misc .
+
+ Or completely awesome. Depends the needs.
+
a root-relative link to markdown-article
+a file-relative link to markdown-article
+
+
Testing sourcecode directive
+
formatter = self . options and VARIANTS [ self . options . keys ()[ 0 ]]
+
+
+
+
Testing another case
+
This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will …
+
read more
+
+
+
+
+
+
+
+
+
+ Published: Sun 14 March 2010
+
+
+In misc .
+
+ This article overrides the listening of the articles under the baz tag.
+
+
read more
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/oh-yeah-fr.html b/pelican/tests/output/basic/oh-yeah-fr.html
new file mode 100644
index 00000000..58b933aa
--- /dev/null
+++ b/pelican/tests/output/basic/oh-yeah-fr.html
@@ -0,0 +1,70 @@
+
+
+
+
+
+ Trop bien !
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Wed 20 October 2010
+
+
+In misc .
+Translations:
+ en
+
+ Et voila du contenu en français
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/oh-yeah.html b/pelican/tests/output/basic/oh-yeah.html
new file mode 100644
index 00000000..1b8077f8
--- /dev/null
+++ b/pelican/tests/output/basic/oh-yeah.html
@@ -0,0 +1,78 @@
+
+
+
+
+
+ Oh yeah !
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Wed 20 October 2010
+
+
+
+ By Alexis Métaireau
+
+In bar .
+tags: oh bar yeah
Translations:
+ fr
+
+
+
Why not ?
+
After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !
+
+
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/override/index.html b/pelican/tests/output/basic/override/index.html
new file mode 100644
index 00000000..c3613174
--- /dev/null
+++ b/pelican/tests/output/basic/override/index.html
@@ -0,0 +1,50 @@
+
+
+
+
+
+ Override url/save_as
+
+
+
+
+
+
+
+ Override url/save_as
+
+ Test page which overrides save_as and url so that this page will be generated
+at a custom location.
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/pages/this-is-a-test-hidden-page.html b/pelican/tests/output/basic/pages/this-is-a-test-hidden-page.html
new file mode 100644
index 00000000..21fabf62
--- /dev/null
+++ b/pelican/tests/output/basic/pages/this-is-a-test-hidden-page.html
@@ -0,0 +1,50 @@
+
+
+
+
+
+ This is a test hidden page
+
+
+
+
+
+
+
+ This is a test hidden page
+
+ This is great for things like error(404) pages
+Anyone can see this page but it's not linked to anywhere!
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/pages/this-is-a-test-page.html b/pelican/tests/output/basic/pages/this-is-a-test-page.html
new file mode 100644
index 00000000..3970e3ff
--- /dev/null
+++ b/pelican/tests/output/basic/pages/this-is-a-test-page.html
@@ -0,0 +1,51 @@
+
+
+
+
+
+ This is a test page
+
+
+
+
+
+
+
+ This is a test page
+
+ Just an image.
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/pictures/Fat_Cat.jpg b/pelican/tests/output/basic/pictures/Fat_Cat.jpg
new file mode 100644
index 00000000..d8a96d35
Binary files /dev/null and b/pelican/tests/output/basic/pictures/Fat_Cat.jpg differ
diff --git a/pelican/tests/output/basic/pictures/Sushi.jpg b/pelican/tests/output/basic/pictures/Sushi.jpg
new file mode 100644
index 00000000..e49e5f0a
Binary files /dev/null and b/pelican/tests/output/basic/pictures/Sushi.jpg differ
diff --git a/pelican/tests/output/basic/pictures/Sushi_Macro.jpg b/pelican/tests/output/basic/pictures/Sushi_Macro.jpg
new file mode 100644
index 00000000..21f935a1
Binary files /dev/null and b/pelican/tests/output/basic/pictures/Sushi_Macro.jpg differ
diff --git a/pelican/tests/output/basic/second-article-fr.html b/pelican/tests/output/basic/second-article-fr.html
new file mode 100644
index 00000000..87b95cc4
--- /dev/null
+++ b/pelican/tests/output/basic/second-article-fr.html
@@ -0,0 +1,70 @@
+
+
+
+
+
+ Deuxième article
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Wed 29 February 2012
+
+
+In misc .
+tags: foo bar baz
Translations:
+ en
+
+ Ceci est un article, en français.
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/second-article.html b/pelican/tests/output/basic/second-article.html
new file mode 100644
index 00000000..3e0c5296
--- /dev/null
+++ b/pelican/tests/output/basic/second-article.html
@@ -0,0 +1,70 @@
+
+
+
+
+
+ Second article
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Wed 29 February 2012
+
+
+In misc .
+tags: foo bar baz
Translations:
+ fr
+
+ This is some article, in english
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/tag/bar.html b/pelican/tests/output/basic/tag/bar.html
new file mode 100644
index 00000000..ead02cd8
--- /dev/null
+++ b/pelican/tests/output/basic/tag/bar.html
@@ -0,0 +1,123 @@
+
+
+
+
+
+ A Pelican Blog - bar
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Wed 29 February 2012
+
+
+In misc .
+tags: foo bar baz
Translations:
+ fr
+
+ This is some article, in english
+
+
+
+ Other articles
+
+
+
+
+
+
+
+
+
+ Published: Thu 02 December 2010
+
+
+
+ Updated: Sun 17 November 2013
+
+
+
+ By Alexis Métaireau
+
+In yeah .
+tags: foo bar foobar
+ Multi-line metadata should be supported
+as well as inline markup .
+
+
read more
+
+
+
+
+
+
+
+
+
+ Published: Wed 20 October 2010
+
+
+
+ By Alexis Métaireau
+
+In bar .
+tags: oh bar yeah
Translations:
+ fr
+
+
+
Why not ?
+
After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !
+
+
+
+
read more
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/tag/baz.html b/pelican/tests/output/basic/tag/baz.html
new file mode 100644
index 00000000..c891121e
--- /dev/null
+++ b/pelican/tests/output/basic/tag/baz.html
@@ -0,0 +1,66 @@
+
+
+
+
+
+ The baz tag
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Sun 14 March 2010
+
+
+In misc .
+
+ This article overrides the listening of the articles under the baz tag.
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/tag/foo.html b/pelican/tests/output/basic/tag/foo.html
new file mode 100644
index 00000000..3a00a6ab
--- /dev/null
+++ b/pelican/tests/output/basic/tag/foo.html
@@ -0,0 +1,93 @@
+
+
+
+
+
+ A Pelican Blog - foo
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Wed 29 February 2012
+
+
+In misc .
+tags: foo bar baz
Translations:
+ fr
+
+ This is some article, in english
+
+
+
+ Other articles
+
+
+
+
+
+
+
+
+
+ Published: Thu 02 December 2010
+
+
+
+ Updated: Sun 17 November 2013
+
+
+
+ By Alexis Métaireau
+
+In yeah .
+tags: foo bar foobar
+ Multi-line metadata should be supported
+as well as inline markup .
+
+
read more
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/tag/foobar.html b/pelican/tests/output/basic/tag/foobar.html
new file mode 100644
index 00000000..5418f872
--- /dev/null
+++ b/pelican/tests/output/basic/tag/foobar.html
@@ -0,0 +1,75 @@
+
+
+
+
+
+ A Pelican Blog - foobar
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Thu 02 December 2010
+
+
+
+ Updated: Sun 17 November 2013
+
+
+
+ By Alexis Métaireau
+
+In yeah .
+tags: foo bar foobar
+ Some content here !
+
+
This is a simple title
+
And here comes the cool stuff .
+
+
+
+>>> from ipdb import set_trace
+>>> set_trace()
+
+
→ And now try with some utf8 hell: ééé
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/tag/oh.html b/pelican/tests/output/basic/tag/oh.html
new file mode 100644
index 00000000..c2de8b5b
--- /dev/null
+++ b/pelican/tests/output/basic/tag/oh.html
@@ -0,0 +1,49 @@
+
+
+
+
+
+ Oh Oh Oh
+
+
+
+
+
+
+
+ Oh Oh Oh
+
+ This page overrides the listening of the articles under the oh tag.
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/tag/yeah.html b/pelican/tests/output/basic/tag/yeah.html
new file mode 100644
index 00000000..88660359
--- /dev/null
+++ b/pelican/tests/output/basic/tag/yeah.html
@@ -0,0 +1,67 @@
+
+
+
+
+
+ A Pelican Blog - yeah
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Wed 20 October 2010
+
+
+
+ By Alexis Métaireau
+
+In bar .
+tags: oh bar yeah
Translations:
+ fr
+
+
+
Why not ?
+
After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/tags.html b/pelican/tests/output/basic/tags.html
new file mode 100644
index 00000000..3f88ce9e
--- /dev/null
+++ b/pelican/tests/output/basic/tags.html
@@ -0,0 +1,56 @@
+
+
+
+
+
+ A Pelican Blog - Tags
+
+
+
+
+
+
+
+
+ Tags for A Pelican Blog
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/theme/css/fonts.css b/pelican/tests/output/basic/theme/css/fonts.css
new file mode 100644
index 00000000..56015076
--- /dev/null
+++ b/pelican/tests/output/basic/theme/css/fonts.css
@@ -0,0 +1,12 @@
+@font-face {
+ font-family: 'Yanone Kaffeesatz';
+ font-style: normal;
+ font-weight: 400;
+ src:
+ local('Yanone Kaffeesatz Regular'),
+ local('YanoneKaffeesatz-Regular'),
+ /* from https://fonts.gstatic.com/s/yanonekaffeesatz/v8/YDAoLskQQ5MOAgvHUQCcLRTHiN2BPBirwIkMLKUspj4.woff */
+ url('../fonts/Yanone_Kaffeesatz_400.woff') format('woff'),
+ /* from https://fonts.gstatic.com/s/yanonekaffeesatz/v8/YDAoLskQQ5MOAgvHUQCcLfGwxTS8d1Q9KiDNCMKLFUM.woff2 */
+ url('../fonts/Yanone_Kaffeesatz_400.woff2') format('woff2');
+}
diff --git a/pelican/tests/output/basic/theme/css/main.css b/pelican/tests/output/basic/theme/css/main.css
new file mode 100644
index 00000000..63f5adc0
--- /dev/null
+++ b/pelican/tests/output/basic/theme/css/main.css
@@ -0,0 +1,466 @@
+/*
+ Name: Smashing HTML5
+ Date: July 2009
+ Description: Sample layout for HTML5 and CSS3 goodness.
+ Version: 1.0
+ License: MIT
+ Licensed by: Smashing Media GmbH
+ Original author: Enrique Ramírez
+*/
+
+/* Imports */
+@import url("reset.css");
+@import url("pygment.css");
+@import url("typogrify.css");
+@import url("fonts.css");
+
+/***** Global *****/
+/* Body */
+body {
+ background: #F5F4EF;
+ color: #000305;
+ font-size: 87.5%; /* Base font size: 14px */
+ font-family: 'Trebuchet MS', Trebuchet, 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif;
+ line-height: 1.429;
+ margin: 0;
+ padding: 0;
+ text-align: left;
+}
+
+/* Headings */
+h1 {font-size: 2em }
+h2 {font-size: 1.571em} /* 22px */
+h3 {font-size: 1.429em} /* 20px */
+h4 {font-size: 1.286em} /* 18px */
+h5 {font-size: 1.143em} /* 16px */
+h6 {font-size: 1em} /* 14px */
+
+h1, h2, h3, h4, h5, h6 {
+ font-weight: 400;
+ line-height: 1.1;
+ margin-bottom: .8em;
+ font-family: 'Yanone Kaffeesatz', arial, serif;
+}
+
+h3, h4, h5, h6 { margin-top: .8em; }
+
+hr { border: 2px solid #EEEEEE; }
+
+/* Anchors */
+a {outline: 0;}
+a img {border: 0px; text-decoration: none;}
+a:link, a:visited {
+ color: #C74350;
+ padding: 0 1px;
+ text-decoration: underline;
+}
+a:hover, a:active {
+ background-color: #C74350;
+ color: #fff;
+ text-decoration: none;
+ text-shadow: 1px 1px 1px #333;
+}
+
+h1 a:hover {
+ background-color: inherit
+}
+
+/* Paragraphs */
+div.line-block,
+p { margin-top: 1em;
+ margin-bottom: 1em;}
+
+strong, b {font-weight: bold;}
+em, i {font-style: italic;}
+
+/* Lists */
+ul {
+ list-style: outside disc;
+ margin: 0em 0 0 1.5em;
+}
+
+ol {
+ list-style: outside decimal;
+ margin: 0em 0 0 1.5em;
+}
+
+li { margin-top: 0.5em;
+ margin-bottom: 1em; }
+
+.post-info {
+ float:right;
+ margin:10px;
+ padding:5px;
+}
+
+.post-info p{
+ margin-top: 1px;
+ margin-bottom: 1px;
+}
+
+.readmore { float: right }
+
+dl {margin: 0 0 1.5em 0;}
+dt {font-weight: bold;}
+dd {margin-left: 1.5em;}
+
+pre{background-color: rgb(238, 238, 238); padding: 10px; margin: 10px; overflow: auto;}
+
+/* Quotes */
+blockquote {
+ margin: 20px;
+ font-style: italic;
+}
+cite {}
+
+q {}
+
+div.note {
+ float: right;
+ margin: 5px;
+ font-size: 85%;
+ max-width: 300px;
+}
+
+/* Tables */
+table {margin: .5em auto 1.5em auto; width: 98%;}
+
+ /* Thead */
+ thead th {padding: .5em .4em; text-align: left;}
+ thead td {}
+
+ /* Tbody */
+ tbody td {padding: .5em .4em;}
+ tbody th {}
+
+ tbody .alt td {}
+ tbody .alt th {}
+
+ /* Tfoot */
+ tfoot th {}
+ tfoot td {}
+
+/* HTML5 tags */
+header, section, footer,
+aside, nav, article, figure {
+ display: block;
+}
+
+/***** Layout *****/
+.body {clear: both; margin: 0 auto; width: 800px;}
+img.right, figure.right, div.figure.align-right {
+ float: right;
+ margin: 0 0 2em 2em;
+}
+img.left, figure.left, div.figure.align-left {
+ float: left;
+ margin: 0 2em 2em 0;
+}
+
+/* .rst support */
+div.figure img, figure img { /* to fill figure exactly */
+ width: 100%;
+}
+div.figure p.caption, figure p.caption { /* margin provided by figure */
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+/*
+ Header
+*****************/
+#banner {
+ margin: 0 auto;
+ padding: 2.5em 0 0 0;
+}
+
+ /* Banner */
+ #banner h1 {font-size: 3.571em; line-height: 0;}
+ #banner h1 a:link, #banner h1 a:visited {
+ color: #000305;
+ display: block;
+ font-weight: bold;
+ margin: 0 0 .6em .2em;
+ text-decoration: none;
+ }
+ #banner h1 a:hover, #banner h1 a:active {
+ background: none;
+ color: #C74350;
+ text-shadow: none;
+ }
+
+ #banner h1 strong {font-size: 0.36em; font-weight: normal;}
+
+ /* Main Nav */
+ #banner nav {
+ background: #000305;
+ font-size: 1.143em;
+ height: 40px;
+ line-height: 30px;
+ margin: 0 auto 2em auto;
+ padding: 0;
+ text-align: center;
+ width: 800px;
+
+ border-radius: 5px;
+ -moz-border-radius: 5px;
+ -webkit-border-radius: 5px;
+ }
+
+ #banner nav ul {list-style: none; margin: 0 auto; width: 800px;}
+ #banner nav li {float: left; display: inline; margin: 0;}
+
+ #banner nav a:link, #banner nav a:visited {
+ color: #fff;
+ display: inline-block;
+ height: 30px;
+ padding: 5px 1.5em;
+ text-decoration: none;
+ }
+ #banner nav a:hover, #banner nav a:active,
+ #banner nav .active a:link, #banner nav .active a:visited {
+ background: #C74451;
+ color: #fff;
+ text-shadow: none !important;
+ }
+
+ #banner nav li:first-child a {
+ border-top-left-radius: 5px;
+ -moz-border-radius-topleft: 5px;
+ -webkit-border-top-left-radius: 5px;
+
+ border-bottom-left-radius: 5px;
+ -moz-border-radius-bottomleft: 5px;
+ -webkit-border-bottom-left-radius: 5px;
+ }
+
+/*
+ Featured
+*****************/
+#featured {
+ background: #fff;
+ margin-bottom: 2em;
+ overflow: hidden;
+ padding: 20px;
+ width: 760px;
+
+ border-radius: 10px;
+ -moz-border-radius: 10px;
+ -webkit-border-radius: 10px;
+}
+
+#featured figure {
+ border: 2px solid #eee;
+ float: right;
+ margin: 0.786em 2em 0 5em;
+ width: 248px;
+}
+#featured figure img {display: block; float: right;}
+
+#featured h2 {color: #C74451; font-size: 1.714em; margin-bottom: 0.333em;}
+#featured h3 {font-size: 1.429em; margin-bottom: .5em;}
+
+#featured h3 a:link, #featured h3 a:visited {color: #000305; text-decoration: none;}
+#featured h3 a:hover, #featured h3 a:active {color: #fff;}
+
+/*
+ Body
+*****************/
+#content {
+ background: #fff;
+ margin-bottom: 2em;
+ overflow: hidden;
+ padding: 20px 20px;
+ width: 760px;
+
+ border-radius: 10px;
+ -moz-border-radius: 10px;
+ -webkit-border-radius: 10px;
+}
+
+/*
+ Extras
+*****************/
+#extras {margin: 0 auto 3em auto; overflow: hidden;}
+
+#extras ul {list-style: none; margin: 0;}
+#extras li {border-bottom: 1px solid #fff;}
+#extras h2 {
+ color: #C74350;
+ font-size: 1.429em;
+ margin-bottom: .25em;
+ padding: 0 3px;
+}
+
+#extras a:link, #extras a:visited {
+ color: #444;
+ display: block;
+ border-bottom: 1px solid #F4E3E3;
+ text-decoration: none;
+ padding: .3em .25em;
+}
+
+#extras a:hover, #extras a:active {color: #fff;}
+
+ /* Blogroll */
+ #extras .blogroll {
+ float: left;
+ width: 615px;
+ }
+
+ #extras .blogroll li {float: left; margin: 0 20px 0 0; width: 185px;}
+
+ /* Social */
+ #extras .social {
+ float: right;
+ width: 175px;
+ }
+
+ #extras div[class='social'] a {
+ background-repeat: no-repeat;
+ background-position: 3px 6px;
+ padding-left: 25px;
+ }
+
+ /* Icons */
+ .social a[href*='about.me'] {background-image: url('../images/icons/aboutme.png');}
+ .social a[href*='bitbucket.org'] {background-image: url('../images/icons/bitbucket.png');}
+ .social a[href*='delicious.com'] {background-image: url('../images/icons/delicious.png');}
+ .social a[href*='facebook.com'] {background-image: url('../images/icons/facebook.png');}
+ .social a[href*='gitorious.org'] {background-image: url('../images/icons/gitorious.png');}
+ .social a[href*='github.com'],
+ .social a[href*='git.io'] {
+ background-image: url('../images/icons/github.png');
+ background-size: 16px 16px;
+ }
+ .social a[href*='gittip.com'] {background-image: url('../images/icons/gittip.png');}
+ .social a[href*='plus.google.com'] {background-image: url('../images/icons/google-plus.png');}
+ .social a[href*='groups.google.com'] {background-image: url('../images/icons/google-groups.png');}
+ .social a[href*='news.ycombinator.com'],
+ .social a[href*='hackernewsers.com'] {background-image: url('../images/icons/hackernews.png');}
+ .social a[href*='last.fm'], .social a[href*='lastfm.'] {background-image: url('../images/icons/lastfm.png');}
+ .social a[href*='linkedin.com'] {background-image: url('../images/icons/linkedin.png');}
+ .social a[href*='reddit.com'] {background-image: url('../images/icons/reddit.png');}
+ .social a[type$='atom+xml'], .social a[type$='rss+xml'] {background-image: url('../images/icons/rss.png');}
+ .social a[href*='slideshare.net'] {background-image: url('../images/icons/slideshare.png');}
+ .social a[href*='speakerdeck.com'] {background-image: url('../images/icons/speakerdeck.png');}
+ .social a[href*='stackoverflow.com'] {background-image: url('../images/icons/stackoverflow.png');}
+ .social a[href*='twitter.com'] {background-image: url('../images/icons/twitter.png');}
+ .social a[href*='vimeo.com'] {background-image: url('../images/icons/vimeo.png');}
+ .social a[href*='youtube.com'] {background-image: url('../images/icons/youtube.png');}
+
+/*
+ About
+*****************/
+#about {
+ background: #fff;
+ font-style: normal;
+ margin-bottom: 2em;
+ overflow: hidden;
+ padding: 20px;
+ text-align: left;
+ width: 760px;
+
+ border-radius: 10px;
+ -moz-border-radius: 10px;
+ -webkit-border-radius: 10px;
+}
+
+#about .primary {float: left; width: 165px;}
+#about .primary strong {color: #C64350; display: block; font-size: 1.286em;}
+#about .photo {float: left; margin: 5px 20px;}
+
+#about .url:link, #about .url:visited {text-decoration: none;}
+
+#about .bio {float: right; width: 500px;}
+
+/*
+ Footer
+*****************/
+#contentinfo {padding-bottom: 2em; text-align: right;}
+
+/***** Sections *****/
+/* Blog */
+.hentry {
+ display: block;
+ clear: both;
+ border-bottom: 1px solid #eee;
+ padding: 1.5em 0;
+}
+li:last-child .hentry, #content > .hentry {border: 0; margin: 0;}
+#content > .hentry {padding: 1em 0;}
+.hentry img{display : none ;}
+.entry-title {font-size: 3em; margin-bottom: 10px; margin-top: 0;}
+.entry-title a:link, .entry-title a:visited {text-decoration: none; color: #333;}
+.entry-title a:visited {background-color: #fff;}
+
+.hentry .post-info * {font-style: normal;}
+
+ /* Content */
+ .hentry footer {margin-bottom: 2em;}
+ .hentry footer address {display: inline;}
+ #posts-list footer address {display: block;}
+
+ /* Blog Index */
+ #posts-list {list-style: none; margin: 0;}
+ #posts-list .hentry {padding-left: 10px; position: relative;}
+
+ #posts-list footer {
+ left: 10px;
+ position: relative;
+ float: left;
+ top: 0.5em;
+ width: 190px;
+ }
+
+ /* About the Author */
+ #about-author {
+ background: #f9f9f9;
+ clear: both;
+ font-style: normal;
+ margin: 2em 0;
+ padding: 10px 20px 15px 20px;
+
+ border-radius: 5px;
+ -moz-border-radius: 5px;
+ -webkit-border-radius: 5px;
+ }
+
+ #about-author strong {
+ color: #C64350;
+ clear: both;
+ display: block;
+ font-size: 1.429em;
+ }
+
+ #about-author .photo {border: 1px solid #ddd; float: left; margin: 5px 1em 0 0;}
+
+ /* Comments */
+ #comments-list {list-style: none; margin: 0 1em;}
+ #comments-list blockquote {
+ background: #f8f8f8;
+ clear: both;
+ font-style: normal;
+ margin: 0;
+ padding: 15px 20px;
+
+ border-radius: 5px;
+ -moz-border-radius: 5px;
+ -webkit-border-radius: 5px;
+ }
+ #comments-list footer {color: #888; padding: .5em 1em 0 0; text-align: right;}
+
+ #comments-list li:nth-child(2n) blockquote {background: #F5f5f5;}
+
+ /* Add a Comment */
+ #add-comment label {clear: left; float: left; text-align: left; width: 150px;}
+ #add-comment input[type='text'],
+ #add-comment input[type='email'],
+ #add-comment input[type='url'] {float: left; width: 200px;}
+
+ #add-comment textarea {float: left; height: 150px; width: 495px;}
+
+ #add-comment p.req {clear: both; margin: 0 .5em 1em 0; text-align: right;}
+
+ #add-comment input[type='submit'] {float: right; margin: 0 .5em;}
+ #add-comment * {margin-bottom: .5em;}
diff --git a/pelican/tests/output/basic/theme/css/pygment.css b/pelican/tests/output/basic/theme/css/pygment.css
new file mode 100644
index 00000000..fdd056f6
--- /dev/null
+++ b/pelican/tests/output/basic/theme/css/pygment.css
@@ -0,0 +1,205 @@
+.hll {
+background-color:#eee;
+}
+.c {
+color:#408090;
+font-style:italic;
+}
+.err {
+border:1px solid #FF0000;
+}
+.k {
+color:#007020;
+font-weight:bold;
+}
+.o {
+color:#666666;
+}
+.cm {
+color:#408090;
+font-style:italic;
+}
+.cp {
+color:#007020;
+}
+.c1 {
+color:#408090;
+font-style:italic;
+}
+.cs {
+background-color:#FFF0F0;
+color:#408090;
+}
+.gd {
+color:#A00000;
+}
+.ge {
+font-style:italic;
+}
+.gr {
+color:#FF0000;
+}
+.gh {
+color:#000080;
+font-weight:bold;
+}
+.gi {
+color:#00A000;
+}
+.go {
+color:#303030;
+}
+.gp {
+color:#C65D09;
+font-weight:bold;
+}
+.gs {
+font-weight:bold;
+}
+.gu {
+color:#800080;
+font-weight:bold;
+}
+.gt {
+color:#0040D0;
+}
+.kc {
+color:#007020;
+font-weight:bold;
+}
+.kd {
+color:#007020;
+font-weight:bold;
+}
+.kn {
+color:#007020;
+font-weight:bold;
+}
+.kp {
+color:#007020;
+}
+.kr {
+color:#007020;
+font-weight:bold;
+}
+.kt {
+color:#902000;
+}
+.m {
+color:#208050;
+}
+.s {
+color:#4070A0;
+}
+.na {
+color:#4070A0;
+}
+.nb {
+color:#007020;
+}
+.nc {
+color:#0E84B5;
+font-weight:bold;
+}
+.no {
+color:#60ADD5;
+}
+.nd {
+color:#555555;
+font-weight:bold;
+}
+.ni {
+color:#D55537;
+font-weight:bold;
+}
+.ne {
+color:#007020;
+}
+.nf {
+color:#06287E;
+}
+.nl {
+color:#002070;
+font-weight:bold;
+}
+.nn {
+color:#0E84B5;
+font-weight:bold;
+}
+.nt {
+color:#062873;
+font-weight:bold;
+}
+.nv {
+color:#BB60D5;
+}
+.ow {
+color:#007020;
+font-weight:bold;
+}
+.w {
+color:#BBBBBB;
+}
+.mf {
+color:#208050;
+}
+.mh {
+color:#208050;
+}
+.mi {
+color:#208050;
+}
+.mo {
+color:#208050;
+}
+.sb {
+color:#4070A0;
+}
+.sc {
+color:#4070A0;
+}
+.sd {
+color:#4070A0;
+font-style:italic;
+}
+.s2 {
+color:#4070A0;
+}
+.se {
+color:#4070A0;
+font-weight:bold;
+}
+.sh {
+color:#4070A0;
+}
+.si {
+color:#70A0D0;
+font-style:italic;
+}
+.sx {
+color:#C65D09;
+}
+.sr {
+color:#235388;
+}
+.s1 {
+color:#4070A0;
+}
+.ss {
+color:#517918;
+}
+.bp {
+color:#007020;
+}
+.vc {
+color:#BB60D5;
+}
+.vg {
+color:#BB60D5;
+}
+.vi {
+color:#BB60D5;
+}
+.il {
+color:#208050;
+}
diff --git a/pelican/tests/output/basic/theme/css/reset.css b/pelican/tests/output/basic/theme/css/reset.css
new file mode 100644
index 00000000..c88e6196
--- /dev/null
+++ b/pelican/tests/output/basic/theme/css/reset.css
@@ -0,0 +1,52 @@
+/*
+ Name: Reset Stylesheet
+ Description: Resets browser's default CSS
+ Author: Eric Meyer
+ Author URI: https://meyerweb.com/eric/tools/css/reset/
+*/
+
+/* v1.0 | 20080212 */
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, font, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td {
+ background: transparent;
+ border: 0;
+ font-size: 100%;
+ margin: 0;
+ outline: 0;
+ padding: 0;
+ vertical-align: baseline;
+}
+
+body {line-height: 1;}
+
+ol, ul {list-style: none;}
+
+blockquote, q {quotes: none;}
+
+blockquote:before, blockquote:after,
+q:before, q:after {
+ content: '';
+ content: none;
+}
+
+/* remember to define focus styles! */
+:focus {
+ outline: 0;
+}
+
+/* remember to highlight inserts somehow! */
+ins {text-decoration: none;}
+del {text-decoration: line-through;}
+
+/* tables still need 'cellspacing="0"' in the markup */
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
\ No newline at end of file
diff --git a/pelican/tests/output/basic/theme/css/typogrify.css b/pelican/tests/output/basic/theme/css/typogrify.css
new file mode 100644
index 00000000..3bae4976
--- /dev/null
+++ b/pelican/tests/output/basic/theme/css/typogrify.css
@@ -0,0 +1,3 @@
+.caps {font-size:.92em;}
+.amp {color:#666; font-size:1.05em;font-family:"Warnock Pro", "Goudy Old Style","Palatino","Book Antiqua",serif; font-style:italic;}
+.dquo {margin-left:-.38em;}
diff --git a/pelican/tests/output/basic/theme/css/wide.css b/pelican/tests/output/basic/theme/css/wide.css
new file mode 100644
index 00000000..88fd59ce
--- /dev/null
+++ b/pelican/tests/output/basic/theme/css/wide.css
@@ -0,0 +1,48 @@
+@import url("main.css");
+
+body {
+ font:1.3em/1.3 "Hoefler Text","Georgia",Georgia,serif,sans-serif;
+}
+
+.post-info{
+ display: none;
+}
+
+#banner nav {
+ display: none;
+ -moz-border-radius: 0px;
+ margin-bottom: 20px;
+ overflow: hidden;
+ font-size: 1em;
+ background: #F5F4EF;
+}
+
+#banner nav ul{
+ padding-right: 50px;
+}
+
+#banner nav li{
+ float: right;
+ color: #000;
+}
+
+#banner nav li a {
+ color: #000;
+}
+
+#banner h1 {
+ margin-bottom: -18px;
+}
+
+#featured, #extras {
+ padding: 50px;
+}
+
+#featured {
+ padding-top: 20px;
+}
+
+#extras {
+ padding-top: 0px;
+ padding-bottom: 0px;
+}
diff --git a/pelican/tests/output/basic/theme/fonts/Yanone_Kaffeesatz_400.eot b/pelican/tests/output/basic/theme/fonts/Yanone_Kaffeesatz_400.eot
new file mode 100644
index 00000000..b3b90dbc
Binary files /dev/null and b/pelican/tests/output/basic/theme/fonts/Yanone_Kaffeesatz_400.eot differ
diff --git a/pelican/tests/output/basic/theme/fonts/Yanone_Kaffeesatz_400.svg b/pelican/tests/output/basic/theme/fonts/Yanone_Kaffeesatz_400.svg
new file mode 100644
index 00000000..a69669b5
--- /dev/null
+++ b/pelican/tests/output/basic/theme/fonts/Yanone_Kaffeesatz_400.svg
@@ -0,0 +1,407 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pelican/tests/output/basic/theme/fonts/Yanone_Kaffeesatz_400.ttf b/pelican/tests/output/basic/theme/fonts/Yanone_Kaffeesatz_400.ttf
new file mode 100644
index 00000000..6f4feb02
Binary files /dev/null and b/pelican/tests/output/basic/theme/fonts/Yanone_Kaffeesatz_400.ttf differ
diff --git a/pelican/tests/output/basic/theme/fonts/Yanone_Kaffeesatz_400.woff b/pelican/tests/output/basic/theme/fonts/Yanone_Kaffeesatz_400.woff
new file mode 100644
index 00000000..ddccf765
Binary files /dev/null and b/pelican/tests/output/basic/theme/fonts/Yanone_Kaffeesatz_400.woff differ
diff --git a/pelican/tests/output/basic/theme/fonts/Yanone_Kaffeesatz_400.woff2 b/pelican/tests/output/basic/theme/fonts/Yanone_Kaffeesatz_400.woff2
new file mode 100644
index 00000000..7b18f7ea
Binary files /dev/null and b/pelican/tests/output/basic/theme/fonts/Yanone_Kaffeesatz_400.woff2 differ
diff --git a/pelican/tests/output/basic/theme/fonts/font.css b/pelican/tests/output/basic/theme/fonts/font.css
new file mode 100644
index 00000000..a9eb87e6
--- /dev/null
+++ b/pelican/tests/output/basic/theme/fonts/font.css
@@ -0,0 +1,12 @@
+@font-face {
+ font-family: 'Yanone Kaffeesatz';
+ font-style: normal;
+ font-weight: 400;
+ src:
+ local('Yanone Kaffeesatz Regular'),
+ local('YanoneKaffeesatz-Regular'),
+ /* from https://fonts.gstatic.com/s/yanonekaffeesatz/v8/YDAoLskQQ5MOAgvHUQCcLRTHiN2BPBirwIkMLKUspj4.woff */
+ url('Yanone_Kaffeesatz_400.woff') format('woff'),
+ /* from https://fonts.gstatic.com/s/yanonekaffeesatz/v8/YDAoLskQQ5MOAgvHUQCcLfGwxTS8d1Q9KiDNCMKLFUM.woff2 */
+ url('Yanone_Kaffeesatz_400.woff2') format('woff2');
+}
diff --git a/pelican/tests/output/basic/theme/images/icons/aboutme.png b/pelican/tests/output/basic/theme/images/icons/aboutme.png
new file mode 100644
index 00000000..600110f2
Binary files /dev/null and b/pelican/tests/output/basic/theme/images/icons/aboutme.png differ
diff --git a/pelican/tests/output/basic/theme/images/icons/bitbucket.png b/pelican/tests/output/basic/theme/images/icons/bitbucket.png
new file mode 100644
index 00000000..277a7dfb
Binary files /dev/null and b/pelican/tests/output/basic/theme/images/icons/bitbucket.png differ
diff --git a/pelican/tests/output/basic/theme/images/icons/delicious.png b/pelican/tests/output/basic/theme/images/icons/delicious.png
new file mode 100644
index 00000000..34868c5c
Binary files /dev/null and b/pelican/tests/output/basic/theme/images/icons/delicious.png differ
diff --git a/pelican/tests/output/basic/theme/images/icons/facebook.png b/pelican/tests/output/basic/theme/images/icons/facebook.png
new file mode 100644
index 00000000..1d8a4327
Binary files /dev/null and b/pelican/tests/output/basic/theme/images/icons/facebook.png differ
diff --git a/pelican/tests/output/basic/theme/images/icons/github.png b/pelican/tests/output/basic/theme/images/icons/github.png
new file mode 100644
index 00000000..5d9109de
Binary files /dev/null and b/pelican/tests/output/basic/theme/images/icons/github.png differ
diff --git a/pelican/tests/output/basic/theme/images/icons/gitorious.png b/pelican/tests/output/basic/theme/images/icons/gitorious.png
new file mode 100644
index 00000000..a6705d0f
Binary files /dev/null and b/pelican/tests/output/basic/theme/images/icons/gitorious.png differ
diff --git a/pelican/tests/output/basic/theme/images/icons/gittip.png b/pelican/tests/output/basic/theme/images/icons/gittip.png
new file mode 100644
index 00000000..b9f67aaa
Binary files /dev/null and b/pelican/tests/output/basic/theme/images/icons/gittip.png differ
diff --git a/pelican/tests/output/basic/theme/images/icons/google-groups.png b/pelican/tests/output/basic/theme/images/icons/google-groups.png
new file mode 100644
index 00000000..bbd0a0fd
Binary files /dev/null and b/pelican/tests/output/basic/theme/images/icons/google-groups.png differ
diff --git a/pelican/tests/output/basic/theme/images/icons/google-plus.png b/pelican/tests/output/basic/theme/images/icons/google-plus.png
new file mode 100644
index 00000000..f8553d45
Binary files /dev/null and b/pelican/tests/output/basic/theme/images/icons/google-plus.png differ
diff --git a/pelican/tests/output/basic/theme/images/icons/hackernews.png b/pelican/tests/output/basic/theme/images/icons/hackernews.png
new file mode 100644
index 00000000..8e05e3ee
Binary files /dev/null and b/pelican/tests/output/basic/theme/images/icons/hackernews.png differ
diff --git a/pelican/tests/output/basic/theme/images/icons/lastfm.png b/pelican/tests/output/basic/theme/images/icons/lastfm.png
new file mode 100644
index 00000000..2eedd2da
Binary files /dev/null and b/pelican/tests/output/basic/theme/images/icons/lastfm.png differ
diff --git a/pelican/tests/output/basic/theme/images/icons/linkedin.png b/pelican/tests/output/basic/theme/images/icons/linkedin.png
new file mode 100644
index 00000000..06a88016
Binary files /dev/null and b/pelican/tests/output/basic/theme/images/icons/linkedin.png differ
diff --git a/pelican/tests/output/basic/theme/images/icons/reddit.png b/pelican/tests/output/basic/theme/images/icons/reddit.png
new file mode 100644
index 00000000..d826d3e7
Binary files /dev/null and b/pelican/tests/output/basic/theme/images/icons/reddit.png differ
diff --git a/pelican/tests/output/basic/theme/images/icons/rss.png b/pelican/tests/output/basic/theme/images/icons/rss.png
new file mode 100644
index 00000000..12448f53
Binary files /dev/null and b/pelican/tests/output/basic/theme/images/icons/rss.png differ
diff --git a/pelican/tests/output/basic/theme/images/icons/slideshare.png b/pelican/tests/output/basic/theme/images/icons/slideshare.png
new file mode 100644
index 00000000..9cbe8588
Binary files /dev/null and b/pelican/tests/output/basic/theme/images/icons/slideshare.png differ
diff --git a/pelican/tests/output/basic/theme/images/icons/speakerdeck.png b/pelican/tests/output/basic/theme/images/icons/speakerdeck.png
new file mode 100644
index 00000000..7281ec48
Binary files /dev/null and b/pelican/tests/output/basic/theme/images/icons/speakerdeck.png differ
diff --git a/pelican/tests/output/basic/theme/images/icons/stackoverflow.png b/pelican/tests/output/basic/theme/images/icons/stackoverflow.png
new file mode 100644
index 00000000..3c6862e9
Binary files /dev/null and b/pelican/tests/output/basic/theme/images/icons/stackoverflow.png differ
diff --git a/pelican/tests/output/basic/theme/images/icons/twitter.png b/pelican/tests/output/basic/theme/images/icons/twitter.png
new file mode 100644
index 00000000..cef1cef9
Binary files /dev/null and b/pelican/tests/output/basic/theme/images/icons/twitter.png differ
diff --git a/pelican/tests/output/basic/theme/images/icons/vimeo.png b/pelican/tests/output/basic/theme/images/icons/vimeo.png
new file mode 100644
index 00000000..4b9d7212
Binary files /dev/null and b/pelican/tests/output/basic/theme/images/icons/vimeo.png differ
diff --git a/pelican/tests/output/basic/theme/images/icons/youtube.png b/pelican/tests/output/basic/theme/images/icons/youtube.png
new file mode 100644
index 00000000..e334e68e
Binary files /dev/null and b/pelican/tests/output/basic/theme/images/icons/youtube.png differ
diff --git a/pelican/tests/output/basic/this-is-a-super-article.html b/pelican/tests/output/basic/this-is-a-super-article.html
new file mode 100644
index 00000000..bd1472e4
--- /dev/null
+++ b/pelican/tests/output/basic/this-is-a-super-article.html
@@ -0,0 +1,84 @@
+
+
+
+
+
+ This is a super article !
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Thu 02 December 2010
+
+
+
+ Updated: Sun 17 November 2013
+
+
+
+ By Alexis Métaireau
+
+In yeah .
+tags: foo bar foobar
+ Some content here !
+
+
This is a simple title
+
And here comes the cool stuff .
+
+
+
+>>> from ipdb import set_trace
+>>> set_trace()
+
+
→ And now try with some utf8 hell: ééé
+
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/basic/unbelievable.html b/pelican/tests/output/basic/unbelievable.html
new file mode 100644
index 00000000..e0836d70
--- /dev/null
+++ b/pelican/tests/output/basic/unbelievable.html
@@ -0,0 +1,98 @@
+
+
+
+
+
+ Unbelievable !
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Fri 15 October 2010
+
+
+In misc .
+
+ Or completely awesome. Depends the needs.
+
a root-relative link to markdown-article
+a file-relative link to markdown-article
+
+
Testing sourcecode directive
+
formatter = self . options and VARIANTS [ self . options . keys ()[ 0 ]]
+
+
+
+
Testing another case
+
This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will have nothing in default.
+
formatter = self . options and VARIANTS [ self . options . keys ()[ 0 ]]
+
+
Lovely.
+
+
+
Testing more sourcecode directives
+
8 def run ( self ): self . assert_has_content () 10 try : lexer = get_lexer_by_name ( self . arguments [ 0 ]) 12 except ValueError : # no lexer found - use the text one instead of an exception 14 lexer = TextLexer () 16 if ( 'linenos' in self . options and self . options [ 'linenos' ] not in ( 'table' , 'inline' )): 18 self . options [ 'linenos' ] = 'table' 20 for flag in ( 'nowrap' , 'nobackground' , 'anchorlinenos' ): if flag in self . options : 22 self . options [ flag ] = True 24 # noclasses should already default to False, but just in case... formatter = HtmlFormatter ( noclasses = False , ** self . options ) 26 parsed = highlight ( ' \n ' . join ( self . content ), lexer , formatter ) return [ nodes . raw ( '' , parsed , format = 'html' )]
+
Lovely.
+
+
+
Testing even more sourcecode directives
+
formatter = self . options and VARIANTS [ self . options . keys ()[ 0 ]]
+
Lovely.
+
+
+
Testing overriding config defaults
+
Even if the default is line numbers, we can override it here
+
formatter = self . options and VARIANTS [ self . options . keys ()[ 0 ]]
+
+
Lovely.
+
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/a-markdown-powered-article.html b/pelican/tests/output/custom/a-markdown-powered-article.html
new file mode 100644
index 00000000..00aa18e8
--- /dev/null
+++ b/pelican/tests/output/custom/a-markdown-powered-article.html
@@ -0,0 +1,113 @@
+
+
+
+
+
+ A markdown powered article
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/archives.html b/pelican/tests/output/custom/archives.html
new file mode 100644
index 00000000..1e9a8377
--- /dev/null
+++ b/pelican/tests/output/custom/archives.html
@@ -0,0 +1,97 @@
+
+
+
+
+
+ Alexis' log
+
+
+
+
+
+
+
+
+
+
+
+Archives for Alexis' log
+
+
+ Fri 30 November 2012
+ FILENAME_METADATA example
+ Wed 29 February 2012
+ Second article
+ Wed 20 April 2011
+ A markdown powered article
+ Thu 17 February 2011
+ Article 1
+ Thu 17 February 2011
+ Article 2
+ Thu 17 February 2011
+ Article 3
+ Thu 02 December 2010
+ This is a super article !
+ Wed 20 October 2010
+ Oh yeah !
+ Fri 15 October 2010
+ Unbelievable !
+ Sun 14 March 2010
+ The baz tag
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/article-1.html b/pelican/tests/output/custom/article-1.html
new file mode 100644
index 00000000..cc15ba5c
--- /dev/null
+++ b/pelican/tests/output/custom/article-1.html
@@ -0,0 +1,112 @@
+
+
+
+
+
+ Article 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Thu 17 February 2011
+
+
+
+ By Alexis Métaireau
+
+In cat1 .
+
+ Article 1
+
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/article-2.html b/pelican/tests/output/custom/article-2.html
new file mode 100644
index 00000000..cc331a4d
--- /dev/null
+++ b/pelican/tests/output/custom/article-2.html
@@ -0,0 +1,112 @@
+
+
+
+
+
+ Article 2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Thu 17 February 2011
+
+
+
+ By Alexis Métaireau
+
+In cat1 .
+
+ Article 2
+
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/article-3.html b/pelican/tests/output/custom/article-3.html
new file mode 100644
index 00000000..8b180180
--- /dev/null
+++ b/pelican/tests/output/custom/article-3.html
@@ -0,0 +1,112 @@
+
+
+
+
+
+ Article 3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Thu 17 February 2011
+
+
+
+ By Alexis Métaireau
+
+In cat1 .
+
+ Article 3
+
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/author/alexis-metaireau.html b/pelican/tests/output/custom/author/alexis-metaireau.html
new file mode 100644
index 00000000..7a277e2c
--- /dev/null
+++ b/pelican/tests/output/custom/author/alexis-metaireau.html
@@ -0,0 +1,171 @@
+
+
+
+
+
+ Alexis' log - Alexis Métaireau
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Fri 30 November 2012
+
+
+
+ By Alexis Métaireau
+
+In misc .
+
+ Some cool stuff!
+There are comments .
+
+
+ Other articles
+
+
+
+
+
+
+
+
+
+ Published: Wed 29 February 2012
+
+
+
+ By Alexis Métaireau
+
+In misc .
+tags: foo bar baz
Translations:
+ fr
+
+ This is some article, in english
+
+
read more
+
There are comments .
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Thu 17 February 2011
+
+
+
+ By Alexis Métaireau
+
+In cat1 .
+
+ Article 1
+
+
read more
+
There are comments .
+
+
+
+ Page 1 / 3
+ »
+ ⇉
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
diff --git a/pelican/tests/output/custom/author/alexis-metaireau2.html b/pelican/tests/output/custom/author/alexis-metaireau2.html
new file mode 100644
index 00000000..a5588090
--- /dev/null
+++ b/pelican/tests/output/custom/author/alexis-metaireau2.html
@@ -0,0 +1,186 @@
+
+
+
+
+
+ Alexis' log - Alexis Métaireau
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Thu 17 February 2011
+
+
+
+ By Alexis Métaireau
+
+In cat1 .
+
+ Article 2
+
+
read more
+
There are comments .
+
+
+
+
+
+
+
+
+ Published: Thu 17 February 2011
+
+
+
+ By Alexis Métaireau
+
+In cat1 .
+
+ Article 3
+
+
read more
+
There are comments .
+
+
+
+
+
+
+
+
+ Published: Thu 02 December 2010
+
+
+
+ Updated: Sun 17 November 2013
+
+
+
+ By Alexis Métaireau
+
+In yeah .
+tags: foo bar foobar
+ Multi-line metadata should be supported
+as well as inline markup .
+
+
read more
+
There are comments .
+
+
+
+
+
+
+
+
+ Published: Wed 20 October 2010
+
+
+
+ By Alexis Métaireau
+
+In bar .
+tags: oh bar yeah
Translations:
+ fr
+
+
+
Why not ?
+
After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !
+
+
+
+
read more
+
There are comments .
+
+
+
+ ⇇
+ «
+ Page 2 / 3
+ »
+ ⇉
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
diff --git a/pelican/tests/output/custom/author/alexis-metaireau3.html b/pelican/tests/output/custom/author/alexis-metaireau3.html
new file mode 100644
index 00000000..095b39b6
--- /dev/null
+++ b/pelican/tests/output/custom/author/alexis-metaireau3.html
@@ -0,0 +1,136 @@
+
+
+
+
+
+ Alexis' log - Alexis Métaireau
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Fri 15 October 2010
+
+
+
+ By Alexis Métaireau
+
+In misc .
+
+ Or completely awesome. Depends the needs.
+
a root-relative link to markdown-article
+a file-relative link to markdown-article
+
+
Testing sourcecode directive
+
formatter = self . options and VARIANTS [ self . options . keys ()[ 0 ]]
+
+
+
+
Testing another case
+
This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will …
+
read more
+
There are comments .
+
+
+
+
+
+
+
+
+ Published: Sun 14 March 2010
+
+
+
+ By Alexis Métaireau
+
+In misc .
+
+ This article overrides the listening of the articles under the baz tag.
+
+
read more
+
There are comments .
+
+
+
+ ⇇
+ «
+ Page 3 / 3
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
diff --git a/pelican/tests/output/custom/authors.html b/pelican/tests/output/custom/authors.html
new file mode 100644
index 00000000..6374c4a4
--- /dev/null
+++ b/pelican/tests/output/custom/authors.html
@@ -0,0 +1,79 @@
+
+
+
+
+
+ Alexis' log - Authors
+
+
+
+
+
+
+
+
+
+
+
+
+ Authors on Alexis' log
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/categories.html b/pelican/tests/output/custom/categories.html
new file mode 100644
index 00000000..393aef32
--- /dev/null
+++ b/pelican/tests/output/custom/categories.html
@@ -0,0 +1,78 @@
+
+
+
+
+
+ Alexis' log - Categories
+
+
+
+
+
+
+
+
+
+
+ Categories on Alexis' log
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/category/bar.html b/pelican/tests/output/custom/category/bar.html
new file mode 100644
index 00000000..4511bcb6
--- /dev/null
+++ b/pelican/tests/output/custom/category/bar.html
@@ -0,0 +1,95 @@
+
+
+
+
+
+ Alexis' log - bar
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Wed 20 October 2010
+
+
+
+ By Alexis Métaireau
+
+In bar .
+tags: oh bar yeah
Translations:
+ fr
+
+
+
Why not ?
+
After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !
+
+
+There are comments .
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/category/cat1.html b/pelican/tests/output/custom/category/cat1.html
new file mode 100644
index 00000000..dc5c0579
--- /dev/null
+++ b/pelican/tests/output/custom/category/cat1.html
@@ -0,0 +1,164 @@
+
+
+
+
+
+ Alexis' log - cat1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Wed 20 April 2011
+
+
+
+ By Alexis Métaireau
+
+In cat1 .
+
+ You're mutually oblivious.
+a root-relative link to unbelievable
+a file-relative link to unbelievable
There are comments .
+
+
+ Other articles
+
+
+
+
+
+
+
+
+
+ Published: Thu 17 February 2011
+
+
+
+ By Alexis Métaireau
+
+In cat1 .
+
+ Article 1
+
+
read more
+
There are comments .
+
+
+
+
+
+
+
+
+ Published: Thu 17 February 2011
+
+
+
+ By Alexis Métaireau
+
+In cat1 .
+
+ Article 2
+
+
read more
+
There are comments .
+
+
+
+
+
+
+
+
+ Published: Thu 17 February 2011
+
+
+
+ By Alexis Métaireau
+
+In cat1 .
+
+ Article 3
+
+
read more
+
There are comments .
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/category/misc.html b/pelican/tests/output/custom/category/misc.html
new file mode 100644
index 00000000..78b9837c
--- /dev/null
+++ b/pelican/tests/output/custom/category/misc.html
@@ -0,0 +1,175 @@
+
+
+
+
+
+ Alexis' log - misc
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Fri 30 November 2012
+
+
+
+ By Alexis Métaireau
+
+In misc .
+
+ Some cool stuff!
+There are comments .
+
+
+ Other articles
+
+
+
+
+
+
+
+
+
+ Published: Wed 29 February 2012
+
+
+
+ By Alexis Métaireau
+
+In misc .
+tags: foo bar baz
Translations:
+ fr
+
+ This is some article, in english
+
+
read more
+
There are comments .
+
+
+
+
+
+
+
+
+ Published: Fri 15 October 2010
+
+
+
+ By Alexis Métaireau
+
+In misc .
+
+ Or completely awesome. Depends the needs.
+
a root-relative link to markdown-article
+a file-relative link to markdown-article
+
+
Testing sourcecode directive
+
formatter = self . options and VARIANTS [ self . options . keys ()[ 0 ]]
+
+
+
+
Testing another case
+
This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will …
+
read more
+
There are comments .
+
+
+
+
+
+
+
+
+ Published: Sun 14 March 2010
+
+
+
+ By Alexis Métaireau
+
+In misc .
+
+ This article overrides the listening of the articles under the baz tag.
+
+
read more
+
There are comments .
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/category/yeah.html b/pelican/tests/output/custom/category/yeah.html
new file mode 100644
index 00000000..17e8103e
--- /dev/null
+++ b/pelican/tests/output/custom/category/yeah.html
@@ -0,0 +1,103 @@
+
+
+
+
+
+ Alexis' log - yeah
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Thu 02 December 2010
+
+
+
+ Updated: Sun 17 November 2013
+
+
+
+ By Alexis Métaireau
+
+In yeah .
+tags: foo bar foobar
+ Some content here !
+
+
This is a simple title
+
And here comes the cool stuff .
+
+
+
+>>> from ipdb import set_trace
+>>> set_trace()
+
+
→ And now try with some utf8 hell: ééé
+
+There are comments .
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/drafts/a-draft-article-without-date.html b/pelican/tests/output/custom/drafts/a-draft-article-without-date.html
new file mode 100644
index 00000000..03aecc56
--- /dev/null
+++ b/pelican/tests/output/custom/drafts/a-draft-article-without-date.html
@@ -0,0 +1,98 @@
+
+
+
+
+
+ A draft article without date
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Fri 02 March 2012
+
+
+
+ By Alexis Métaireau
+
+In misc .
+
+ This is a draft article, it should live under the /drafts/ folder and not be
+listed anywhere else.
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/drafts/a-draft-article.html b/pelican/tests/output/custom/drafts/a-draft-article.html
new file mode 100644
index 00000000..87be8bec
--- /dev/null
+++ b/pelican/tests/output/custom/drafts/a-draft-article.html
@@ -0,0 +1,98 @@
+
+
+
+
+
+ A draft article
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Sun 08 May 2011
+
+
+
+ By Alexis Métaireau
+
+In misc .
+
+ This is a draft article, it should live under the /drafts/ folder and not be
+listed anywhere else.
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/feeds/alexis-metaireau.atom.xml b/pelican/tests/output/custom/feeds/alexis-metaireau.atom.xml
new file mode 100644
index 00000000..19b706e8
--- /dev/null
+++ b/pelican/tests/output/custom/feeds/alexis-metaireau.atom.xml
@@ -0,0 +1,74 @@
+
+Alexis' log - Alexis Métaireau http://blog.notmyidea.org/ 2013-11-17T23:29:00+01:00 A personal blog. FILENAME_METADATA example 2012-11-30T00:00:00+01:00 2012-11-30T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2012-11-30:/filename_metadata-example.html <p>Some cool stuff!</p>
+ Second article 2012-02-29T00:00:00+01:00 2012-02-29T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2012-02-29:/second-article.html <p>This is some article, in english</p>
+ A markdown powered article 2011-04-20T00:00:00+02:00 2011-04-20T00:00:00+02:00 Alexis Métaireau tag:blog.notmyidea.org,2011-04-20:/a-markdown-powered-article.html <p>You're mutually oblivious.</p>
+<p><a href="http://blog.notmyidea.org/unbelievable.html">a root-relative link to unbelievable</a>
+<a href="http://blog.notmyidea.org/unbelievable.html">a file-relative link to unbelievable</a></p> Article 1 2011-02-17T00:00:00+01:00 2011-02-17T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2011-02-17:/article-1.html <p>Article 1</p>
+ Article 2 2011-02-17T00:00:00+01:00 2011-02-17T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2011-02-17:/article-2.html <p>Article 2</p>
+ Article 3 2011-02-17T00:00:00+01:00 2011-02-17T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2011-02-17:/article-3.html <p>Article 3</p>
+ This is a super article ! 2010-12-02T10:14:00+01:00 2013-11-17T23:29:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2010-12-02:/this-is-a-super-article.html <p class="first last">Multi-line metadata should be supported
+as well as <strong>inline markup</strong>.</p>
+ <p>Some content here !</p>
+<div class="section" id="this-is-a-simple-title">
+<h2>This is a simple title</h2>
+<p>And here comes the cool <a class="reference external" href="http://books.couchdb.org/relax/design-documents/views">stuff</a>.</p>
+<img alt="alternate text" src="http://blog.notmyidea.org/pictures/Sushi.jpg" style="width: 600px; height: 450px;" />
+<img alt="alternate text" src="http://blog.notmyidea.org/pictures/Sushi_Macro.jpg" style="width: 600px; height: 450px;" />
+<pre class="literal-block">
+>>> from ipdb import set_trace
+>>> set_trace()
+</pre>
+<p>→ And now try with some utf8 hell: ééé</p>
+</div>
+ Oh yeah ! 2010-10-20T10:14:00+02:00 2010-10-20T10:14:00+02:00 Alexis Métaireau tag:blog.notmyidea.org,2010-10-20:/oh-yeah.html <div class="section" id="why-not">
+<h2>Why not ?</h2>
+<p>After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !</p>
+<img alt="alternate text" src="http://blog.notmyidea.org/pictures/Sushi.jpg" style="width: 600px; height: 450px;" />
+</div>
+ Unbelievable ! 2010-10-15T20:30:00+02:00 2010-10-15T20:30:00+02:00 Alexis Métaireau tag:blog.notmyidea.org,2010-10-15:/unbelievable.html <p>Or completely awesome. Depends the needs.</p>
+<p><a class="reference external" href="http://blog.notmyidea.org/a-markdown-powered-article.html">a root-relative link to markdown-article</a>
+<a class="reference external" href="http://blog.notmyidea.org/a-markdown-powered-article.html">a file-relative link to markdown-article</a></p>
+<div class="section" id="testing-sourcecode-directive">
+<h2>Testing sourcecode directive</h2>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table></div>
+<div class="section" id="testing-another-case">
+<h2>Testing another case</h2>
+<p>This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will …</p></div> <p>Or completely awesome. Depends the needs.</p>
+<p><a class="reference external" href="http://blog.notmyidea.org/a-markdown-powered-article.html">a root-relative link to markdown-article</a>
+<a class="reference external" href="http://blog.notmyidea.org/a-markdown-powered-article.html">a file-relative link to markdown-article</a></p>
+<div class="section" id="testing-sourcecode-directive">
+<h2>Testing sourcecode directive</h2>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table></div>
+<div class="section" id="testing-another-case">
+<h2>Testing another case</h2>
+<p>This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will have nothing in default.</p>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table><p>Lovely.</p>
+</div>
+<div class="section" id="testing-more-sourcecode-directives">
+<h2>Testing more sourcecode directives</h2>
+<div class="highlight"><pre><span></span><span id="foo-8"><a name="foo-8"></a><span class="lineno special"> 8 </span><span class="testingk">def</span> <span class="testingnf">run</span><span class="testingp">(</span><span class="testingbp">self</span><span class="testingp">):</span><br></span><span id="foo-9"><a name="foo-9"></a><span class="lineno"> </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">assert_has_content</span><span class="testingp">()</span><br></span><span id="foo-10"><a name="foo-10"></a><span class="lineno special">10 </span> <span class="testingk">try</span><span class="testingp">:</span><br></span><span id="foo-11"><a name="foo-11"></a><span class="lineno"> </span> <span class="testingn">lexer</span> <span class="testingo">=</span> <span class="testingn">get_lexer_by_name</span><span class="testingp">(</span><span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">arguments</span><span class="testingp">[</span><span class="testingmi">0</span><span class="testingp">])</span><br></span><span id="foo-12"><a name="foo-12"></a><span class="lineno special">12 </span> <span class="testingk">except</span> <span class="testingne">ValueError</span><span class="testingp">:</span><br></span><span id="foo-13"><a name="foo-13"></a><span class="lineno"> </span> <span class="testingc1"># no lexer found - use the text one instead of an exception</span><br></span><span id="foo-14"><a name="foo-14"></a><span class="lineno special">14 </span> <span class="testingn">lexer</span> <span class="testingo">=</span> <span class="testingn">TextLexer</span><span class="testingp">()</span><br></span><span id="foo-15"><a name="foo-15"></a><span class="lineno"> </span><br></span><span id="foo-16"><a name="foo-16"></a><span class="lineno special">16 </span> <span class="testingk">if</span> <span class="testingp">(</span><span class="testings1">'linenos'</span> <span class="testingow">in</span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span> <span class="testingow">and</span><br></span><span id="foo-17"><a name="foo-17"></a><span class="lineno"> </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">[</span><span class="testings1">'linenos'</span><span class="testingp">]</span> <span class="testingow">not</span> <span class="testingow">in</span> <span class="testingp">(</span><span class="testings1">'table'</span><span class="testingp">,</span> <span class="testings1">'inline'</span><span class="testingp">)):</span><br></span><span id="foo-18"><a name="foo-18"></a><span class="lineno special">18 </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">[</span><span class="testings1">'linenos'</span><span class="testingp">]</span> <span class="testingo">=</span> <span class="testings1">'table'</span><br></span><span id="foo-19"><a name="foo-19"></a><span class="lineno"> </span><br></span><span id="foo-20"><a name="foo-20"></a><span class="lineno special">20 </span> <span class="testingk">for</span> <span class="testingn">flag</span> <span class="testingow">in</span> <span class="testingp">(</span><span class="testings1">'nowrap'</span><span class="testingp">,</span> <span class="testings1">'nobackground'</span><span class="testingp">,</span> <span class="testings1">'anchorlinenos'</span><span class="testingp">):</span><br></span><span id="foo-21"><a name="foo-21"></a><span class="lineno"> </span> <span class="testingk">if</span> <span class="testingn">flag</span> <span class="testingow">in</span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">:</span><br></span><span id="foo-22"><a name="foo-22"></a><span class="lineno special">22 </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">[</span><span class="testingn">flag</span><span class="testingp">]</span> <span class="testingo">=</span> <span class="testingkc">True</span><br></span><span id="foo-23"><a name="foo-23"></a><span class="lineno"> </span><br></span><span id="foo-24"><a name="foo-24"></a><span class="lineno special">24 </span> <span class="testingc1"># noclasses should already default to False, but just in case...</span><br></span><span id="foo-25"><a name="foo-25"></a><span class="lineno"> </span> <span class="testingn">formatter</span> <span class="testingo">=</span> <span class="testingn">HtmlFormatter</span><span class="testingp">(</span><span class="testingn">noclasses</span><span class="testingo">=</span><span class="testingkc">False</span><span class="testingp">,</span> <span class="testingo">**</span><span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">)</span><br></span><span id="foo-26"><a name="foo-26"></a><span class="lineno special">26 </span> <span class="testingn">parsed</span> <span class="testingo">=</span> <span class="testingn">highlight</span><span class="testingp">(</span><span class="testings1">'</span><span class="testingse">\n</span><span class="testings1">'</span><span class="testingo">.</span><span class="testingn">join</span><span class="testingp">(</span><span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">content</span><span class="testingp">),</span> <span class="testingn">lexer</span><span class="testingp">,</span> <span class="testingn">formatter</span><span class="testingp">)</span><br></span><span id="foo-27"><a name="foo-27"></a><span class="lineno"> </span> <span class="testingk">return</span> <span class="testingp">[</span><span class="testingn">nodes</span><span class="testingo">.</span><span class="testingn">raw</span><span class="testingp">(</span><span class="testings1">''</span><span class="testingp">,</span> <span class="testingn">parsed</span><span class="testingp">,</span> <span class="testingnb">format</span><span class="testingo">=</span><span class="testings1">'html'</span><span class="testingp">)]</span><br></span></pre></div>
+<p>Lovely.</p>
+</div>
+<div class="section" id="testing-even-more-sourcecode-directives">
+<h2>Testing even more sourcecode directives</h2>
+<span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+<p>Lovely.</p>
+</div>
+<div class="section" id="testing-overriding-config-defaults">
+<h2>Testing overriding config defaults</h2>
+<p>Even if the default is line numbers, we can override it here</p>
+<div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+<p>Lovely.</p>
+</div>
+ The baz tag 2010-03-14T00:00:00+01:00 2010-03-14T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2010-03-14:/tag/baz.html <p>This article overrides the listening of the articles under the <em>baz</em> tag.</p>
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/feeds/alexis-metaireau.rss.xml b/pelican/tests/output/custom/feeds/alexis-metaireau.rss.xml
new file mode 100644
index 00000000..e5042f26
--- /dev/null
+++ b/pelican/tests/output/custom/feeds/alexis-metaireau.rss.xml
@@ -0,0 +1,29 @@
+
+Alexis' log - Alexis Métaireau http://blog.notmyidea.org/A personal blog. Sun, 17 Nov 2013 23:29:00 +0100 FILENAME_METADATA example http://blog.notmyidea.org/filename_metadata-example.html<p>Some cool stuff!</p>
+ Alexis Métaireau Fri, 30 Nov 2012 00:00:00 +0100 tag:blog.notmyidea.org,2012-11-30:/filename_metadata-example.html misc Second article http://blog.notmyidea.org/second-article.html<p>This is some article, in english</p>
+ Alexis Métaireau Wed, 29 Feb 2012 00:00:00 +0100 tag:blog.notmyidea.org,2012-02-29:/second-article.html misc foo bar baz A markdown powered article http://blog.notmyidea.org/a-markdown-powered-article.html<p>You're mutually oblivious.</p>
+<p><a href="http://blog.notmyidea.org/unbelievable.html">a root-relative link to unbelievable</a>
+<a href="http://blog.notmyidea.org/unbelievable.html">a file-relative link to unbelievable</a></p> Alexis Métaireau Wed, 20 Apr 2011 00:00:00 +0200 tag:blog.notmyidea.org,2011-04-20:/a-markdown-powered-article.html cat1 Article 1 http://blog.notmyidea.org/article-1.html<p>Article 1</p>
+ Alexis Métaireau Thu, 17 Feb 2011 00:00:00 +0100 tag:blog.notmyidea.org,2011-02-17:/article-1.html cat1 Article 2 http://blog.notmyidea.org/article-2.html<p>Article 2</p>
+ Alexis Métaireau Thu, 17 Feb 2011 00:00:00 +0100 tag:blog.notmyidea.org,2011-02-17:/article-2.html cat1 Article 3 http://blog.notmyidea.org/article-3.html<p>Article 3</p>
+ Alexis Métaireau Thu, 17 Feb 2011 00:00:00 +0100 tag:blog.notmyidea.org,2011-02-17:/article-3.html cat1 This is a super article ! http://blog.notmyidea.org/this-is-a-super-article.html<p class="first last">Multi-line metadata should be supported
+as well as <strong>inline markup</strong>.</p>
+ Alexis Métaireau Thu, 02 Dec 2010 10:14:00 +0100 tag:blog.notmyidea.org,2010-12-02:/this-is-a-super-article.html yeah foo bar foobar Oh yeah ! http://blog.notmyidea.org/oh-yeah.html<div class="section" id="why-not">
+<h2>Why not ?</h2>
+<p>After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !</p>
+<img alt="alternate text" src="http://blog.notmyidea.org/pictures/Sushi.jpg" style="width: 600px; height: 450px;" />
+</div>
+ Alexis Métaireau Wed, 20 Oct 2010 10:14:00 +0200 tag:blog.notmyidea.org,2010-10-20:/oh-yeah.html bar oh bar yeah Unbelievable ! http://blog.notmyidea.org/unbelievable.html<p>Or completely awesome. Depends the needs.</p>
+<p><a class="reference external" href="http://blog.notmyidea.org/a-markdown-powered-article.html">a root-relative link to markdown-article</a>
+<a class="reference external" href="http://blog.notmyidea.org/a-markdown-powered-article.html">a file-relative link to markdown-article</a></p>
+<div class="section" id="testing-sourcecode-directive">
+<h2>Testing sourcecode directive</h2>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table></div>
+<div class="section" id="testing-another-case">
+<h2>Testing another case</h2>
+<p>This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will …</p></div> Alexis Métaireau Fri, 15 Oct 2010 20:30:00 +0200 tag:blog.notmyidea.org,2010-10-15:/unbelievable.html misc The baz tag http://blog.notmyidea.org/tag/baz.html<p>This article overrides the listening of the articles under the <em>baz</em> tag.</p>
+ Alexis Métaireau Sun, 14 Mar 2010 00:00:00 +0100 tag:blog.notmyidea.org,2010-03-14:/tag/baz.html misc
\ No newline at end of file
diff --git a/pelican/tests/output/custom/feeds/all-en.atom.xml b/pelican/tests/output/custom/feeds/all-en.atom.xml
new file mode 100644
index 00000000..15ec3903
--- /dev/null
+++ b/pelican/tests/output/custom/feeds/all-en.atom.xml
@@ -0,0 +1,74 @@
+
+Alexis' log http://blog.notmyidea.org/ 2013-11-17T23:29:00+01:00 A personal blog. FILENAME_METADATA example 2012-11-30T00:00:00+01:00 2012-11-30T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2012-11-30:/filename_metadata-example.html <p>Some cool stuff!</p>
+ Second article 2012-02-29T00:00:00+01:00 2012-02-29T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2012-02-29:/second-article.html <p>This is some article, in english</p>
+ A markdown powered article 2011-04-20T00:00:00+02:00 2011-04-20T00:00:00+02:00 Alexis Métaireau tag:blog.notmyidea.org,2011-04-20:/a-markdown-powered-article.html <p>You're mutually oblivious.</p>
+<p><a href="http://blog.notmyidea.org/unbelievable.html">a root-relative link to unbelievable</a>
+<a href="http://blog.notmyidea.org/unbelievable.html">a file-relative link to unbelievable</a></p> Article 1 2011-02-17T00:00:00+01:00 2011-02-17T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2011-02-17:/article-1.html <p>Article 1</p>
+ Article 2 2011-02-17T00:00:00+01:00 2011-02-17T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2011-02-17:/article-2.html <p>Article 2</p>
+ Article 3 2011-02-17T00:00:00+01:00 2011-02-17T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2011-02-17:/article-3.html <p>Article 3</p>
+ This is a super article ! 2010-12-02T10:14:00+01:00 2013-11-17T23:29:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2010-12-02:/this-is-a-super-article.html <p class="first last">Multi-line metadata should be supported
+as well as <strong>inline markup</strong>.</p>
+ <p>Some content here !</p>
+<div class="section" id="this-is-a-simple-title">
+<h2>This is a simple title</h2>
+<p>And here comes the cool <a class="reference external" href="http://books.couchdb.org/relax/design-documents/views">stuff</a>.</p>
+<img alt="alternate text" src="http://blog.notmyidea.org/pictures/Sushi.jpg" style="width: 600px; height: 450px;" />
+<img alt="alternate text" src="http://blog.notmyidea.org/pictures/Sushi_Macro.jpg" style="width: 600px; height: 450px;" />
+<pre class="literal-block">
+>>> from ipdb import set_trace
+>>> set_trace()
+</pre>
+<p>→ And now try with some utf8 hell: ééé</p>
+</div>
+ Oh yeah ! 2010-10-20T10:14:00+02:00 2010-10-20T10:14:00+02:00 Alexis Métaireau tag:blog.notmyidea.org,2010-10-20:/oh-yeah.html <div class="section" id="why-not">
+<h2>Why not ?</h2>
+<p>After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !</p>
+<img alt="alternate text" src="http://blog.notmyidea.org/pictures/Sushi.jpg" style="width: 600px; height: 450px;" />
+</div>
+ Unbelievable ! 2010-10-15T20:30:00+02:00 2010-10-15T20:30:00+02:00 Alexis Métaireau tag:blog.notmyidea.org,2010-10-15:/unbelievable.html <p>Or completely awesome. Depends the needs.</p>
+<p><a class="reference external" href="http://blog.notmyidea.org/a-markdown-powered-article.html">a root-relative link to markdown-article</a>
+<a class="reference external" href="http://blog.notmyidea.org/a-markdown-powered-article.html">a file-relative link to markdown-article</a></p>
+<div class="section" id="testing-sourcecode-directive">
+<h2>Testing sourcecode directive</h2>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table></div>
+<div class="section" id="testing-another-case">
+<h2>Testing another case</h2>
+<p>This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will …</p></div> <p>Or completely awesome. Depends the needs.</p>
+<p><a class="reference external" href="http://blog.notmyidea.org/a-markdown-powered-article.html">a root-relative link to markdown-article</a>
+<a class="reference external" href="http://blog.notmyidea.org/a-markdown-powered-article.html">a file-relative link to markdown-article</a></p>
+<div class="section" id="testing-sourcecode-directive">
+<h2>Testing sourcecode directive</h2>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table></div>
+<div class="section" id="testing-another-case">
+<h2>Testing another case</h2>
+<p>This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will have nothing in default.</p>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table><p>Lovely.</p>
+</div>
+<div class="section" id="testing-more-sourcecode-directives">
+<h2>Testing more sourcecode directives</h2>
+<div class="highlight"><pre><span></span><span id="foo-8"><a name="foo-8"></a><span class="lineno special"> 8 </span><span class="testingk">def</span> <span class="testingnf">run</span><span class="testingp">(</span><span class="testingbp">self</span><span class="testingp">):</span><br></span><span id="foo-9"><a name="foo-9"></a><span class="lineno"> </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">assert_has_content</span><span class="testingp">()</span><br></span><span id="foo-10"><a name="foo-10"></a><span class="lineno special">10 </span> <span class="testingk">try</span><span class="testingp">:</span><br></span><span id="foo-11"><a name="foo-11"></a><span class="lineno"> </span> <span class="testingn">lexer</span> <span class="testingo">=</span> <span class="testingn">get_lexer_by_name</span><span class="testingp">(</span><span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">arguments</span><span class="testingp">[</span><span class="testingmi">0</span><span class="testingp">])</span><br></span><span id="foo-12"><a name="foo-12"></a><span class="lineno special">12 </span> <span class="testingk">except</span> <span class="testingne">ValueError</span><span class="testingp">:</span><br></span><span id="foo-13"><a name="foo-13"></a><span class="lineno"> </span> <span class="testingc1"># no lexer found - use the text one instead of an exception</span><br></span><span id="foo-14"><a name="foo-14"></a><span class="lineno special">14 </span> <span class="testingn">lexer</span> <span class="testingo">=</span> <span class="testingn">TextLexer</span><span class="testingp">()</span><br></span><span id="foo-15"><a name="foo-15"></a><span class="lineno"> </span><br></span><span id="foo-16"><a name="foo-16"></a><span class="lineno special">16 </span> <span class="testingk">if</span> <span class="testingp">(</span><span class="testings1">'linenos'</span> <span class="testingow">in</span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span> <span class="testingow">and</span><br></span><span id="foo-17"><a name="foo-17"></a><span class="lineno"> </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">[</span><span class="testings1">'linenos'</span><span class="testingp">]</span> <span class="testingow">not</span> <span class="testingow">in</span> <span class="testingp">(</span><span class="testings1">'table'</span><span class="testingp">,</span> <span class="testings1">'inline'</span><span class="testingp">)):</span><br></span><span id="foo-18"><a name="foo-18"></a><span class="lineno special">18 </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">[</span><span class="testings1">'linenos'</span><span class="testingp">]</span> <span class="testingo">=</span> <span class="testings1">'table'</span><br></span><span id="foo-19"><a name="foo-19"></a><span class="lineno"> </span><br></span><span id="foo-20"><a name="foo-20"></a><span class="lineno special">20 </span> <span class="testingk">for</span> <span class="testingn">flag</span> <span class="testingow">in</span> <span class="testingp">(</span><span class="testings1">'nowrap'</span><span class="testingp">,</span> <span class="testings1">'nobackground'</span><span class="testingp">,</span> <span class="testings1">'anchorlinenos'</span><span class="testingp">):</span><br></span><span id="foo-21"><a name="foo-21"></a><span class="lineno"> </span> <span class="testingk">if</span> <span class="testingn">flag</span> <span class="testingow">in</span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">:</span><br></span><span id="foo-22"><a name="foo-22"></a><span class="lineno special">22 </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">[</span><span class="testingn">flag</span><span class="testingp">]</span> <span class="testingo">=</span> <span class="testingkc">True</span><br></span><span id="foo-23"><a name="foo-23"></a><span class="lineno"> </span><br></span><span id="foo-24"><a name="foo-24"></a><span class="lineno special">24 </span> <span class="testingc1"># noclasses should already default to False, but just in case...</span><br></span><span id="foo-25"><a name="foo-25"></a><span class="lineno"> </span> <span class="testingn">formatter</span> <span class="testingo">=</span> <span class="testingn">HtmlFormatter</span><span class="testingp">(</span><span class="testingn">noclasses</span><span class="testingo">=</span><span class="testingkc">False</span><span class="testingp">,</span> <span class="testingo">**</span><span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">)</span><br></span><span id="foo-26"><a name="foo-26"></a><span class="lineno special">26 </span> <span class="testingn">parsed</span> <span class="testingo">=</span> <span class="testingn">highlight</span><span class="testingp">(</span><span class="testings1">'</span><span class="testingse">\n</span><span class="testings1">'</span><span class="testingo">.</span><span class="testingn">join</span><span class="testingp">(</span><span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">content</span><span class="testingp">),</span> <span class="testingn">lexer</span><span class="testingp">,</span> <span class="testingn">formatter</span><span class="testingp">)</span><br></span><span id="foo-27"><a name="foo-27"></a><span class="lineno"> </span> <span class="testingk">return</span> <span class="testingp">[</span><span class="testingn">nodes</span><span class="testingo">.</span><span class="testingn">raw</span><span class="testingp">(</span><span class="testings1">''</span><span class="testingp">,</span> <span class="testingn">parsed</span><span class="testingp">,</span> <span class="testingnb">format</span><span class="testingo">=</span><span class="testings1">'html'</span><span class="testingp">)]</span><br></span></pre></div>
+<p>Lovely.</p>
+</div>
+<div class="section" id="testing-even-more-sourcecode-directives">
+<h2>Testing even more sourcecode directives</h2>
+<span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+<p>Lovely.</p>
+</div>
+<div class="section" id="testing-overriding-config-defaults">
+<h2>Testing overriding config defaults</h2>
+<p>Even if the default is line numbers, we can override it here</p>
+<div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+<p>Lovely.</p>
+</div>
+ The baz tag 2010-03-14T00:00:00+01:00 2010-03-14T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2010-03-14:/tag/baz.html <p>This article overrides the listening of the articles under the <em>baz</em> tag.</p>
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/feeds/all-fr.atom.xml b/pelican/tests/output/custom/feeds/all-fr.atom.xml
new file mode 100644
index 00000000..39565ca6
--- /dev/null
+++ b/pelican/tests/output/custom/feeds/all-fr.atom.xml
@@ -0,0 +1,4 @@
+
+Alexis' log http://blog.notmyidea.org/ 2012-02-29T00:00:00+01:00 A personal blog. Deuxième article 2012-02-29T00:00:00+01:00 2012-02-29T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2012-02-29:/second-article-fr.html <p>Ceci est un article, en français.</p>
+ Trop bien ! 2010-10-20T10:14:00+02:00 2010-10-20T10:14:00+02:00 Alexis Métaireau tag:blog.notmyidea.org,2010-10-20:/oh-yeah-fr.html <p>Et voila du contenu en français</p>
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/feeds/all.atom.xml b/pelican/tests/output/custom/feeds/all.atom.xml
new file mode 100644
index 00000000..cf214c5a
--- /dev/null
+++ b/pelican/tests/output/custom/feeds/all.atom.xml
@@ -0,0 +1,76 @@
+
+Alexis' log http://blog.notmyidea.org/ 2013-11-17T23:29:00+01:00 A personal blog. FILENAME_METADATA example 2012-11-30T00:00:00+01:00 2012-11-30T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2012-11-30:/filename_metadata-example.html <p>Some cool stuff!</p>
+ Second article 2012-02-29T00:00:00+01:00 2012-02-29T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2012-02-29:/second-article.html <p>This is some article, in english</p>
+ Deuxième article 2012-02-29T00:00:00+01:00 2012-02-29T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2012-02-29:/second-article-fr.html <p>Ceci est un article, en français.</p>
+ A markdown powered article 2011-04-20T00:00:00+02:00 2011-04-20T00:00:00+02:00 Alexis Métaireau tag:blog.notmyidea.org,2011-04-20:/a-markdown-powered-article.html <p>You're mutually oblivious.</p>
+<p><a href="http://blog.notmyidea.org/unbelievable.html">a root-relative link to unbelievable</a>
+<a href="http://blog.notmyidea.org/unbelievable.html">a file-relative link to unbelievable</a></p> Article 1 2011-02-17T00:00:00+01:00 2011-02-17T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2011-02-17:/article-1.html <p>Article 1</p>
+ Article 2 2011-02-17T00:00:00+01:00 2011-02-17T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2011-02-17:/article-2.html <p>Article 2</p>
+ Article 3 2011-02-17T00:00:00+01:00 2011-02-17T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2011-02-17:/article-3.html <p>Article 3</p>
+ This is a super article ! 2010-12-02T10:14:00+01:00 2013-11-17T23:29:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2010-12-02:/this-is-a-super-article.html <p class="first last">Multi-line metadata should be supported
+as well as <strong>inline markup</strong>.</p>
+ <p>Some content here !</p>
+<div class="section" id="this-is-a-simple-title">
+<h2>This is a simple title</h2>
+<p>And here comes the cool <a class="reference external" href="http://books.couchdb.org/relax/design-documents/views">stuff</a>.</p>
+<img alt="alternate text" src="http://blog.notmyidea.org/pictures/Sushi.jpg" style="width: 600px; height: 450px;" />
+<img alt="alternate text" src="http://blog.notmyidea.org/pictures/Sushi_Macro.jpg" style="width: 600px; height: 450px;" />
+<pre class="literal-block">
+>>> from ipdb import set_trace
+>>> set_trace()
+</pre>
+<p>→ And now try with some utf8 hell: ééé</p>
+</div>
+ Oh yeah ! 2010-10-20T10:14:00+02:00 2010-10-20T10:14:00+02:00 Alexis Métaireau tag:blog.notmyidea.org,2010-10-20:/oh-yeah.html <div class="section" id="why-not">
+<h2>Why not ?</h2>
+<p>After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !</p>
+<img alt="alternate text" src="http://blog.notmyidea.org/pictures/Sushi.jpg" style="width: 600px; height: 450px;" />
+</div>
+ Trop bien ! 2010-10-20T10:14:00+02:00 2010-10-20T10:14:00+02:00 Alexis Métaireau tag:blog.notmyidea.org,2010-10-20:/oh-yeah-fr.html <p>Et voila du contenu en français</p>
+ Unbelievable ! 2010-10-15T20:30:00+02:00 2010-10-15T20:30:00+02:00 Alexis Métaireau tag:blog.notmyidea.org,2010-10-15:/unbelievable.html <p>Or completely awesome. Depends the needs.</p>
+<p><a class="reference external" href="http://blog.notmyidea.org/a-markdown-powered-article.html">a root-relative link to markdown-article</a>
+<a class="reference external" href="http://blog.notmyidea.org/a-markdown-powered-article.html">a file-relative link to markdown-article</a></p>
+<div class="section" id="testing-sourcecode-directive">
+<h2>Testing sourcecode directive</h2>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table></div>
+<div class="section" id="testing-another-case">
+<h2>Testing another case</h2>
+<p>This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will …</p></div> <p>Or completely awesome. Depends the needs.</p>
+<p><a class="reference external" href="http://blog.notmyidea.org/a-markdown-powered-article.html">a root-relative link to markdown-article</a>
+<a class="reference external" href="http://blog.notmyidea.org/a-markdown-powered-article.html">a file-relative link to markdown-article</a></p>
+<div class="section" id="testing-sourcecode-directive">
+<h2>Testing sourcecode directive</h2>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table></div>
+<div class="section" id="testing-another-case">
+<h2>Testing another case</h2>
+<p>This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will have nothing in default.</p>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table><p>Lovely.</p>
+</div>
+<div class="section" id="testing-more-sourcecode-directives">
+<h2>Testing more sourcecode directives</h2>
+<div class="highlight"><pre><span></span><span id="foo-8"><a name="foo-8"></a><span class="lineno special"> 8 </span><span class="testingk">def</span> <span class="testingnf">run</span><span class="testingp">(</span><span class="testingbp">self</span><span class="testingp">):</span><br></span><span id="foo-9"><a name="foo-9"></a><span class="lineno"> </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">assert_has_content</span><span class="testingp">()</span><br></span><span id="foo-10"><a name="foo-10"></a><span class="lineno special">10 </span> <span class="testingk">try</span><span class="testingp">:</span><br></span><span id="foo-11"><a name="foo-11"></a><span class="lineno"> </span> <span class="testingn">lexer</span> <span class="testingo">=</span> <span class="testingn">get_lexer_by_name</span><span class="testingp">(</span><span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">arguments</span><span class="testingp">[</span><span class="testingmi">0</span><span class="testingp">])</span><br></span><span id="foo-12"><a name="foo-12"></a><span class="lineno special">12 </span> <span class="testingk">except</span> <span class="testingne">ValueError</span><span class="testingp">:</span><br></span><span id="foo-13"><a name="foo-13"></a><span class="lineno"> </span> <span class="testingc1"># no lexer found - use the text one instead of an exception</span><br></span><span id="foo-14"><a name="foo-14"></a><span class="lineno special">14 </span> <span class="testingn">lexer</span> <span class="testingo">=</span> <span class="testingn">TextLexer</span><span class="testingp">()</span><br></span><span id="foo-15"><a name="foo-15"></a><span class="lineno"> </span><br></span><span id="foo-16"><a name="foo-16"></a><span class="lineno special">16 </span> <span class="testingk">if</span> <span class="testingp">(</span><span class="testings1">'linenos'</span> <span class="testingow">in</span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span> <span class="testingow">and</span><br></span><span id="foo-17"><a name="foo-17"></a><span class="lineno"> </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">[</span><span class="testings1">'linenos'</span><span class="testingp">]</span> <span class="testingow">not</span> <span class="testingow">in</span> <span class="testingp">(</span><span class="testings1">'table'</span><span class="testingp">,</span> <span class="testings1">'inline'</span><span class="testingp">)):</span><br></span><span id="foo-18"><a name="foo-18"></a><span class="lineno special">18 </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">[</span><span class="testings1">'linenos'</span><span class="testingp">]</span> <span class="testingo">=</span> <span class="testings1">'table'</span><br></span><span id="foo-19"><a name="foo-19"></a><span class="lineno"> </span><br></span><span id="foo-20"><a name="foo-20"></a><span class="lineno special">20 </span> <span class="testingk">for</span> <span class="testingn">flag</span> <span class="testingow">in</span> <span class="testingp">(</span><span class="testings1">'nowrap'</span><span class="testingp">,</span> <span class="testings1">'nobackground'</span><span class="testingp">,</span> <span class="testings1">'anchorlinenos'</span><span class="testingp">):</span><br></span><span id="foo-21"><a name="foo-21"></a><span class="lineno"> </span> <span class="testingk">if</span> <span class="testingn">flag</span> <span class="testingow">in</span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">:</span><br></span><span id="foo-22"><a name="foo-22"></a><span class="lineno special">22 </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">[</span><span class="testingn">flag</span><span class="testingp">]</span> <span class="testingo">=</span> <span class="testingkc">True</span><br></span><span id="foo-23"><a name="foo-23"></a><span class="lineno"> </span><br></span><span id="foo-24"><a name="foo-24"></a><span class="lineno special">24 </span> <span class="testingc1"># noclasses should already default to False, but just in case...</span><br></span><span id="foo-25"><a name="foo-25"></a><span class="lineno"> </span> <span class="testingn">formatter</span> <span class="testingo">=</span> <span class="testingn">HtmlFormatter</span><span class="testingp">(</span><span class="testingn">noclasses</span><span class="testingo">=</span><span class="testingkc">False</span><span class="testingp">,</span> <span class="testingo">**</span><span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">)</span><br></span><span id="foo-26"><a name="foo-26"></a><span class="lineno special">26 </span> <span class="testingn">parsed</span> <span class="testingo">=</span> <span class="testingn">highlight</span><span class="testingp">(</span><span class="testings1">'</span><span class="testingse">\n</span><span class="testings1">'</span><span class="testingo">.</span><span class="testingn">join</span><span class="testingp">(</span><span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">content</span><span class="testingp">),</span> <span class="testingn">lexer</span><span class="testingp">,</span> <span class="testingn">formatter</span><span class="testingp">)</span><br></span><span id="foo-27"><a name="foo-27"></a><span class="lineno"> </span> <span class="testingk">return</span> <span class="testingp">[</span><span class="testingn">nodes</span><span class="testingo">.</span><span class="testingn">raw</span><span class="testingp">(</span><span class="testings1">''</span><span class="testingp">,</span> <span class="testingn">parsed</span><span class="testingp">,</span> <span class="testingnb">format</span><span class="testingo">=</span><span class="testings1">'html'</span><span class="testingp">)]</span><br></span></pre></div>
+<p>Lovely.</p>
+</div>
+<div class="section" id="testing-even-more-sourcecode-directives">
+<h2>Testing even more sourcecode directives</h2>
+<span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+<p>Lovely.</p>
+</div>
+<div class="section" id="testing-overriding-config-defaults">
+<h2>Testing overriding config defaults</h2>
+<p>Even if the default is line numbers, we can override it here</p>
+<div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+<p>Lovely.</p>
+</div>
+ The baz tag 2010-03-14T00:00:00+01:00 2010-03-14T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2010-03-14:/tag/baz.html <p>This article overrides the listening of the articles under the <em>baz</em> tag.</p>
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/feeds/all.rss.xml b/pelican/tests/output/custom/feeds/all.rss.xml
new file mode 100644
index 00000000..479f67ba
--- /dev/null
+++ b/pelican/tests/output/custom/feeds/all.rss.xml
@@ -0,0 +1,31 @@
+
+Alexis' log http://blog.notmyidea.org/A personal blog. Sun, 17 Nov 2013 23:29:00 +0100 FILENAME_METADATA example http://blog.notmyidea.org/filename_metadata-example.html<p>Some cool stuff!</p>
+ Alexis Métaireau Fri, 30 Nov 2012 00:00:00 +0100 tag:blog.notmyidea.org,2012-11-30:/filename_metadata-example.html misc Second article http://blog.notmyidea.org/second-article.html<p>This is some article, in english</p>
+ Alexis Métaireau Wed, 29 Feb 2012 00:00:00 +0100 tag:blog.notmyidea.org,2012-02-29:/second-article.html misc foo bar baz Deuxième article http://blog.notmyidea.org/second-article-fr.html<p>Ceci est un article, en français.</p>
+ Alexis Métaireau Wed, 29 Feb 2012 00:00:00 +0100 tag:blog.notmyidea.org,2012-02-29:/second-article-fr.html misc foo bar baz A markdown powered article http://blog.notmyidea.org/a-markdown-powered-article.html<p>You're mutually oblivious.</p>
+<p><a href="http://blog.notmyidea.org/unbelievable.html">a root-relative link to unbelievable</a>
+<a href="http://blog.notmyidea.org/unbelievable.html">a file-relative link to unbelievable</a></p> Alexis Métaireau Wed, 20 Apr 2011 00:00:00 +0200 tag:blog.notmyidea.org,2011-04-20:/a-markdown-powered-article.html cat1 Article 1 http://blog.notmyidea.org/article-1.html<p>Article 1</p>
+ Alexis Métaireau Thu, 17 Feb 2011 00:00:00 +0100 tag:blog.notmyidea.org,2011-02-17:/article-1.html cat1 Article 2 http://blog.notmyidea.org/article-2.html<p>Article 2</p>
+ Alexis Métaireau Thu, 17 Feb 2011 00:00:00 +0100 tag:blog.notmyidea.org,2011-02-17:/article-2.html cat1 Article 3 http://blog.notmyidea.org/article-3.html<p>Article 3</p>
+ Alexis Métaireau Thu, 17 Feb 2011 00:00:00 +0100 tag:blog.notmyidea.org,2011-02-17:/article-3.html cat1 This is a super article ! http://blog.notmyidea.org/this-is-a-super-article.html<p class="first last">Multi-line metadata should be supported
+as well as <strong>inline markup</strong>.</p>
+ Alexis Métaireau Thu, 02 Dec 2010 10:14:00 +0100 tag:blog.notmyidea.org,2010-12-02:/this-is-a-super-article.html yeah foo bar foobar Oh yeah ! http://blog.notmyidea.org/oh-yeah.html<div class="section" id="why-not">
+<h2>Why not ?</h2>
+<p>After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !</p>
+<img alt="alternate text" src="http://blog.notmyidea.org/pictures/Sushi.jpg" style="width: 600px; height: 450px;" />
+</div>
+ Alexis Métaireau Wed, 20 Oct 2010 10:14:00 +0200 tag:blog.notmyidea.org,2010-10-20:/oh-yeah.html bar oh bar yeah Trop bien ! http://blog.notmyidea.org/oh-yeah-fr.html<p>Et voila du contenu en français</p>
+ Alexis Métaireau Wed, 20 Oct 2010 10:14:00 +0200 tag:blog.notmyidea.org,2010-10-20:/oh-yeah-fr.html misc Unbelievable ! http://blog.notmyidea.org/unbelievable.html<p>Or completely awesome. Depends the needs.</p>
+<p><a class="reference external" href="http://blog.notmyidea.org/a-markdown-powered-article.html">a root-relative link to markdown-article</a>
+<a class="reference external" href="http://blog.notmyidea.org/a-markdown-powered-article.html">a file-relative link to markdown-article</a></p>
+<div class="section" id="testing-sourcecode-directive">
+<h2>Testing sourcecode directive</h2>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table></div>
+<div class="section" id="testing-another-case">
+<h2>Testing another case</h2>
+<p>This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will …</p></div> Alexis Métaireau Fri, 15 Oct 2010 20:30:00 +0200 tag:blog.notmyidea.org,2010-10-15:/unbelievable.html misc The baz tag http://blog.notmyidea.org/tag/baz.html<p>This article overrides the listening of the articles under the <em>baz</em> tag.</p>
+ Alexis Métaireau Sun, 14 Mar 2010 00:00:00 +0100 tag:blog.notmyidea.org,2010-03-14:/tag/baz.html misc
\ No newline at end of file
diff --git a/pelican/tests/output/custom/feeds/bar.atom.xml b/pelican/tests/output/custom/feeds/bar.atom.xml
new file mode 100644
index 00000000..002de037
--- /dev/null
+++ b/pelican/tests/output/custom/feeds/bar.atom.xml
@@ -0,0 +1,8 @@
+
+Alexis' log - bar http://blog.notmyidea.org/ 2010-10-20T10:14:00+02:00 A personal blog. Oh yeah ! 2010-10-20T10:14:00+02:00 2010-10-20T10:14:00+02:00 Alexis Métaireau tag:blog.notmyidea.org,2010-10-20:/oh-yeah.html <div class="section" id="why-not">
+<h2>Why not ?</h2>
+<p>After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !</p>
+<img alt="alternate text" src="http://blog.notmyidea.org/pictures/Sushi.jpg" style="width: 600px; height: 450px;" />
+</div>
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/feeds/bar.rss.xml b/pelican/tests/output/custom/feeds/bar.rss.xml
new file mode 100644
index 00000000..53035e71
--- /dev/null
+++ b/pelican/tests/output/custom/feeds/bar.rss.xml
@@ -0,0 +1,8 @@
+
+Alexis' log - bar http://blog.notmyidea.org/A personal blog. Wed, 20 Oct 2010 10:14:00 +0200 Oh yeah ! http://blog.notmyidea.org/oh-yeah.html<div class="section" id="why-not">
+<h2>Why not ?</h2>
+<p>After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !</p>
+<img alt="alternate text" src="http://blog.notmyidea.org/pictures/Sushi.jpg" style="width: 600px; height: 450px;" />
+</div>
+ Alexis Métaireau Wed, 20 Oct 2010 10:14:00 +0200 tag:blog.notmyidea.org,2010-10-20:/oh-yeah.html bar oh bar yeah
\ No newline at end of file
diff --git a/pelican/tests/output/custom/feeds/cat1.atom.xml b/pelican/tests/output/custom/feeds/cat1.atom.xml
new file mode 100644
index 00000000..e8ed355b
--- /dev/null
+++ b/pelican/tests/output/custom/feeds/cat1.atom.xml
@@ -0,0 +1,7 @@
+
+Alexis' log - cat1 http://blog.notmyidea.org/ 2011-04-20T00:00:00+02:00 A personal blog. A markdown powered article 2011-04-20T00:00:00+02:00 2011-04-20T00:00:00+02:00 Alexis Métaireau tag:blog.notmyidea.org,2011-04-20:/a-markdown-powered-article.html <p>You're mutually oblivious.</p>
+<p><a href="http://blog.notmyidea.org/unbelievable.html">a root-relative link to unbelievable</a>
+<a href="http://blog.notmyidea.org/unbelievable.html">a file-relative link to unbelievable</a></p> Article 1 2011-02-17T00:00:00+01:00 2011-02-17T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2011-02-17:/article-1.html <p>Article 1</p>
+ Article 2 2011-02-17T00:00:00+01:00 2011-02-17T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2011-02-17:/article-2.html <p>Article 2</p>
+ Article 3 2011-02-17T00:00:00+01:00 2011-02-17T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2011-02-17:/article-3.html <p>Article 3</p>
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/feeds/cat1.rss.xml b/pelican/tests/output/custom/feeds/cat1.rss.xml
new file mode 100644
index 00000000..9951b293
--- /dev/null
+++ b/pelican/tests/output/custom/feeds/cat1.rss.xml
@@ -0,0 +1,7 @@
+
+Alexis' log - cat1 http://blog.notmyidea.org/A personal blog. Wed, 20 Apr 2011 00:00:00 +0200 A markdown powered article http://blog.notmyidea.org/a-markdown-powered-article.html<p>You're mutually oblivious.</p>
+<p><a href="http://blog.notmyidea.org/unbelievable.html">a root-relative link to unbelievable</a>
+<a href="http://blog.notmyidea.org/unbelievable.html">a file-relative link to unbelievable</a></p> Alexis Métaireau Wed, 20 Apr 2011 00:00:00 +0200 tag:blog.notmyidea.org,2011-04-20:/a-markdown-powered-article.html cat1 Article 1 http://blog.notmyidea.org/article-1.html<p>Article 1</p>
+ Alexis Métaireau Thu, 17 Feb 2011 00:00:00 +0100 tag:blog.notmyidea.org,2011-02-17:/article-1.html cat1 Article 2 http://blog.notmyidea.org/article-2.html<p>Article 2</p>
+ Alexis Métaireau Thu, 17 Feb 2011 00:00:00 +0100 tag:blog.notmyidea.org,2011-02-17:/article-2.html cat1 Article 3 http://blog.notmyidea.org/article-3.html<p>Article 3</p>
+ Alexis Métaireau Thu, 17 Feb 2011 00:00:00 +0100 tag:blog.notmyidea.org,2011-02-17:/article-3.html cat1
\ No newline at end of file
diff --git a/pelican/tests/output/custom/feeds/misc.atom.xml b/pelican/tests/output/custom/feeds/misc.atom.xml
new file mode 100644
index 00000000..325e4c01
--- /dev/null
+++ b/pelican/tests/output/custom/feeds/misc.atom.xml
@@ -0,0 +1,49 @@
+
+Alexis' log - misc http://blog.notmyidea.org/ 2012-11-30T00:00:00+01:00 A personal blog. FILENAME_METADATA example 2012-11-30T00:00:00+01:00 2012-11-30T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2012-11-30:/filename_metadata-example.html <p>Some cool stuff!</p>
+ Second article 2012-02-29T00:00:00+01:00 2012-02-29T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2012-02-29:/second-article.html <p>This is some article, in english</p>
+ Unbelievable ! 2010-10-15T20:30:00+02:00 2010-10-15T20:30:00+02:00 Alexis Métaireau tag:blog.notmyidea.org,2010-10-15:/unbelievable.html <p>Or completely awesome. Depends the needs.</p>
+<p><a class="reference external" href="http://blog.notmyidea.org/a-markdown-powered-article.html">a root-relative link to markdown-article</a>
+<a class="reference external" href="http://blog.notmyidea.org/a-markdown-powered-article.html">a file-relative link to markdown-article</a></p>
+<div class="section" id="testing-sourcecode-directive">
+<h2>Testing sourcecode directive</h2>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table></div>
+<div class="section" id="testing-another-case">
+<h2>Testing another case</h2>
+<p>This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will …</p></div> <p>Or completely awesome. Depends the needs.</p>
+<p><a class="reference external" href="http://blog.notmyidea.org/a-markdown-powered-article.html">a root-relative link to markdown-article</a>
+<a class="reference external" href="http://blog.notmyidea.org/a-markdown-powered-article.html">a file-relative link to markdown-article</a></p>
+<div class="section" id="testing-sourcecode-directive">
+<h2>Testing sourcecode directive</h2>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table></div>
+<div class="section" id="testing-another-case">
+<h2>Testing another case</h2>
+<p>This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will have nothing in default.</p>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table><p>Lovely.</p>
+</div>
+<div class="section" id="testing-more-sourcecode-directives">
+<h2>Testing more sourcecode directives</h2>
+<div class="highlight"><pre><span></span><span id="foo-8"><a name="foo-8"></a><span class="lineno special"> 8 </span><span class="testingk">def</span> <span class="testingnf">run</span><span class="testingp">(</span><span class="testingbp">self</span><span class="testingp">):</span><br></span><span id="foo-9"><a name="foo-9"></a><span class="lineno"> </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">assert_has_content</span><span class="testingp">()</span><br></span><span id="foo-10"><a name="foo-10"></a><span class="lineno special">10 </span> <span class="testingk">try</span><span class="testingp">:</span><br></span><span id="foo-11"><a name="foo-11"></a><span class="lineno"> </span> <span class="testingn">lexer</span> <span class="testingo">=</span> <span class="testingn">get_lexer_by_name</span><span class="testingp">(</span><span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">arguments</span><span class="testingp">[</span><span class="testingmi">0</span><span class="testingp">])</span><br></span><span id="foo-12"><a name="foo-12"></a><span class="lineno special">12 </span> <span class="testingk">except</span> <span class="testingne">ValueError</span><span class="testingp">:</span><br></span><span id="foo-13"><a name="foo-13"></a><span class="lineno"> </span> <span class="testingc1"># no lexer found - use the text one instead of an exception</span><br></span><span id="foo-14"><a name="foo-14"></a><span class="lineno special">14 </span> <span class="testingn">lexer</span> <span class="testingo">=</span> <span class="testingn">TextLexer</span><span class="testingp">()</span><br></span><span id="foo-15"><a name="foo-15"></a><span class="lineno"> </span><br></span><span id="foo-16"><a name="foo-16"></a><span class="lineno special">16 </span> <span class="testingk">if</span> <span class="testingp">(</span><span class="testings1">'linenos'</span> <span class="testingow">in</span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span> <span class="testingow">and</span><br></span><span id="foo-17"><a name="foo-17"></a><span class="lineno"> </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">[</span><span class="testings1">'linenos'</span><span class="testingp">]</span> <span class="testingow">not</span> <span class="testingow">in</span> <span class="testingp">(</span><span class="testings1">'table'</span><span class="testingp">,</span> <span class="testings1">'inline'</span><span class="testingp">)):</span><br></span><span id="foo-18"><a name="foo-18"></a><span class="lineno special">18 </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">[</span><span class="testings1">'linenos'</span><span class="testingp">]</span> <span class="testingo">=</span> <span class="testings1">'table'</span><br></span><span id="foo-19"><a name="foo-19"></a><span class="lineno"> </span><br></span><span id="foo-20"><a name="foo-20"></a><span class="lineno special">20 </span> <span class="testingk">for</span> <span class="testingn">flag</span> <span class="testingow">in</span> <span class="testingp">(</span><span class="testings1">'nowrap'</span><span class="testingp">,</span> <span class="testings1">'nobackground'</span><span class="testingp">,</span> <span class="testings1">'anchorlinenos'</span><span class="testingp">):</span><br></span><span id="foo-21"><a name="foo-21"></a><span class="lineno"> </span> <span class="testingk">if</span> <span class="testingn">flag</span> <span class="testingow">in</span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">:</span><br></span><span id="foo-22"><a name="foo-22"></a><span class="lineno special">22 </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">[</span><span class="testingn">flag</span><span class="testingp">]</span> <span class="testingo">=</span> <span class="testingkc">True</span><br></span><span id="foo-23"><a name="foo-23"></a><span class="lineno"> </span><br></span><span id="foo-24"><a name="foo-24"></a><span class="lineno special">24 </span> <span class="testingc1"># noclasses should already default to False, but just in case...</span><br></span><span id="foo-25"><a name="foo-25"></a><span class="lineno"> </span> <span class="testingn">formatter</span> <span class="testingo">=</span> <span class="testingn">HtmlFormatter</span><span class="testingp">(</span><span class="testingn">noclasses</span><span class="testingo">=</span><span class="testingkc">False</span><span class="testingp">,</span> <span class="testingo">**</span><span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">)</span><br></span><span id="foo-26"><a name="foo-26"></a><span class="lineno special">26 </span> <span class="testingn">parsed</span> <span class="testingo">=</span> <span class="testingn">highlight</span><span class="testingp">(</span><span class="testings1">'</span><span class="testingse">\n</span><span class="testings1">'</span><span class="testingo">.</span><span class="testingn">join</span><span class="testingp">(</span><span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">content</span><span class="testingp">),</span> <span class="testingn">lexer</span><span class="testingp">,</span> <span class="testingn">formatter</span><span class="testingp">)</span><br></span><span id="foo-27"><a name="foo-27"></a><span class="lineno"> </span> <span class="testingk">return</span> <span class="testingp">[</span><span class="testingn">nodes</span><span class="testingo">.</span><span class="testingn">raw</span><span class="testingp">(</span><span class="testings1">''</span><span class="testingp">,</span> <span class="testingn">parsed</span><span class="testingp">,</span> <span class="testingnb">format</span><span class="testingo">=</span><span class="testings1">'html'</span><span class="testingp">)]</span><br></span></pre></div>
+<p>Lovely.</p>
+</div>
+<div class="section" id="testing-even-more-sourcecode-directives">
+<h2>Testing even more sourcecode directives</h2>
+<span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+<p>Lovely.</p>
+</div>
+<div class="section" id="testing-overriding-config-defaults">
+<h2>Testing overriding config defaults</h2>
+<p>Even if the default is line numbers, we can override it here</p>
+<div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+<p>Lovely.</p>
+</div>
+ The baz tag 2010-03-14T00:00:00+01:00 2010-03-14T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2010-03-14:/tag/baz.html <p>This article overrides the listening of the articles under the <em>baz</em> tag.</p>
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/feeds/misc.rss.xml b/pelican/tests/output/custom/feeds/misc.rss.xml
new file mode 100644
index 00000000..1c427eea
--- /dev/null
+++ b/pelican/tests/output/custom/feeds/misc.rss.xml
@@ -0,0 +1,16 @@
+
+Alexis' log - misc http://blog.notmyidea.org/A personal blog. Fri, 30 Nov 2012 00:00:00 +0100 FILENAME_METADATA example http://blog.notmyidea.org/filename_metadata-example.html<p>Some cool stuff!</p>
+ Alexis Métaireau Fri, 30 Nov 2012 00:00:00 +0100 tag:blog.notmyidea.org,2012-11-30:/filename_metadata-example.html misc Second article http://blog.notmyidea.org/second-article.html<p>This is some article, in english</p>
+ Alexis Métaireau Wed, 29 Feb 2012 00:00:00 +0100 tag:blog.notmyidea.org,2012-02-29:/second-article.html misc foo bar baz Unbelievable ! http://blog.notmyidea.org/unbelievable.html<p>Or completely awesome. Depends the needs.</p>
+<p><a class="reference external" href="http://blog.notmyidea.org/a-markdown-powered-article.html">a root-relative link to markdown-article</a>
+<a class="reference external" href="http://blog.notmyidea.org/a-markdown-powered-article.html">a file-relative link to markdown-article</a></p>
+<div class="section" id="testing-sourcecode-directive">
+<h2>Testing sourcecode directive</h2>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table></div>
+<div class="section" id="testing-another-case">
+<h2>Testing another case</h2>
+<p>This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will …</p></div> Alexis Métaireau Fri, 15 Oct 2010 20:30:00 +0200 tag:blog.notmyidea.org,2010-10-15:/unbelievable.html misc The baz tag http://blog.notmyidea.org/tag/baz.html<p>This article overrides the listening of the articles under the <em>baz</em> tag.</p>
+ Alexis Métaireau Sun, 14 Mar 2010 00:00:00 +0100 tag:blog.notmyidea.org,2010-03-14:/tag/baz.html misc
\ No newline at end of file
diff --git a/pelican/tests/output/custom/feeds/yeah.atom.xml b/pelican/tests/output/custom/feeds/yeah.atom.xml
new file mode 100644
index 00000000..127ab590
--- /dev/null
+++ b/pelican/tests/output/custom/feeds/yeah.atom.xml
@@ -0,0 +1,16 @@
+
+Alexis' log - yeah http://blog.notmyidea.org/ 2013-11-17T23:29:00+01:00 A personal blog. This is a super article ! 2010-12-02T10:14:00+01:00 2013-11-17T23:29:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2010-12-02:/this-is-a-super-article.html <p class="first last">Multi-line metadata should be supported
+as well as <strong>inline markup</strong>.</p>
+ <p>Some content here !</p>
+<div class="section" id="this-is-a-simple-title">
+<h2>This is a simple title</h2>
+<p>And here comes the cool <a class="reference external" href="http://books.couchdb.org/relax/design-documents/views">stuff</a>.</p>
+<img alt="alternate text" src="http://blog.notmyidea.org/pictures/Sushi.jpg" style="width: 600px; height: 450px;" />
+<img alt="alternate text" src="http://blog.notmyidea.org/pictures/Sushi_Macro.jpg" style="width: 600px; height: 450px;" />
+<pre class="literal-block">
+>>> from ipdb import set_trace
+>>> set_trace()
+</pre>
+<p>→ And now try with some utf8 hell: ééé</p>
+</div>
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/feeds/yeah.rss.xml b/pelican/tests/output/custom/feeds/yeah.rss.xml
new file mode 100644
index 00000000..98b820ec
--- /dev/null
+++ b/pelican/tests/output/custom/feeds/yeah.rss.xml
@@ -0,0 +1,4 @@
+
+Alexis' log - yeah http://blog.notmyidea.org/A personal blog. Sun, 17 Nov 2013 23:29:00 +0100 This is a super article ! http://blog.notmyidea.org/this-is-a-super-article.html<p class="first last">Multi-line metadata should be supported
+as well as <strong>inline markup</strong>.</p>
+ Alexis Métaireau Thu, 02 Dec 2010 10:14:00 +0100 tag:blog.notmyidea.org,2010-12-02:/this-is-a-super-article.html yeah foo bar foobar
\ No newline at end of file
diff --git a/pelican/tests/output/custom/filename_metadata-example.html b/pelican/tests/output/custom/filename_metadata-example.html
new file mode 100644
index 00000000..2fcbe562
--- /dev/null
+++ b/pelican/tests/output/custom/filename_metadata-example.html
@@ -0,0 +1,112 @@
+
+
+
+
+
+ FILENAME_METADATA example
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Fri 30 November 2012
+
+
+
+ By Alexis Métaireau
+
+In misc .
+
+ Some cool stuff!
+
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/index.html b/pelican/tests/output/custom/index.html
new file mode 100644
index 00000000..e63db6ec
--- /dev/null
+++ b/pelican/tests/output/custom/index.html
@@ -0,0 +1,171 @@
+
+
+
+
+
+ Alexis' log
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Fri 30 November 2012
+
+
+
+ By Alexis Métaireau
+
+In misc .
+
+ Some cool stuff!
+There are comments .
+
+
+ Other articles
+
+
+
+
+
+
+
+
+
+ Published: Wed 29 February 2012
+
+
+
+ By Alexis Métaireau
+
+In misc .
+tags: foo bar baz
Translations:
+ fr
+
+ This is some article, in english
+
+
read more
+
There are comments .
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Thu 17 February 2011
+
+
+
+ By Alexis Métaireau
+
+In cat1 .
+
+ Article 1
+
+
read more
+
There are comments .
+
+
+
+ Page 1 / 3
+ »
+ ⇉
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
diff --git a/pelican/tests/output/custom/index2.html b/pelican/tests/output/custom/index2.html
new file mode 100644
index 00000000..570e3b81
--- /dev/null
+++ b/pelican/tests/output/custom/index2.html
@@ -0,0 +1,186 @@
+
+
+
+
+
+ Alexis' log
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Thu 17 February 2011
+
+
+
+ By Alexis Métaireau
+
+In cat1 .
+
+ Article 2
+
+
read more
+
There are comments .
+
+
+
+
+
+
+
+
+ Published: Thu 17 February 2011
+
+
+
+ By Alexis Métaireau
+
+In cat1 .
+
+ Article 3
+
+
read more
+
There are comments .
+
+
+
+
+
+
+
+
+ Published: Thu 02 December 2010
+
+
+
+ Updated: Sun 17 November 2013
+
+
+
+ By Alexis Métaireau
+
+In yeah .
+tags: foo bar foobar
+ Multi-line metadata should be supported
+as well as inline markup .
+
+
read more
+
There are comments .
+
+
+
+
+
+
+
+
+ Published: Wed 20 October 2010
+
+
+
+ By Alexis Métaireau
+
+In bar .
+tags: oh bar yeah
Translations:
+ fr
+
+
+
Why not ?
+
After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !
+
+
+
+
read more
+
There are comments .
+
+
+
+ ⇇
+ «
+ Page 2 / 3
+ »
+ ⇉
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
diff --git a/pelican/tests/output/custom/index3.html b/pelican/tests/output/custom/index3.html
new file mode 100644
index 00000000..f92bdbb1
--- /dev/null
+++ b/pelican/tests/output/custom/index3.html
@@ -0,0 +1,136 @@
+
+
+
+
+
+ Alexis' log
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Fri 15 October 2010
+
+
+
+ By Alexis Métaireau
+
+In misc .
+
+ Or completely awesome. Depends the needs.
+
a root-relative link to markdown-article
+a file-relative link to markdown-article
+
+
Testing sourcecode directive
+
formatter = self . options and VARIANTS [ self . options . keys ()[ 0 ]]
+
+
+
+
Testing another case
+
This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will …
+
read more
+
There are comments .
+
+
+
+
+
+
+
+
+ Published: Sun 14 March 2010
+
+
+
+ By Alexis Métaireau
+
+In misc .
+
+ This article overrides the listening of the articles under the baz tag.
+
+
read more
+
There are comments .
+
+
+
+ ⇇
+ «
+ Page 3 / 3
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
diff --git a/pelican/tests/output/custom/jinja2_template.html b/pelican/tests/output/custom/jinja2_template.html
new file mode 100644
index 00000000..1ba06323
--- /dev/null
+++ b/pelican/tests/output/custom/jinja2_template.html
@@ -0,0 +1,74 @@
+
+
+
+
+
+ Alexis' log
+
+
+
+
+
+
+
+
+
+
+
+Some text
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/oh-yeah-fr.html b/pelican/tests/output/custom/oh-yeah-fr.html
new file mode 100644
index 00000000..bb7eb44e
--- /dev/null
+++ b/pelican/tests/output/custom/oh-yeah-fr.html
@@ -0,0 +1,116 @@
+
+
+
+
+
+ Trop bien !
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Wed 20 October 2010
+
+
+
+ By Alexis Métaireau
+
+In misc .
+Translations:
+ en
+
+ Et voila du contenu en français
+
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/oh-yeah.html b/pelican/tests/output/custom/oh-yeah.html
new file mode 100644
index 00000000..f7afb399
--- /dev/null
+++ b/pelican/tests/output/custom/oh-yeah.html
@@ -0,0 +1,121 @@
+
+
+
+
+
+ Oh yeah !
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Wed 20 October 2010
+
+
+
+ By Alexis Métaireau
+
+In bar .
+tags: oh bar yeah
Translations:
+ fr
+
+
+
Why not ?
+
After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !
+
+
+
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/override/index.html b/pelican/tests/output/custom/override/index.html
new file mode 100644
index 00000000..ebf07d8d
--- /dev/null
+++ b/pelican/tests/output/custom/override/index.html
@@ -0,0 +1,78 @@
+
+
+
+
+
+ Override url/save_as
+
+
+
+
+
+
+
+
+
+
+
+ Override url/save_as
+
+ Test page which overrides save_as and url so that this page will be generated
+at a custom location.
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/pages/this-is-a-test-hidden-page.html b/pelican/tests/output/custom/pages/this-is-a-test-hidden-page.html
new file mode 100644
index 00000000..e5fa1aaf
--- /dev/null
+++ b/pelican/tests/output/custom/pages/this-is-a-test-hidden-page.html
@@ -0,0 +1,78 @@
+
+
+
+
+
+ This is a test hidden page
+
+
+
+
+
+
+
+
+
+
+
+ This is a test hidden page
+
+ This is great for things like error(404) pages
+Anyone can see this page but it's not linked to anywhere!
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/pages/this-is-a-test-page.html b/pelican/tests/output/custom/pages/this-is-a-test-page.html
new file mode 100644
index 00000000..26c6c0e9
--- /dev/null
+++ b/pelican/tests/output/custom/pages/this-is-a-test-page.html
@@ -0,0 +1,79 @@
+
+
+
+
+
+ This is a test page
+
+
+
+
+
+
+
+
+
+
+
+ This is a test page
+
+ Just an image.
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/pictures/Fat_Cat.jpg b/pelican/tests/output/custom/pictures/Fat_Cat.jpg
new file mode 100644
index 00000000..d8a96d35
Binary files /dev/null and b/pelican/tests/output/custom/pictures/Fat_Cat.jpg differ
diff --git a/pelican/tests/output/custom/pictures/Sushi.jpg b/pelican/tests/output/custom/pictures/Sushi.jpg
new file mode 100644
index 00000000..e49e5f0a
Binary files /dev/null and b/pelican/tests/output/custom/pictures/Sushi.jpg differ
diff --git a/pelican/tests/output/custom/pictures/Sushi_Macro.jpg b/pelican/tests/output/custom/pictures/Sushi_Macro.jpg
new file mode 100644
index 00000000..21f935a1
Binary files /dev/null and b/pelican/tests/output/custom/pictures/Sushi_Macro.jpg differ
diff --git a/pelican/tests/output/custom/robots.txt b/pelican/tests/output/custom/robots.txt
new file mode 100644
index 00000000..19a6e299
--- /dev/null
+++ b/pelican/tests/output/custom/robots.txt
@@ -0,0 +1,2 @@
+User-agent: *
+Disallow: /pictures
diff --git a/pelican/tests/output/custom/second-article-fr.html b/pelican/tests/output/custom/second-article-fr.html
new file mode 100644
index 00000000..8e360e9c
--- /dev/null
+++ b/pelican/tests/output/custom/second-article-fr.html
@@ -0,0 +1,116 @@
+
+
+
+
+
+ Deuxième article
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Wed 29 February 2012
+
+
+
+ By Alexis Métaireau
+
+In misc .
+tags: foo bar baz
Translations:
+ en
+
+ Ceci est un article, en français.
+
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/second-article.html b/pelican/tests/output/custom/second-article.html
new file mode 100644
index 00000000..5001f0d0
--- /dev/null
+++ b/pelican/tests/output/custom/second-article.html
@@ -0,0 +1,116 @@
+
+
+
+
+
+ Second article
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Wed 29 February 2012
+
+
+
+ By Alexis Métaireau
+
+In misc .
+tags: foo bar baz
Translations:
+ fr
+
+ This is some article, in english
+
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/tag/bar.html b/pelican/tests/output/custom/tag/bar.html
new file mode 100644
index 00000000..b2b22714
--- /dev/null
+++ b/pelican/tests/output/custom/tag/bar.html
@@ -0,0 +1,154 @@
+
+
+
+
+
+ Alexis' log - bar
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Wed 29 February 2012
+
+
+
+ By Alexis Métaireau
+
+In misc .
+tags: foo bar baz
Translations:
+ fr
+
+ This is some article, in english
+There are comments .
+
+
+ Other articles
+
+
+
+
+
+
+
+
+
+ Published: Thu 02 December 2010
+
+
+
+ Updated: Sun 17 November 2013
+
+
+
+ By Alexis Métaireau
+
+In yeah .
+tags: foo bar foobar
+ Multi-line metadata should be supported
+as well as inline markup .
+
+
read more
+
There are comments .
+
+
+
+
+
+
+
+
+ Published: Wed 20 October 2010
+
+
+
+ By Alexis Métaireau
+
+In bar .
+tags: oh bar yeah
Translations:
+ fr
+
+
+
Why not ?
+
After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !
+
+
+
+
read more
+
There are comments .
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/tag/baz.html b/pelican/tests/output/custom/tag/baz.html
new file mode 100644
index 00000000..ef53c34c
--- /dev/null
+++ b/pelican/tests/output/custom/tag/baz.html
@@ -0,0 +1,112 @@
+
+
+
+
+
+ The baz tag
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Sun 14 March 2010
+
+
+
+ By Alexis Métaireau
+
+In misc .
+
+ This article overrides the listening of the articles under the baz tag.
+
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/tag/foo.html b/pelican/tests/output/custom/tag/foo.html
new file mode 100644
index 00000000..7684fbc5
--- /dev/null
+++ b/pelican/tests/output/custom/tag/foo.html
@@ -0,0 +1,124 @@
+
+
+
+
+
+ Alexis' log - foo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Wed 29 February 2012
+
+
+
+ By Alexis Métaireau
+
+In misc .
+tags: foo bar baz
Translations:
+ fr
+
+ This is some article, in english
+There are comments .
+
+
+ Other articles
+
+
+
+
+
+
+
+
+
+ Published: Thu 02 December 2010
+
+
+
+ Updated: Sun 17 November 2013
+
+
+
+ By Alexis Métaireau
+
+In yeah .
+tags: foo bar foobar
+ Multi-line metadata should be supported
+as well as inline markup .
+
+
read more
+
There are comments .
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/tag/foobar.html b/pelican/tests/output/custom/tag/foobar.html
new file mode 100644
index 00000000..5816e395
--- /dev/null
+++ b/pelican/tests/output/custom/tag/foobar.html
@@ -0,0 +1,103 @@
+
+
+
+
+
+ Alexis' log - foobar
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Thu 02 December 2010
+
+
+
+ Updated: Sun 17 November 2013
+
+
+
+ By Alexis Métaireau
+
+In yeah .
+tags: foo bar foobar
+ Some content here !
+
+
This is a simple title
+
And here comes the cool stuff .
+
+
+
+>>> from ipdb import set_trace
+>>> set_trace()
+
+
→ And now try with some utf8 hell: ééé
+
+There are comments .
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/tag/oh.html b/pelican/tests/output/custom/tag/oh.html
new file mode 100644
index 00000000..16ea9d0d
--- /dev/null
+++ b/pelican/tests/output/custom/tag/oh.html
@@ -0,0 +1,77 @@
+
+
+
+
+
+ Oh Oh Oh
+
+
+
+
+
+
+
+
+
+
+
+ Oh Oh Oh
+
+ This page overrides the listening of the articles under the oh tag.
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/tag/yeah.html b/pelican/tests/output/custom/tag/yeah.html
new file mode 100644
index 00000000..929a48e0
--- /dev/null
+++ b/pelican/tests/output/custom/tag/yeah.html
@@ -0,0 +1,95 @@
+
+
+
+
+
+ Alexis' log - yeah
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Wed 20 October 2010
+
+
+
+ By Alexis Métaireau
+
+In bar .
+tags: oh bar yeah
Translations:
+ fr
+
+
+
Why not ?
+
After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !
+
+
+There are comments .
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/tags.html b/pelican/tests/output/custom/tags.html
new file mode 100644
index 00000000..244c21e3
--- /dev/null
+++ b/pelican/tests/output/custom/tags.html
@@ -0,0 +1,84 @@
+
+
+
+
+
+ Alexis' log - Tags
+
+
+
+
+
+
+
+
+
+
+
+
+ Tags for Alexis' log
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/theme/css/fonts.css b/pelican/tests/output/custom/theme/css/fonts.css
new file mode 100644
index 00000000..56015076
--- /dev/null
+++ b/pelican/tests/output/custom/theme/css/fonts.css
@@ -0,0 +1,12 @@
+@font-face {
+ font-family: 'Yanone Kaffeesatz';
+ font-style: normal;
+ font-weight: 400;
+ src:
+ local('Yanone Kaffeesatz Regular'),
+ local('YanoneKaffeesatz-Regular'),
+ /* from https://fonts.gstatic.com/s/yanonekaffeesatz/v8/YDAoLskQQ5MOAgvHUQCcLRTHiN2BPBirwIkMLKUspj4.woff */
+ url('../fonts/Yanone_Kaffeesatz_400.woff') format('woff'),
+ /* from https://fonts.gstatic.com/s/yanonekaffeesatz/v8/YDAoLskQQ5MOAgvHUQCcLfGwxTS8d1Q9KiDNCMKLFUM.woff2 */
+ url('../fonts/Yanone_Kaffeesatz_400.woff2') format('woff2');
+}
diff --git a/pelican/tests/output/custom/theme/css/main.css b/pelican/tests/output/custom/theme/css/main.css
new file mode 100644
index 00000000..63f5adc0
--- /dev/null
+++ b/pelican/tests/output/custom/theme/css/main.css
@@ -0,0 +1,466 @@
+/*
+ Name: Smashing HTML5
+ Date: July 2009
+ Description: Sample layout for HTML5 and CSS3 goodness.
+ Version: 1.0
+ License: MIT
+ Licensed by: Smashing Media GmbH
+ Original author: Enrique Ramírez
+*/
+
+/* Imports */
+@import url("reset.css");
+@import url("pygment.css");
+@import url("typogrify.css");
+@import url("fonts.css");
+
+/***** Global *****/
+/* Body */
+body {
+ background: #F5F4EF;
+ color: #000305;
+ font-size: 87.5%; /* Base font size: 14px */
+ font-family: 'Trebuchet MS', Trebuchet, 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif;
+ line-height: 1.429;
+ margin: 0;
+ padding: 0;
+ text-align: left;
+}
+
+/* Headings */
+h1 {font-size: 2em }
+h2 {font-size: 1.571em} /* 22px */
+h3 {font-size: 1.429em} /* 20px */
+h4 {font-size: 1.286em} /* 18px */
+h5 {font-size: 1.143em} /* 16px */
+h6 {font-size: 1em} /* 14px */
+
+h1, h2, h3, h4, h5, h6 {
+ font-weight: 400;
+ line-height: 1.1;
+ margin-bottom: .8em;
+ font-family: 'Yanone Kaffeesatz', arial, serif;
+}
+
+h3, h4, h5, h6 { margin-top: .8em; }
+
+hr { border: 2px solid #EEEEEE; }
+
+/* Anchors */
+a {outline: 0;}
+a img {border: 0px; text-decoration: none;}
+a:link, a:visited {
+ color: #C74350;
+ padding: 0 1px;
+ text-decoration: underline;
+}
+a:hover, a:active {
+ background-color: #C74350;
+ color: #fff;
+ text-decoration: none;
+ text-shadow: 1px 1px 1px #333;
+}
+
+h1 a:hover {
+ background-color: inherit
+}
+
+/* Paragraphs */
+div.line-block,
+p { margin-top: 1em;
+ margin-bottom: 1em;}
+
+strong, b {font-weight: bold;}
+em, i {font-style: italic;}
+
+/* Lists */
+ul {
+ list-style: outside disc;
+ margin: 0em 0 0 1.5em;
+}
+
+ol {
+ list-style: outside decimal;
+ margin: 0em 0 0 1.5em;
+}
+
+li { margin-top: 0.5em;
+ margin-bottom: 1em; }
+
+.post-info {
+ float:right;
+ margin:10px;
+ padding:5px;
+}
+
+.post-info p{
+ margin-top: 1px;
+ margin-bottom: 1px;
+}
+
+.readmore { float: right }
+
+dl {margin: 0 0 1.5em 0;}
+dt {font-weight: bold;}
+dd {margin-left: 1.5em;}
+
+pre{background-color: rgb(238, 238, 238); padding: 10px; margin: 10px; overflow: auto;}
+
+/* Quotes */
+blockquote {
+ margin: 20px;
+ font-style: italic;
+}
+cite {}
+
+q {}
+
+div.note {
+ float: right;
+ margin: 5px;
+ font-size: 85%;
+ max-width: 300px;
+}
+
+/* Tables */
+table {margin: .5em auto 1.5em auto; width: 98%;}
+
+ /* Thead */
+ thead th {padding: .5em .4em; text-align: left;}
+ thead td {}
+
+ /* Tbody */
+ tbody td {padding: .5em .4em;}
+ tbody th {}
+
+ tbody .alt td {}
+ tbody .alt th {}
+
+ /* Tfoot */
+ tfoot th {}
+ tfoot td {}
+
+/* HTML5 tags */
+header, section, footer,
+aside, nav, article, figure {
+ display: block;
+}
+
+/***** Layout *****/
+.body {clear: both; margin: 0 auto; width: 800px;}
+img.right, figure.right, div.figure.align-right {
+ float: right;
+ margin: 0 0 2em 2em;
+}
+img.left, figure.left, div.figure.align-left {
+ float: left;
+ margin: 0 2em 2em 0;
+}
+
+/* .rst support */
+div.figure img, figure img { /* to fill figure exactly */
+ width: 100%;
+}
+div.figure p.caption, figure p.caption { /* margin provided by figure */
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+/*
+ Header
+*****************/
+#banner {
+ margin: 0 auto;
+ padding: 2.5em 0 0 0;
+}
+
+ /* Banner */
+ #banner h1 {font-size: 3.571em; line-height: 0;}
+ #banner h1 a:link, #banner h1 a:visited {
+ color: #000305;
+ display: block;
+ font-weight: bold;
+ margin: 0 0 .6em .2em;
+ text-decoration: none;
+ }
+ #banner h1 a:hover, #banner h1 a:active {
+ background: none;
+ color: #C74350;
+ text-shadow: none;
+ }
+
+ #banner h1 strong {font-size: 0.36em; font-weight: normal;}
+
+ /* Main Nav */
+ #banner nav {
+ background: #000305;
+ font-size: 1.143em;
+ height: 40px;
+ line-height: 30px;
+ margin: 0 auto 2em auto;
+ padding: 0;
+ text-align: center;
+ width: 800px;
+
+ border-radius: 5px;
+ -moz-border-radius: 5px;
+ -webkit-border-radius: 5px;
+ }
+
+ #banner nav ul {list-style: none; margin: 0 auto; width: 800px;}
+ #banner nav li {float: left; display: inline; margin: 0;}
+
+ #banner nav a:link, #banner nav a:visited {
+ color: #fff;
+ display: inline-block;
+ height: 30px;
+ padding: 5px 1.5em;
+ text-decoration: none;
+ }
+ #banner nav a:hover, #banner nav a:active,
+ #banner nav .active a:link, #banner nav .active a:visited {
+ background: #C74451;
+ color: #fff;
+ text-shadow: none !important;
+ }
+
+ #banner nav li:first-child a {
+ border-top-left-radius: 5px;
+ -moz-border-radius-topleft: 5px;
+ -webkit-border-top-left-radius: 5px;
+
+ border-bottom-left-radius: 5px;
+ -moz-border-radius-bottomleft: 5px;
+ -webkit-border-bottom-left-radius: 5px;
+ }
+
+/*
+ Featured
+*****************/
+#featured {
+ background: #fff;
+ margin-bottom: 2em;
+ overflow: hidden;
+ padding: 20px;
+ width: 760px;
+
+ border-radius: 10px;
+ -moz-border-radius: 10px;
+ -webkit-border-radius: 10px;
+}
+
+#featured figure {
+ border: 2px solid #eee;
+ float: right;
+ margin: 0.786em 2em 0 5em;
+ width: 248px;
+}
+#featured figure img {display: block; float: right;}
+
+#featured h2 {color: #C74451; font-size: 1.714em; margin-bottom: 0.333em;}
+#featured h3 {font-size: 1.429em; margin-bottom: .5em;}
+
+#featured h3 a:link, #featured h3 a:visited {color: #000305; text-decoration: none;}
+#featured h3 a:hover, #featured h3 a:active {color: #fff;}
+
+/*
+ Body
+*****************/
+#content {
+ background: #fff;
+ margin-bottom: 2em;
+ overflow: hidden;
+ padding: 20px 20px;
+ width: 760px;
+
+ border-radius: 10px;
+ -moz-border-radius: 10px;
+ -webkit-border-radius: 10px;
+}
+
+/*
+ Extras
+*****************/
+#extras {margin: 0 auto 3em auto; overflow: hidden;}
+
+#extras ul {list-style: none; margin: 0;}
+#extras li {border-bottom: 1px solid #fff;}
+#extras h2 {
+ color: #C74350;
+ font-size: 1.429em;
+ margin-bottom: .25em;
+ padding: 0 3px;
+}
+
+#extras a:link, #extras a:visited {
+ color: #444;
+ display: block;
+ border-bottom: 1px solid #F4E3E3;
+ text-decoration: none;
+ padding: .3em .25em;
+}
+
+#extras a:hover, #extras a:active {color: #fff;}
+
+ /* Blogroll */
+ #extras .blogroll {
+ float: left;
+ width: 615px;
+ }
+
+ #extras .blogroll li {float: left; margin: 0 20px 0 0; width: 185px;}
+
+ /* Social */
+ #extras .social {
+ float: right;
+ width: 175px;
+ }
+
+ #extras div[class='social'] a {
+ background-repeat: no-repeat;
+ background-position: 3px 6px;
+ padding-left: 25px;
+ }
+
+ /* Icons */
+ .social a[href*='about.me'] {background-image: url('../images/icons/aboutme.png');}
+ .social a[href*='bitbucket.org'] {background-image: url('../images/icons/bitbucket.png');}
+ .social a[href*='delicious.com'] {background-image: url('../images/icons/delicious.png');}
+ .social a[href*='facebook.com'] {background-image: url('../images/icons/facebook.png');}
+ .social a[href*='gitorious.org'] {background-image: url('../images/icons/gitorious.png');}
+ .social a[href*='github.com'],
+ .social a[href*='git.io'] {
+ background-image: url('../images/icons/github.png');
+ background-size: 16px 16px;
+ }
+ .social a[href*='gittip.com'] {background-image: url('../images/icons/gittip.png');}
+ .social a[href*='plus.google.com'] {background-image: url('../images/icons/google-plus.png');}
+ .social a[href*='groups.google.com'] {background-image: url('../images/icons/google-groups.png');}
+ .social a[href*='news.ycombinator.com'],
+ .social a[href*='hackernewsers.com'] {background-image: url('../images/icons/hackernews.png');}
+ .social a[href*='last.fm'], .social a[href*='lastfm.'] {background-image: url('../images/icons/lastfm.png');}
+ .social a[href*='linkedin.com'] {background-image: url('../images/icons/linkedin.png');}
+ .social a[href*='reddit.com'] {background-image: url('../images/icons/reddit.png');}
+ .social a[type$='atom+xml'], .social a[type$='rss+xml'] {background-image: url('../images/icons/rss.png');}
+ .social a[href*='slideshare.net'] {background-image: url('../images/icons/slideshare.png');}
+ .social a[href*='speakerdeck.com'] {background-image: url('../images/icons/speakerdeck.png');}
+ .social a[href*='stackoverflow.com'] {background-image: url('../images/icons/stackoverflow.png');}
+ .social a[href*='twitter.com'] {background-image: url('../images/icons/twitter.png');}
+ .social a[href*='vimeo.com'] {background-image: url('../images/icons/vimeo.png');}
+ .social a[href*='youtube.com'] {background-image: url('../images/icons/youtube.png');}
+
+/*
+ About
+*****************/
+#about {
+ background: #fff;
+ font-style: normal;
+ margin-bottom: 2em;
+ overflow: hidden;
+ padding: 20px;
+ text-align: left;
+ width: 760px;
+
+ border-radius: 10px;
+ -moz-border-radius: 10px;
+ -webkit-border-radius: 10px;
+}
+
+#about .primary {float: left; width: 165px;}
+#about .primary strong {color: #C64350; display: block; font-size: 1.286em;}
+#about .photo {float: left; margin: 5px 20px;}
+
+#about .url:link, #about .url:visited {text-decoration: none;}
+
+#about .bio {float: right; width: 500px;}
+
+/*
+ Footer
+*****************/
+#contentinfo {padding-bottom: 2em; text-align: right;}
+
+/***** Sections *****/
+/* Blog */
+.hentry {
+ display: block;
+ clear: both;
+ border-bottom: 1px solid #eee;
+ padding: 1.5em 0;
+}
+li:last-child .hentry, #content > .hentry {border: 0; margin: 0;}
+#content > .hentry {padding: 1em 0;}
+.hentry img{display : none ;}
+.entry-title {font-size: 3em; margin-bottom: 10px; margin-top: 0;}
+.entry-title a:link, .entry-title a:visited {text-decoration: none; color: #333;}
+.entry-title a:visited {background-color: #fff;}
+
+.hentry .post-info * {font-style: normal;}
+
+ /* Content */
+ .hentry footer {margin-bottom: 2em;}
+ .hentry footer address {display: inline;}
+ #posts-list footer address {display: block;}
+
+ /* Blog Index */
+ #posts-list {list-style: none; margin: 0;}
+ #posts-list .hentry {padding-left: 10px; position: relative;}
+
+ #posts-list footer {
+ left: 10px;
+ position: relative;
+ float: left;
+ top: 0.5em;
+ width: 190px;
+ }
+
+ /* About the Author */
+ #about-author {
+ background: #f9f9f9;
+ clear: both;
+ font-style: normal;
+ margin: 2em 0;
+ padding: 10px 20px 15px 20px;
+
+ border-radius: 5px;
+ -moz-border-radius: 5px;
+ -webkit-border-radius: 5px;
+ }
+
+ #about-author strong {
+ color: #C64350;
+ clear: both;
+ display: block;
+ font-size: 1.429em;
+ }
+
+ #about-author .photo {border: 1px solid #ddd; float: left; margin: 5px 1em 0 0;}
+
+ /* Comments */
+ #comments-list {list-style: none; margin: 0 1em;}
+ #comments-list blockquote {
+ background: #f8f8f8;
+ clear: both;
+ font-style: normal;
+ margin: 0;
+ padding: 15px 20px;
+
+ border-radius: 5px;
+ -moz-border-radius: 5px;
+ -webkit-border-radius: 5px;
+ }
+ #comments-list footer {color: #888; padding: .5em 1em 0 0; text-align: right;}
+
+ #comments-list li:nth-child(2n) blockquote {background: #F5f5f5;}
+
+ /* Add a Comment */
+ #add-comment label {clear: left; float: left; text-align: left; width: 150px;}
+ #add-comment input[type='text'],
+ #add-comment input[type='email'],
+ #add-comment input[type='url'] {float: left; width: 200px;}
+
+ #add-comment textarea {float: left; height: 150px; width: 495px;}
+
+ #add-comment p.req {clear: both; margin: 0 .5em 1em 0; text-align: right;}
+
+ #add-comment input[type='submit'] {float: right; margin: 0 .5em;}
+ #add-comment * {margin-bottom: .5em;}
diff --git a/pelican/tests/output/custom/theme/css/pygment.css b/pelican/tests/output/custom/theme/css/pygment.css
new file mode 100644
index 00000000..fdd056f6
--- /dev/null
+++ b/pelican/tests/output/custom/theme/css/pygment.css
@@ -0,0 +1,205 @@
+.hll {
+background-color:#eee;
+}
+.c {
+color:#408090;
+font-style:italic;
+}
+.err {
+border:1px solid #FF0000;
+}
+.k {
+color:#007020;
+font-weight:bold;
+}
+.o {
+color:#666666;
+}
+.cm {
+color:#408090;
+font-style:italic;
+}
+.cp {
+color:#007020;
+}
+.c1 {
+color:#408090;
+font-style:italic;
+}
+.cs {
+background-color:#FFF0F0;
+color:#408090;
+}
+.gd {
+color:#A00000;
+}
+.ge {
+font-style:italic;
+}
+.gr {
+color:#FF0000;
+}
+.gh {
+color:#000080;
+font-weight:bold;
+}
+.gi {
+color:#00A000;
+}
+.go {
+color:#303030;
+}
+.gp {
+color:#C65D09;
+font-weight:bold;
+}
+.gs {
+font-weight:bold;
+}
+.gu {
+color:#800080;
+font-weight:bold;
+}
+.gt {
+color:#0040D0;
+}
+.kc {
+color:#007020;
+font-weight:bold;
+}
+.kd {
+color:#007020;
+font-weight:bold;
+}
+.kn {
+color:#007020;
+font-weight:bold;
+}
+.kp {
+color:#007020;
+}
+.kr {
+color:#007020;
+font-weight:bold;
+}
+.kt {
+color:#902000;
+}
+.m {
+color:#208050;
+}
+.s {
+color:#4070A0;
+}
+.na {
+color:#4070A0;
+}
+.nb {
+color:#007020;
+}
+.nc {
+color:#0E84B5;
+font-weight:bold;
+}
+.no {
+color:#60ADD5;
+}
+.nd {
+color:#555555;
+font-weight:bold;
+}
+.ni {
+color:#D55537;
+font-weight:bold;
+}
+.ne {
+color:#007020;
+}
+.nf {
+color:#06287E;
+}
+.nl {
+color:#002070;
+font-weight:bold;
+}
+.nn {
+color:#0E84B5;
+font-weight:bold;
+}
+.nt {
+color:#062873;
+font-weight:bold;
+}
+.nv {
+color:#BB60D5;
+}
+.ow {
+color:#007020;
+font-weight:bold;
+}
+.w {
+color:#BBBBBB;
+}
+.mf {
+color:#208050;
+}
+.mh {
+color:#208050;
+}
+.mi {
+color:#208050;
+}
+.mo {
+color:#208050;
+}
+.sb {
+color:#4070A0;
+}
+.sc {
+color:#4070A0;
+}
+.sd {
+color:#4070A0;
+font-style:italic;
+}
+.s2 {
+color:#4070A0;
+}
+.se {
+color:#4070A0;
+font-weight:bold;
+}
+.sh {
+color:#4070A0;
+}
+.si {
+color:#70A0D0;
+font-style:italic;
+}
+.sx {
+color:#C65D09;
+}
+.sr {
+color:#235388;
+}
+.s1 {
+color:#4070A0;
+}
+.ss {
+color:#517918;
+}
+.bp {
+color:#007020;
+}
+.vc {
+color:#BB60D5;
+}
+.vg {
+color:#BB60D5;
+}
+.vi {
+color:#BB60D5;
+}
+.il {
+color:#208050;
+}
diff --git a/pelican/tests/output/custom/theme/css/reset.css b/pelican/tests/output/custom/theme/css/reset.css
new file mode 100644
index 00000000..c88e6196
--- /dev/null
+++ b/pelican/tests/output/custom/theme/css/reset.css
@@ -0,0 +1,52 @@
+/*
+ Name: Reset Stylesheet
+ Description: Resets browser's default CSS
+ Author: Eric Meyer
+ Author URI: https://meyerweb.com/eric/tools/css/reset/
+*/
+
+/* v1.0 | 20080212 */
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, font, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td {
+ background: transparent;
+ border: 0;
+ font-size: 100%;
+ margin: 0;
+ outline: 0;
+ padding: 0;
+ vertical-align: baseline;
+}
+
+body {line-height: 1;}
+
+ol, ul {list-style: none;}
+
+blockquote, q {quotes: none;}
+
+blockquote:before, blockquote:after,
+q:before, q:after {
+ content: '';
+ content: none;
+}
+
+/* remember to define focus styles! */
+:focus {
+ outline: 0;
+}
+
+/* remember to highlight inserts somehow! */
+ins {text-decoration: none;}
+del {text-decoration: line-through;}
+
+/* tables still need 'cellspacing="0"' in the markup */
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
\ No newline at end of file
diff --git a/pelican/tests/output/custom/theme/css/typogrify.css b/pelican/tests/output/custom/theme/css/typogrify.css
new file mode 100644
index 00000000..3bae4976
--- /dev/null
+++ b/pelican/tests/output/custom/theme/css/typogrify.css
@@ -0,0 +1,3 @@
+.caps {font-size:.92em;}
+.amp {color:#666; font-size:1.05em;font-family:"Warnock Pro", "Goudy Old Style","Palatino","Book Antiqua",serif; font-style:italic;}
+.dquo {margin-left:-.38em;}
diff --git a/pelican/tests/output/custom/theme/css/wide.css b/pelican/tests/output/custom/theme/css/wide.css
new file mode 100644
index 00000000..88fd59ce
--- /dev/null
+++ b/pelican/tests/output/custom/theme/css/wide.css
@@ -0,0 +1,48 @@
+@import url("main.css");
+
+body {
+ font:1.3em/1.3 "Hoefler Text","Georgia",Georgia,serif,sans-serif;
+}
+
+.post-info{
+ display: none;
+}
+
+#banner nav {
+ display: none;
+ -moz-border-radius: 0px;
+ margin-bottom: 20px;
+ overflow: hidden;
+ font-size: 1em;
+ background: #F5F4EF;
+}
+
+#banner nav ul{
+ padding-right: 50px;
+}
+
+#banner nav li{
+ float: right;
+ color: #000;
+}
+
+#banner nav li a {
+ color: #000;
+}
+
+#banner h1 {
+ margin-bottom: -18px;
+}
+
+#featured, #extras {
+ padding: 50px;
+}
+
+#featured {
+ padding-top: 20px;
+}
+
+#extras {
+ padding-top: 0px;
+ padding-bottom: 0px;
+}
diff --git a/pelican/tests/output/custom/theme/fonts/Yanone_Kaffeesatz_400.eot b/pelican/tests/output/custom/theme/fonts/Yanone_Kaffeesatz_400.eot
new file mode 100644
index 00000000..b3b90dbc
Binary files /dev/null and b/pelican/tests/output/custom/theme/fonts/Yanone_Kaffeesatz_400.eot differ
diff --git a/pelican/tests/output/custom/theme/fonts/Yanone_Kaffeesatz_400.svg b/pelican/tests/output/custom/theme/fonts/Yanone_Kaffeesatz_400.svg
new file mode 100644
index 00000000..a69669b5
--- /dev/null
+++ b/pelican/tests/output/custom/theme/fonts/Yanone_Kaffeesatz_400.svg
@@ -0,0 +1,407 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pelican/tests/output/custom/theme/fonts/Yanone_Kaffeesatz_400.ttf b/pelican/tests/output/custom/theme/fonts/Yanone_Kaffeesatz_400.ttf
new file mode 100644
index 00000000..6f4feb02
Binary files /dev/null and b/pelican/tests/output/custom/theme/fonts/Yanone_Kaffeesatz_400.ttf differ
diff --git a/pelican/tests/output/custom/theme/fonts/Yanone_Kaffeesatz_400.woff b/pelican/tests/output/custom/theme/fonts/Yanone_Kaffeesatz_400.woff
new file mode 100644
index 00000000..ddccf765
Binary files /dev/null and b/pelican/tests/output/custom/theme/fonts/Yanone_Kaffeesatz_400.woff differ
diff --git a/pelican/tests/output/custom/theme/fonts/Yanone_Kaffeesatz_400.woff2 b/pelican/tests/output/custom/theme/fonts/Yanone_Kaffeesatz_400.woff2
new file mode 100644
index 00000000..7b18f7ea
Binary files /dev/null and b/pelican/tests/output/custom/theme/fonts/Yanone_Kaffeesatz_400.woff2 differ
diff --git a/pelican/tests/output/custom/theme/fonts/font.css b/pelican/tests/output/custom/theme/fonts/font.css
new file mode 100644
index 00000000..a9eb87e6
--- /dev/null
+++ b/pelican/tests/output/custom/theme/fonts/font.css
@@ -0,0 +1,12 @@
+@font-face {
+ font-family: 'Yanone Kaffeesatz';
+ font-style: normal;
+ font-weight: 400;
+ src:
+ local('Yanone Kaffeesatz Regular'),
+ local('YanoneKaffeesatz-Regular'),
+ /* from https://fonts.gstatic.com/s/yanonekaffeesatz/v8/YDAoLskQQ5MOAgvHUQCcLRTHiN2BPBirwIkMLKUspj4.woff */
+ url('Yanone_Kaffeesatz_400.woff') format('woff'),
+ /* from https://fonts.gstatic.com/s/yanonekaffeesatz/v8/YDAoLskQQ5MOAgvHUQCcLfGwxTS8d1Q9KiDNCMKLFUM.woff2 */
+ url('Yanone_Kaffeesatz_400.woff2') format('woff2');
+}
diff --git a/pelican/tests/output/custom/theme/images/icons/aboutme.png b/pelican/tests/output/custom/theme/images/icons/aboutme.png
new file mode 100644
index 00000000..600110f2
Binary files /dev/null and b/pelican/tests/output/custom/theme/images/icons/aboutme.png differ
diff --git a/pelican/tests/output/custom/theme/images/icons/bitbucket.png b/pelican/tests/output/custom/theme/images/icons/bitbucket.png
new file mode 100644
index 00000000..277a7dfb
Binary files /dev/null and b/pelican/tests/output/custom/theme/images/icons/bitbucket.png differ
diff --git a/pelican/tests/output/custom/theme/images/icons/delicious.png b/pelican/tests/output/custom/theme/images/icons/delicious.png
new file mode 100644
index 00000000..34868c5c
Binary files /dev/null and b/pelican/tests/output/custom/theme/images/icons/delicious.png differ
diff --git a/pelican/tests/output/custom/theme/images/icons/facebook.png b/pelican/tests/output/custom/theme/images/icons/facebook.png
new file mode 100644
index 00000000..1d8a4327
Binary files /dev/null and b/pelican/tests/output/custom/theme/images/icons/facebook.png differ
diff --git a/pelican/tests/output/custom/theme/images/icons/github.png b/pelican/tests/output/custom/theme/images/icons/github.png
new file mode 100644
index 00000000..5d9109de
Binary files /dev/null and b/pelican/tests/output/custom/theme/images/icons/github.png differ
diff --git a/pelican/tests/output/custom/theme/images/icons/gitorious.png b/pelican/tests/output/custom/theme/images/icons/gitorious.png
new file mode 100644
index 00000000..a6705d0f
Binary files /dev/null and b/pelican/tests/output/custom/theme/images/icons/gitorious.png differ
diff --git a/pelican/tests/output/custom/theme/images/icons/gittip.png b/pelican/tests/output/custom/theme/images/icons/gittip.png
new file mode 100644
index 00000000..b9f67aaa
Binary files /dev/null and b/pelican/tests/output/custom/theme/images/icons/gittip.png differ
diff --git a/pelican/tests/output/custom/theme/images/icons/google-groups.png b/pelican/tests/output/custom/theme/images/icons/google-groups.png
new file mode 100644
index 00000000..bbd0a0fd
Binary files /dev/null and b/pelican/tests/output/custom/theme/images/icons/google-groups.png differ
diff --git a/pelican/tests/output/custom/theme/images/icons/google-plus.png b/pelican/tests/output/custom/theme/images/icons/google-plus.png
new file mode 100644
index 00000000..f8553d45
Binary files /dev/null and b/pelican/tests/output/custom/theme/images/icons/google-plus.png differ
diff --git a/pelican/tests/output/custom/theme/images/icons/hackernews.png b/pelican/tests/output/custom/theme/images/icons/hackernews.png
new file mode 100644
index 00000000..8e05e3ee
Binary files /dev/null and b/pelican/tests/output/custom/theme/images/icons/hackernews.png differ
diff --git a/pelican/tests/output/custom/theme/images/icons/lastfm.png b/pelican/tests/output/custom/theme/images/icons/lastfm.png
new file mode 100644
index 00000000..2eedd2da
Binary files /dev/null and b/pelican/tests/output/custom/theme/images/icons/lastfm.png differ
diff --git a/pelican/tests/output/custom/theme/images/icons/linkedin.png b/pelican/tests/output/custom/theme/images/icons/linkedin.png
new file mode 100644
index 00000000..06a88016
Binary files /dev/null and b/pelican/tests/output/custom/theme/images/icons/linkedin.png differ
diff --git a/pelican/tests/output/custom/theme/images/icons/reddit.png b/pelican/tests/output/custom/theme/images/icons/reddit.png
new file mode 100644
index 00000000..d826d3e7
Binary files /dev/null and b/pelican/tests/output/custom/theme/images/icons/reddit.png differ
diff --git a/pelican/tests/output/custom/theme/images/icons/rss.png b/pelican/tests/output/custom/theme/images/icons/rss.png
new file mode 100644
index 00000000..12448f53
Binary files /dev/null and b/pelican/tests/output/custom/theme/images/icons/rss.png differ
diff --git a/pelican/tests/output/custom/theme/images/icons/slideshare.png b/pelican/tests/output/custom/theme/images/icons/slideshare.png
new file mode 100644
index 00000000..9cbe8588
Binary files /dev/null and b/pelican/tests/output/custom/theme/images/icons/slideshare.png differ
diff --git a/pelican/tests/output/custom/theme/images/icons/speakerdeck.png b/pelican/tests/output/custom/theme/images/icons/speakerdeck.png
new file mode 100644
index 00000000..7281ec48
Binary files /dev/null and b/pelican/tests/output/custom/theme/images/icons/speakerdeck.png differ
diff --git a/pelican/tests/output/custom/theme/images/icons/stackoverflow.png b/pelican/tests/output/custom/theme/images/icons/stackoverflow.png
new file mode 100644
index 00000000..3c6862e9
Binary files /dev/null and b/pelican/tests/output/custom/theme/images/icons/stackoverflow.png differ
diff --git a/pelican/tests/output/custom/theme/images/icons/twitter.png b/pelican/tests/output/custom/theme/images/icons/twitter.png
new file mode 100644
index 00000000..cef1cef9
Binary files /dev/null and b/pelican/tests/output/custom/theme/images/icons/twitter.png differ
diff --git a/pelican/tests/output/custom/theme/images/icons/vimeo.png b/pelican/tests/output/custom/theme/images/icons/vimeo.png
new file mode 100644
index 00000000..4b9d7212
Binary files /dev/null and b/pelican/tests/output/custom/theme/images/icons/vimeo.png differ
diff --git a/pelican/tests/output/custom/theme/images/icons/youtube.png b/pelican/tests/output/custom/theme/images/icons/youtube.png
new file mode 100644
index 00000000..e334e68e
Binary files /dev/null and b/pelican/tests/output/custom/theme/images/icons/youtube.png differ
diff --git a/pelican/tests/output/custom/this-is-a-super-article.html b/pelican/tests/output/custom/this-is-a-super-article.html
new file mode 100644
index 00000000..55053cb2
--- /dev/null
+++ b/pelican/tests/output/custom/this-is-a-super-article.html
@@ -0,0 +1,127 @@
+
+
+
+
+
+ This is a super article !
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Thu 02 December 2010
+
+
+
+ Updated: Sun 17 November 2013
+
+
+
+ By Alexis Métaireau
+
+In yeah .
+tags: foo bar foobar
+ Some content here !
+
+
This is a simple title
+
And here comes the cool stuff .
+
+
+
+>>> from ipdb import set_trace
+>>> set_trace()
+
+
→ And now try with some utf8 hell: ééé
+
+
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom/unbelievable.html b/pelican/tests/output/custom/unbelievable.html
new file mode 100644
index 00000000..0132162c
--- /dev/null
+++ b/pelican/tests/output/custom/unbelievable.html
@@ -0,0 +1,144 @@
+
+
+
+
+
+ Unbelievable !
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: Fri 15 October 2010
+
+
+
+ By Alexis Métaireau
+
+In misc .
+
+ Or completely awesome. Depends the needs.
+
a root-relative link to markdown-article
+a file-relative link to markdown-article
+
+
Testing sourcecode directive
+
formatter = self . options and VARIANTS [ self . options . keys ()[ 0 ]]
+
+
+
+
Testing another case
+
This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will have nothing in default.
+
formatter = self . options and VARIANTS [ self . options . keys ()[ 0 ]]
+
+
Lovely.
+
+
+
Testing more sourcecode directives
+
8 def run ( self ): self . assert_has_content () 10 try : lexer = get_lexer_by_name ( self . arguments [ 0 ]) 12 except ValueError : # no lexer found - use the text one instead of an exception 14 lexer = TextLexer () 16 if ( 'linenos' in self . options and self . options [ 'linenos' ] not in ( 'table' , 'inline' )): 18 self . options [ 'linenos' ] = 'table' 20 for flag in ( 'nowrap' , 'nobackground' , 'anchorlinenos' ): if flag in self . options : 22 self . options [ flag ] = True 24 # noclasses should already default to False, but just in case... formatter = HtmlFormatter ( noclasses = False , ** self . options ) 26 parsed = highlight ( ' \n ' . join ( self . content ), lexer , formatter ) return [ nodes . raw ( '' , parsed , format = 'html' )]
+
Lovely.
+
+
+
Testing even more sourcecode directives
+
formatter = self . options and VARIANTS [ self . options . keys ()[ 0 ]]
+
Lovely.
+
+
+
Testing overriding config defaults
+
Even if the default is line numbers, we can override it here
+
formatter = self . options and VARIANTS [ self . options . keys ()[ 0 ]]
+
+
Lovely.
+
+
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/archives.html b/pelican/tests/output/custom_locale/archives.html
new file mode 100644
index 00000000..66dbe94a
--- /dev/null
+++ b/pelican/tests/output/custom_locale/archives.html
@@ -0,0 +1,97 @@
+
+
+
+
+
+ Alexis' log
+
+
+
+
+
+
+
+
+
+
+
+Archives for Alexis' log
+
+
+ 30 novembre 2012
+ FILENAME_METADATA example
+ 29 février 2012
+ Second article
+ 20 avril 2011
+ A markdown powered article
+ 17 février 2011
+ Article 1
+ 17 février 2011
+ Article 2
+ 17 février 2011
+ Article 3
+ 02 décembre 2010
+ This is a super article !
+ 20 octobre 2010
+ Oh yeah !
+ 15 octobre 2010
+ Unbelievable !
+ 14 mars 2010
+ The baz tag
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/author/alexis-metaireau.html b/pelican/tests/output/custom_locale/author/alexis-metaireau.html
new file mode 100644
index 00000000..9b4ca7dd
--- /dev/null
+++ b/pelican/tests/output/custom_locale/author/alexis-metaireau.html
@@ -0,0 +1,171 @@
+
+
+
+
+
+ Alexis' log - Alexis Métaireau
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: 30 novembre 2012
+
+
+
+ By Alexis Métaireau
+
+In misc .
+
+ Some cool stuff!
+There are comments .
+
+
+ Other articles
+
+
+
+
+
+
+
+
+
+ Published: 29 février 2012
+
+
+
+ By Alexis Métaireau
+
+In misc .
+tags: foo bar baz
Translations:
+ fr
+
+ This is some article, in english
+
+
read more
+
There are comments .
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: 17 février 2011
+
+
+
+ By Alexis Métaireau
+
+In cat1 .
+
+ Article 1
+
+
read more
+
There are comments .
+
+
+
+ Page 1 / 3
+ »
+ ⇉
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
diff --git a/pelican/tests/output/custom_locale/author/alexis-metaireau2.html b/pelican/tests/output/custom_locale/author/alexis-metaireau2.html
new file mode 100644
index 00000000..a1848677
--- /dev/null
+++ b/pelican/tests/output/custom_locale/author/alexis-metaireau2.html
@@ -0,0 +1,186 @@
+
+
+
+
+
+ Alexis' log - Alexis Métaireau
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: 17 février 2011
+
+
+
+ By Alexis Métaireau
+
+In cat1 .
+
+ Article 2
+
+
read more
+
There are comments .
+
+
+
+
+
+
+
+
+ Published: 17 février 2011
+
+
+
+ By Alexis Métaireau
+
+In cat1 .
+
+ Article 3
+
+
read more
+
There are comments .
+
+
+
+
+
+
+
+
+ Published: 02 décembre 2010
+
+
+
+ Updated: 17 novembre 2013
+
+
+
+ By Alexis Métaireau
+
+In yeah .
+tags: foo bar foobar
+ Multi-line metadata should be supported
+as well as inline markup .
+
+
read more
+
There are comments .
+
+
+
+
+
+
+
+
+ Published: 20 octobre 2010
+
+
+
+ By Alexis Métaireau
+
+In bar .
+tags: oh bar yeah
Translations:
+ fr
+
+
+
Why not ?
+
After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !
+
+
+
+
read more
+
There are comments .
+
+
+
+ ⇇
+ «
+ Page 2 / 3
+ »
+ ⇉
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
diff --git a/pelican/tests/output/custom_locale/author/alexis-metaireau3.html b/pelican/tests/output/custom_locale/author/alexis-metaireau3.html
new file mode 100644
index 00000000..78fdd866
--- /dev/null
+++ b/pelican/tests/output/custom_locale/author/alexis-metaireau3.html
@@ -0,0 +1,136 @@
+
+
+
+
+
+ Alexis' log - Alexis Métaireau
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: 15 octobre 2010
+
+
+
+ By Alexis Métaireau
+
+In misc .
+
+ Or completely awesome. Depends the needs.
+
a root-relative link to markdown-article
+a file-relative link to markdown-article
+
+
Testing sourcecode directive
+
formatter = self . options and VARIANTS [ self . options . keys ()[ 0 ]]
+
+
+
+
Testing another case
+
This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will …
+
read more
+
There are comments .
+
+
+
+
+
+
+
+
+ Published: 14 mars 2010
+
+
+
+ By Alexis Métaireau
+
+In misc .
+
+ This article overrides the listening of the articles under the baz tag.
+
+
read more
+
There are comments .
+
+
+
+ ⇇
+ «
+ Page 3 / 3
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
diff --git a/pelican/tests/output/custom_locale/authors.html b/pelican/tests/output/custom_locale/authors.html
new file mode 100644
index 00000000..84bd502e
--- /dev/null
+++ b/pelican/tests/output/custom_locale/authors.html
@@ -0,0 +1,79 @@
+
+
+
+
+
+ Alexis' log - Authors
+
+
+
+
+
+
+
+
+
+
+
+
+ Authors on Alexis' log
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/categories.html b/pelican/tests/output/custom_locale/categories.html
new file mode 100644
index 00000000..24e83abf
--- /dev/null
+++ b/pelican/tests/output/custom_locale/categories.html
@@ -0,0 +1,78 @@
+
+
+
+
+
+ Alexis' log - Categories
+
+
+
+
+
+
+
+
+
+
+ Categories on Alexis' log
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/category/bar.html b/pelican/tests/output/custom_locale/category/bar.html
new file mode 100644
index 00000000..f9d47948
--- /dev/null
+++ b/pelican/tests/output/custom_locale/category/bar.html
@@ -0,0 +1,95 @@
+
+
+
+
+
+ Alexis' log - bar
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: 20 octobre 2010
+
+
+
+ By Alexis Métaireau
+
+In bar .
+tags: oh bar yeah
Translations:
+ fr
+
+
+
Why not ?
+
After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !
+
+
+There are comments .
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/category/cat1.html b/pelican/tests/output/custom_locale/category/cat1.html
new file mode 100644
index 00000000..2bc756e6
--- /dev/null
+++ b/pelican/tests/output/custom_locale/category/cat1.html
@@ -0,0 +1,164 @@
+
+
+
+
+
+ Alexis' log - cat1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: 20 avril 2011
+
+
+
+ By Alexis Métaireau
+
+In cat1 .
+
+ You're mutually oblivious.
+a root-relative link to unbelievable
+a file-relative link to unbelievable
There are comments .
+
+
+ Other articles
+
+
+
+
+
+
+
+
+
+ Published: 17 février 2011
+
+
+
+ By Alexis Métaireau
+
+In cat1 .
+
+ Article 1
+
+
read more
+
There are comments .
+
+
+
+
+
+
+
+
+ Published: 17 février 2011
+
+
+
+ By Alexis Métaireau
+
+In cat1 .
+
+ Article 2
+
+
read more
+
There are comments .
+
+
+
+
+
+
+
+
+ Published: 17 février 2011
+
+
+
+ By Alexis Métaireau
+
+In cat1 .
+
+ Article 3
+
+
read more
+
There are comments .
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/category/misc.html b/pelican/tests/output/custom_locale/category/misc.html
new file mode 100644
index 00000000..c9fc111f
--- /dev/null
+++ b/pelican/tests/output/custom_locale/category/misc.html
@@ -0,0 +1,175 @@
+
+
+
+
+
+ Alexis' log - misc
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: 30 novembre 2012
+
+
+
+ By Alexis Métaireau
+
+In misc .
+
+ Some cool stuff!
+There are comments .
+
+
+ Other articles
+
+
+
+
+
+
+
+
+
+ Published: 29 février 2012
+
+
+
+ By Alexis Métaireau
+
+In misc .
+tags: foo bar baz
Translations:
+ fr
+
+ This is some article, in english
+
+
read more
+
There are comments .
+
+
+
+
+
+
+
+
+ Published: 15 octobre 2010
+
+
+
+ By Alexis Métaireau
+
+In misc .
+
+ Or completely awesome. Depends the needs.
+
a root-relative link to markdown-article
+a file-relative link to markdown-article
+
+
Testing sourcecode directive
+
formatter = self . options and VARIANTS [ self . options . keys ()[ 0 ]]
+
+
+
+
Testing another case
+
This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will …
+
read more
+
There are comments .
+
+
+
+
+
+
+
+
+ Published: 14 mars 2010
+
+
+
+ By Alexis Métaireau
+
+In misc .
+
+ This article overrides the listening of the articles under the baz tag.
+
+
read more
+
There are comments .
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/category/yeah.html b/pelican/tests/output/custom_locale/category/yeah.html
new file mode 100644
index 00000000..660c593c
--- /dev/null
+++ b/pelican/tests/output/custom_locale/category/yeah.html
@@ -0,0 +1,103 @@
+
+
+
+
+
+ Alexis' log - yeah
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: 02 décembre 2010
+
+
+
+ Updated: 17 novembre 2013
+
+
+
+ By Alexis Métaireau
+
+In yeah .
+tags: foo bar foobar
+ Some content here !
+
+
This is a simple title
+
And here comes the cool stuff .
+
+
+
+>>> from ipdb import set_trace
+>>> set_trace()
+
+
→ And now try with some utf8 hell: ééé
+
+There are comments .
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/drafts/a-draft-article-without-date.html b/pelican/tests/output/custom_locale/drafts/a-draft-article-without-date.html
new file mode 100644
index 00000000..c289ec72
--- /dev/null
+++ b/pelican/tests/output/custom_locale/drafts/a-draft-article-without-date.html
@@ -0,0 +1,98 @@
+
+
+
+
+
+ A draft article without date
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: 02 mars 2012
+
+
+
+ By Alexis Métaireau
+
+In misc .
+
+ This is a draft article, it should live under the /drafts/ folder and not be
+listed anywhere else.
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/drafts/a-draft-article.html b/pelican/tests/output/custom_locale/drafts/a-draft-article.html
new file mode 100644
index 00000000..a166efdf
--- /dev/null
+++ b/pelican/tests/output/custom_locale/drafts/a-draft-article.html
@@ -0,0 +1,98 @@
+
+
+
+
+
+ A draft article
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: 08 mai 2011
+
+
+
+ By Alexis Métaireau
+
+In misc .
+
+ This is a draft article, it should live under the /drafts/ folder and not be
+listed anywhere else.
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/feeds/alexis-metaireau.atom.xml b/pelican/tests/output/custom_locale/feeds/alexis-metaireau.atom.xml
new file mode 100644
index 00000000..5e5b888a
--- /dev/null
+++ b/pelican/tests/output/custom_locale/feeds/alexis-metaireau.atom.xml
@@ -0,0 +1,74 @@
+
+Alexis' log - Alexis Métaireau http://blog.notmyidea.org/ 2013-11-17T23:29:00+01:00 FILENAME_METADATA example 2012-11-30T00:00:00+01:00 2012-11-30T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2012-11-30:/posts/2012/novembre/30/filename_metadata-example/ <p>Some cool stuff!</p>
+ Second article 2012-02-29T00:00:00+01:00 2012-02-29T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2012-02-29:/posts/2012/février/29/second-article/ <p>This is some article, in english</p>
+ A markdown powered article 2011-04-20T00:00:00+02:00 2011-04-20T00:00:00+02:00 Alexis Métaireau tag:blog.notmyidea.org,2011-04-20:/posts/2011/avril/20/a-markdown-powered-article/ <p>You're mutually oblivious.</p>
+<p><a href="http://blog.notmyidea.org/posts/2010/octobre/15/unbelievable/">a root-relative link to unbelievable</a>
+<a href="http://blog.notmyidea.org/posts/2010/octobre/15/unbelievable/">a file-relative link to unbelievable</a></p> Article 1 2011-02-17T00:00:00+01:00 2011-02-17T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2011-02-17:/posts/2011/février/17/article-1/ <p>Article 1</p>
+ Article 2 2011-02-17T00:00:00+01:00 2011-02-17T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2011-02-17:/posts/2011/février/17/article-2/ <p>Article 2</p>
+ Article 3 2011-02-17T00:00:00+01:00 2011-02-17T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2011-02-17:/posts/2011/février/17/article-3/ <p>Article 3</p>
+ This is a super article ! 2010-12-02T10:14:00+01:00 2013-11-17T23:29:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2010-12-02:/posts/2010/décembre/02/this-is-a-super-article/ <p class="first last">Multi-line metadata should be supported
+as well as <strong>inline markup</strong>.</p>
+ <p>Some content here !</p>
+<div class="section" id="this-is-a-simple-title">
+<h2>This is a simple title</h2>
+<p>And here comes the cool <a class="reference external" href="http://books.couchdb.org/relax/design-documents/views">stuff</a>.</p>
+<img alt="alternate text" src="http://blog.notmyidea.org/pictures/Sushi.jpg" style="width: 600px; height: 450px;" />
+<img alt="alternate text" src="http://blog.notmyidea.org/pictures/Sushi_Macro.jpg" style="width: 600px; height: 450px;" />
+<pre class="literal-block">
+>>> from ipdb import set_trace
+>>> set_trace()
+</pre>
+<p>→ And now try with some utf8 hell: ééé</p>
+</div>
+ Oh yeah ! 2010-10-20T10:14:00+02:00 2010-10-20T10:14:00+02:00 Alexis Métaireau tag:blog.notmyidea.org,2010-10-20:/posts/2010/octobre/20/oh-yeah/ <div class="section" id="why-not">
+<h2>Why not ?</h2>
+<p>After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !</p>
+<img alt="alternate text" src="http://blog.notmyidea.org/pictures/Sushi.jpg" style="width: 600px; height: 450px;" />
+</div>
+ Unbelievable ! 2010-10-15T20:30:00+02:00 2010-10-15T20:30:00+02:00 Alexis Métaireau tag:blog.notmyidea.org,2010-10-15:/posts/2010/octobre/15/unbelievable/ <p>Or completely awesome. Depends the needs.</p>
+<p><a class="reference external" href="http://blog.notmyidea.org/posts/2011/avril/20/a-markdown-powered-article/">a root-relative link to markdown-article</a>
+<a class="reference external" href="http://blog.notmyidea.org/posts/2011/avril/20/a-markdown-powered-article/">a file-relative link to markdown-article</a></p>
+<div class="section" id="testing-sourcecode-directive">
+<h2>Testing sourcecode directive</h2>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table></div>
+<div class="section" id="testing-another-case">
+<h2>Testing another case</h2>
+<p>This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will …</p></div> <p>Or completely awesome. Depends the needs.</p>
+<p><a class="reference external" href="http://blog.notmyidea.org/posts/2011/avril/20/a-markdown-powered-article/">a root-relative link to markdown-article</a>
+<a class="reference external" href="http://blog.notmyidea.org/posts/2011/avril/20/a-markdown-powered-article/">a file-relative link to markdown-article</a></p>
+<div class="section" id="testing-sourcecode-directive">
+<h2>Testing sourcecode directive</h2>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table></div>
+<div class="section" id="testing-another-case">
+<h2>Testing another case</h2>
+<p>This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will have nothing in default.</p>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table><p>Lovely.</p>
+</div>
+<div class="section" id="testing-more-sourcecode-directives">
+<h2>Testing more sourcecode directives</h2>
+<div class="highlight"><pre><span></span><span id="foo-8"><a name="foo-8"></a><span class="lineno special"> 8 </span><span class="testingk">def</span> <span class="testingnf">run</span><span class="testingp">(</span><span class="testingbp">self</span><span class="testingp">):</span><br></span><span id="foo-9"><a name="foo-9"></a><span class="lineno"> </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">assert_has_content</span><span class="testingp">()</span><br></span><span id="foo-10"><a name="foo-10"></a><span class="lineno special">10 </span> <span class="testingk">try</span><span class="testingp">:</span><br></span><span id="foo-11"><a name="foo-11"></a><span class="lineno"> </span> <span class="testingn">lexer</span> <span class="testingo">=</span> <span class="testingn">get_lexer_by_name</span><span class="testingp">(</span><span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">arguments</span><span class="testingp">[</span><span class="testingmi">0</span><span class="testingp">])</span><br></span><span id="foo-12"><a name="foo-12"></a><span class="lineno special">12 </span> <span class="testingk">except</span> <span class="testingne">ValueError</span><span class="testingp">:</span><br></span><span id="foo-13"><a name="foo-13"></a><span class="lineno"> </span> <span class="testingc1"># no lexer found - use the text one instead of an exception</span><br></span><span id="foo-14"><a name="foo-14"></a><span class="lineno special">14 </span> <span class="testingn">lexer</span> <span class="testingo">=</span> <span class="testingn">TextLexer</span><span class="testingp">()</span><br></span><span id="foo-15"><a name="foo-15"></a><span class="lineno"> </span><br></span><span id="foo-16"><a name="foo-16"></a><span class="lineno special">16 </span> <span class="testingk">if</span> <span class="testingp">(</span><span class="testings1">'linenos'</span> <span class="testingow">in</span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span> <span class="testingow">and</span><br></span><span id="foo-17"><a name="foo-17"></a><span class="lineno"> </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">[</span><span class="testings1">'linenos'</span><span class="testingp">]</span> <span class="testingow">not</span> <span class="testingow">in</span> <span class="testingp">(</span><span class="testings1">'table'</span><span class="testingp">,</span> <span class="testings1">'inline'</span><span class="testingp">)):</span><br></span><span id="foo-18"><a name="foo-18"></a><span class="lineno special">18 </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">[</span><span class="testings1">'linenos'</span><span class="testingp">]</span> <span class="testingo">=</span> <span class="testings1">'table'</span><br></span><span id="foo-19"><a name="foo-19"></a><span class="lineno"> </span><br></span><span id="foo-20"><a name="foo-20"></a><span class="lineno special">20 </span> <span class="testingk">for</span> <span class="testingn">flag</span> <span class="testingow">in</span> <span class="testingp">(</span><span class="testings1">'nowrap'</span><span class="testingp">,</span> <span class="testings1">'nobackground'</span><span class="testingp">,</span> <span class="testings1">'anchorlinenos'</span><span class="testingp">):</span><br></span><span id="foo-21"><a name="foo-21"></a><span class="lineno"> </span> <span class="testingk">if</span> <span class="testingn">flag</span> <span class="testingow">in</span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">:</span><br></span><span id="foo-22"><a name="foo-22"></a><span class="lineno special">22 </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">[</span><span class="testingn">flag</span><span class="testingp">]</span> <span class="testingo">=</span> <span class="testingkc">True</span><br></span><span id="foo-23"><a name="foo-23"></a><span class="lineno"> </span><br></span><span id="foo-24"><a name="foo-24"></a><span class="lineno special">24 </span> <span class="testingc1"># noclasses should already default to False, but just in case...</span><br></span><span id="foo-25"><a name="foo-25"></a><span class="lineno"> </span> <span class="testingn">formatter</span> <span class="testingo">=</span> <span class="testingn">HtmlFormatter</span><span class="testingp">(</span><span class="testingn">noclasses</span><span class="testingo">=</span><span class="testingkc">False</span><span class="testingp">,</span> <span class="testingo">**</span><span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">)</span><br></span><span id="foo-26"><a name="foo-26"></a><span class="lineno special">26 </span> <span class="testingn">parsed</span> <span class="testingo">=</span> <span class="testingn">highlight</span><span class="testingp">(</span><span class="testings1">'</span><span class="testingse">\n</span><span class="testings1">'</span><span class="testingo">.</span><span class="testingn">join</span><span class="testingp">(</span><span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">content</span><span class="testingp">),</span> <span class="testingn">lexer</span><span class="testingp">,</span> <span class="testingn">formatter</span><span class="testingp">)</span><br></span><span id="foo-27"><a name="foo-27"></a><span class="lineno"> </span> <span class="testingk">return</span> <span class="testingp">[</span><span class="testingn">nodes</span><span class="testingo">.</span><span class="testingn">raw</span><span class="testingp">(</span><span class="testings1">''</span><span class="testingp">,</span> <span class="testingn">parsed</span><span class="testingp">,</span> <span class="testingnb">format</span><span class="testingo">=</span><span class="testings1">'html'</span><span class="testingp">)]</span><br></span></pre></div>
+<p>Lovely.</p>
+</div>
+<div class="section" id="testing-even-more-sourcecode-directives">
+<h2>Testing even more sourcecode directives</h2>
+<span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+<p>Lovely.</p>
+</div>
+<div class="section" id="testing-overriding-config-defaults">
+<h2>Testing overriding config defaults</h2>
+<p>Even if the default is line numbers, we can override it here</p>
+<div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+<p>Lovely.</p>
+</div>
+ The baz tag 2010-03-14T00:00:00+01:00 2010-03-14T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2010-03-14:/tag/baz.html <p>This article overrides the listening of the articles under the <em>baz</em> tag.</p>
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/feeds/alexis-metaireau.rss.xml b/pelican/tests/output/custom_locale/feeds/alexis-metaireau.rss.xml
new file mode 100644
index 00000000..6cbd64db
--- /dev/null
+++ b/pelican/tests/output/custom_locale/feeds/alexis-metaireau.rss.xml
@@ -0,0 +1,29 @@
+
+Alexis' log - Alexis Métaireau http://blog.notmyidea.org/Sun, 17 Nov 2013 23:29:00 +0100 FILENAME_METADATA example http://blog.notmyidea.org/posts/2012/novembre/30/filename_metadata-example/<p>Some cool stuff!</p>
+ Alexis Métaireau Fri, 30 Nov 2012 00:00:00 +0100 tag:blog.notmyidea.org,2012-11-30:/posts/2012/novembre/30/filename_metadata-example/ misc Second article http://blog.notmyidea.org/posts/2012/f%C3%A9vrier/29/second-article/<p>This is some article, in english</p>
+ Alexis Métaireau Wed, 29 Feb 2012 00:00:00 +0100 tag:blog.notmyidea.org,2012-02-29:/posts/2012/février/29/second-article/ misc foo bar baz A markdown powered article http://blog.notmyidea.org/posts/2011/avril/20/a-markdown-powered-article/<p>You're mutually oblivious.</p>
+<p><a href="http://blog.notmyidea.org/posts/2010/octobre/15/unbelievable/">a root-relative link to unbelievable</a>
+<a href="http://blog.notmyidea.org/posts/2010/octobre/15/unbelievable/">a file-relative link to unbelievable</a></p> Alexis Métaireau Wed, 20 Apr 2011 00:00:00 +0200 tag:blog.notmyidea.org,2011-04-20:/posts/2011/avril/20/a-markdown-powered-article/ cat1 Article 1 http://blog.notmyidea.org/posts/2011/f%C3%A9vrier/17/article-1/<p>Article 1</p>
+ Alexis Métaireau Thu, 17 Feb 2011 00:00:00 +0100 tag:blog.notmyidea.org,2011-02-17:/posts/2011/février/17/article-1/ cat1 Article 2 http://blog.notmyidea.org/posts/2011/f%C3%A9vrier/17/article-2/<p>Article 2</p>
+ Alexis Métaireau Thu, 17 Feb 2011 00:00:00 +0100 tag:blog.notmyidea.org,2011-02-17:/posts/2011/février/17/article-2/ cat1 Article 3 http://blog.notmyidea.org/posts/2011/f%C3%A9vrier/17/article-3/<p>Article 3</p>
+ Alexis Métaireau Thu, 17 Feb 2011 00:00:00 +0100 tag:blog.notmyidea.org,2011-02-17:/posts/2011/février/17/article-3/ cat1 This is a super article ! http://blog.notmyidea.org/posts/2010/d%C3%A9cembre/02/this-is-a-super-article/<p class="first last">Multi-line metadata should be supported
+as well as <strong>inline markup</strong>.</p>
+ Alexis Métaireau Thu, 02 Dec 2010 10:14:00 +0100 tag:blog.notmyidea.org,2010-12-02:/posts/2010/décembre/02/this-is-a-super-article/ yeah foo bar foobar Oh yeah ! http://blog.notmyidea.org/posts/2010/octobre/20/oh-yeah/<div class="section" id="why-not">
+<h2>Why not ?</h2>
+<p>After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !</p>
+<img alt="alternate text" src="http://blog.notmyidea.org/pictures/Sushi.jpg" style="width: 600px; height: 450px;" />
+</div>
+ Alexis Métaireau Wed, 20 Oct 2010 10:14:00 +0200 tag:blog.notmyidea.org,2010-10-20:/posts/2010/octobre/20/oh-yeah/ bar oh bar yeah Unbelievable ! http://blog.notmyidea.org/posts/2010/octobre/15/unbelievable/<p>Or completely awesome. Depends the needs.</p>
+<p><a class="reference external" href="http://blog.notmyidea.org/posts/2011/avril/20/a-markdown-powered-article/">a root-relative link to markdown-article</a>
+<a class="reference external" href="http://blog.notmyidea.org/posts/2011/avril/20/a-markdown-powered-article/">a file-relative link to markdown-article</a></p>
+<div class="section" id="testing-sourcecode-directive">
+<h2>Testing sourcecode directive</h2>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table></div>
+<div class="section" id="testing-another-case">
+<h2>Testing another case</h2>
+<p>This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will …</p></div> Alexis Métaireau Fri, 15 Oct 2010 20:30:00 +0200 tag:blog.notmyidea.org,2010-10-15:/posts/2010/octobre/15/unbelievable/ misc The baz tag http://blog.notmyidea.org/tag/baz.html<p>This article overrides the listening of the articles under the <em>baz</em> tag.</p>
+ Alexis Métaireau Sun, 14 Mar 2010 00:00:00 +0100 tag:blog.notmyidea.org,2010-03-14:/tag/baz.html misc
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/feeds/all-en.atom.xml b/pelican/tests/output/custom_locale/feeds/all-en.atom.xml
new file mode 100644
index 00000000..1b6dc18f
--- /dev/null
+++ b/pelican/tests/output/custom_locale/feeds/all-en.atom.xml
@@ -0,0 +1,74 @@
+
+Alexis' log http://blog.notmyidea.org/ 2013-11-17T23:29:00+01:00 FILENAME_METADATA example 2012-11-30T00:00:00+01:00 2012-11-30T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2012-11-30:/posts/2012/novembre/30/filename_metadata-example/ <p>Some cool stuff!</p>
+ Second article 2012-02-29T00:00:00+01:00 2012-02-29T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2012-02-29:/posts/2012/février/29/second-article/ <p>This is some article, in english</p>
+ A markdown powered article 2011-04-20T00:00:00+02:00 2011-04-20T00:00:00+02:00 Alexis Métaireau tag:blog.notmyidea.org,2011-04-20:/posts/2011/avril/20/a-markdown-powered-article/ <p>You're mutually oblivious.</p>
+<p><a href="http://blog.notmyidea.org/posts/2010/octobre/15/unbelievable/">a root-relative link to unbelievable</a>
+<a href="http://blog.notmyidea.org/posts/2010/octobre/15/unbelievable/">a file-relative link to unbelievable</a></p> Article 1 2011-02-17T00:00:00+01:00 2011-02-17T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2011-02-17:/posts/2011/février/17/article-1/ <p>Article 1</p>
+ Article 2 2011-02-17T00:00:00+01:00 2011-02-17T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2011-02-17:/posts/2011/février/17/article-2/ <p>Article 2</p>
+ Article 3 2011-02-17T00:00:00+01:00 2011-02-17T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2011-02-17:/posts/2011/février/17/article-3/ <p>Article 3</p>
+ This is a super article ! 2010-12-02T10:14:00+01:00 2013-11-17T23:29:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2010-12-02:/posts/2010/décembre/02/this-is-a-super-article/ <p class="first last">Multi-line metadata should be supported
+as well as <strong>inline markup</strong>.</p>
+ <p>Some content here !</p>
+<div class="section" id="this-is-a-simple-title">
+<h2>This is a simple title</h2>
+<p>And here comes the cool <a class="reference external" href="http://books.couchdb.org/relax/design-documents/views">stuff</a>.</p>
+<img alt="alternate text" src="http://blog.notmyidea.org/pictures/Sushi.jpg" style="width: 600px; height: 450px;" />
+<img alt="alternate text" src="http://blog.notmyidea.org/pictures/Sushi_Macro.jpg" style="width: 600px; height: 450px;" />
+<pre class="literal-block">
+>>> from ipdb import set_trace
+>>> set_trace()
+</pre>
+<p>→ And now try with some utf8 hell: ééé</p>
+</div>
+ Oh yeah ! 2010-10-20T10:14:00+02:00 2010-10-20T10:14:00+02:00 Alexis Métaireau tag:blog.notmyidea.org,2010-10-20:/posts/2010/octobre/20/oh-yeah/ <div class="section" id="why-not">
+<h2>Why not ?</h2>
+<p>After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !</p>
+<img alt="alternate text" src="http://blog.notmyidea.org/pictures/Sushi.jpg" style="width: 600px; height: 450px;" />
+</div>
+ Unbelievable ! 2010-10-15T20:30:00+02:00 2010-10-15T20:30:00+02:00 Alexis Métaireau tag:blog.notmyidea.org,2010-10-15:/posts/2010/octobre/15/unbelievable/ <p>Or completely awesome. Depends the needs.</p>
+<p><a class="reference external" href="http://blog.notmyidea.org/posts/2011/avril/20/a-markdown-powered-article/">a root-relative link to markdown-article</a>
+<a class="reference external" href="http://blog.notmyidea.org/posts/2011/avril/20/a-markdown-powered-article/">a file-relative link to markdown-article</a></p>
+<div class="section" id="testing-sourcecode-directive">
+<h2>Testing sourcecode directive</h2>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table></div>
+<div class="section" id="testing-another-case">
+<h2>Testing another case</h2>
+<p>This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will …</p></div> <p>Or completely awesome. Depends the needs.</p>
+<p><a class="reference external" href="http://blog.notmyidea.org/posts/2011/avril/20/a-markdown-powered-article/">a root-relative link to markdown-article</a>
+<a class="reference external" href="http://blog.notmyidea.org/posts/2011/avril/20/a-markdown-powered-article/">a file-relative link to markdown-article</a></p>
+<div class="section" id="testing-sourcecode-directive">
+<h2>Testing sourcecode directive</h2>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table></div>
+<div class="section" id="testing-another-case">
+<h2>Testing another case</h2>
+<p>This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will have nothing in default.</p>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table><p>Lovely.</p>
+</div>
+<div class="section" id="testing-more-sourcecode-directives">
+<h2>Testing more sourcecode directives</h2>
+<div class="highlight"><pre><span></span><span id="foo-8"><a name="foo-8"></a><span class="lineno special"> 8 </span><span class="testingk">def</span> <span class="testingnf">run</span><span class="testingp">(</span><span class="testingbp">self</span><span class="testingp">):</span><br></span><span id="foo-9"><a name="foo-9"></a><span class="lineno"> </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">assert_has_content</span><span class="testingp">()</span><br></span><span id="foo-10"><a name="foo-10"></a><span class="lineno special">10 </span> <span class="testingk">try</span><span class="testingp">:</span><br></span><span id="foo-11"><a name="foo-11"></a><span class="lineno"> </span> <span class="testingn">lexer</span> <span class="testingo">=</span> <span class="testingn">get_lexer_by_name</span><span class="testingp">(</span><span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">arguments</span><span class="testingp">[</span><span class="testingmi">0</span><span class="testingp">])</span><br></span><span id="foo-12"><a name="foo-12"></a><span class="lineno special">12 </span> <span class="testingk">except</span> <span class="testingne">ValueError</span><span class="testingp">:</span><br></span><span id="foo-13"><a name="foo-13"></a><span class="lineno"> </span> <span class="testingc1"># no lexer found - use the text one instead of an exception</span><br></span><span id="foo-14"><a name="foo-14"></a><span class="lineno special">14 </span> <span class="testingn">lexer</span> <span class="testingo">=</span> <span class="testingn">TextLexer</span><span class="testingp">()</span><br></span><span id="foo-15"><a name="foo-15"></a><span class="lineno"> </span><br></span><span id="foo-16"><a name="foo-16"></a><span class="lineno special">16 </span> <span class="testingk">if</span> <span class="testingp">(</span><span class="testings1">'linenos'</span> <span class="testingow">in</span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span> <span class="testingow">and</span><br></span><span id="foo-17"><a name="foo-17"></a><span class="lineno"> </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">[</span><span class="testings1">'linenos'</span><span class="testingp">]</span> <span class="testingow">not</span> <span class="testingow">in</span> <span class="testingp">(</span><span class="testings1">'table'</span><span class="testingp">,</span> <span class="testings1">'inline'</span><span class="testingp">)):</span><br></span><span id="foo-18"><a name="foo-18"></a><span class="lineno special">18 </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">[</span><span class="testings1">'linenos'</span><span class="testingp">]</span> <span class="testingo">=</span> <span class="testings1">'table'</span><br></span><span id="foo-19"><a name="foo-19"></a><span class="lineno"> </span><br></span><span id="foo-20"><a name="foo-20"></a><span class="lineno special">20 </span> <span class="testingk">for</span> <span class="testingn">flag</span> <span class="testingow">in</span> <span class="testingp">(</span><span class="testings1">'nowrap'</span><span class="testingp">,</span> <span class="testings1">'nobackground'</span><span class="testingp">,</span> <span class="testings1">'anchorlinenos'</span><span class="testingp">):</span><br></span><span id="foo-21"><a name="foo-21"></a><span class="lineno"> </span> <span class="testingk">if</span> <span class="testingn">flag</span> <span class="testingow">in</span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">:</span><br></span><span id="foo-22"><a name="foo-22"></a><span class="lineno special">22 </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">[</span><span class="testingn">flag</span><span class="testingp">]</span> <span class="testingo">=</span> <span class="testingkc">True</span><br></span><span id="foo-23"><a name="foo-23"></a><span class="lineno"> </span><br></span><span id="foo-24"><a name="foo-24"></a><span class="lineno special">24 </span> <span class="testingc1"># noclasses should already default to False, but just in case...</span><br></span><span id="foo-25"><a name="foo-25"></a><span class="lineno"> </span> <span class="testingn">formatter</span> <span class="testingo">=</span> <span class="testingn">HtmlFormatter</span><span class="testingp">(</span><span class="testingn">noclasses</span><span class="testingo">=</span><span class="testingkc">False</span><span class="testingp">,</span> <span class="testingo">**</span><span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">)</span><br></span><span id="foo-26"><a name="foo-26"></a><span class="lineno special">26 </span> <span class="testingn">parsed</span> <span class="testingo">=</span> <span class="testingn">highlight</span><span class="testingp">(</span><span class="testings1">'</span><span class="testingse">\n</span><span class="testings1">'</span><span class="testingo">.</span><span class="testingn">join</span><span class="testingp">(</span><span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">content</span><span class="testingp">),</span> <span class="testingn">lexer</span><span class="testingp">,</span> <span class="testingn">formatter</span><span class="testingp">)</span><br></span><span id="foo-27"><a name="foo-27"></a><span class="lineno"> </span> <span class="testingk">return</span> <span class="testingp">[</span><span class="testingn">nodes</span><span class="testingo">.</span><span class="testingn">raw</span><span class="testingp">(</span><span class="testings1">''</span><span class="testingp">,</span> <span class="testingn">parsed</span><span class="testingp">,</span> <span class="testingnb">format</span><span class="testingo">=</span><span class="testings1">'html'</span><span class="testingp">)]</span><br></span></pre></div>
+<p>Lovely.</p>
+</div>
+<div class="section" id="testing-even-more-sourcecode-directives">
+<h2>Testing even more sourcecode directives</h2>
+<span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+<p>Lovely.</p>
+</div>
+<div class="section" id="testing-overriding-config-defaults">
+<h2>Testing overriding config defaults</h2>
+<p>Even if the default is line numbers, we can override it here</p>
+<div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+<p>Lovely.</p>
+</div>
+ The baz tag 2010-03-14T00:00:00+01:00 2010-03-14T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2010-03-14:/tag/baz.html <p>This article overrides the listening of the articles under the <em>baz</em> tag.</p>
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/feeds/all-fr.atom.xml b/pelican/tests/output/custom_locale/feeds/all-fr.atom.xml
new file mode 100644
index 00000000..cfd6eeb2
--- /dev/null
+++ b/pelican/tests/output/custom_locale/feeds/all-fr.atom.xml
@@ -0,0 +1,4 @@
+
+Alexis' log http://blog.notmyidea.org/ 2012-02-29T00:00:00+01:00 Deuxième article 2012-02-29T00:00:00+01:00 2012-02-29T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2012-02-29:/second-article-fr.html <p>Ceci est un article, en français.</p>
+ Trop bien ! 2010-10-20T10:14:00+02:00 2010-10-20T10:14:00+02:00 Alexis Métaireau tag:blog.notmyidea.org,2010-10-20:/oh-yeah-fr.html <p>Et voila du contenu en français</p>
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/feeds/all.atom.xml b/pelican/tests/output/custom_locale/feeds/all.atom.xml
new file mode 100644
index 00000000..67a57122
--- /dev/null
+++ b/pelican/tests/output/custom_locale/feeds/all.atom.xml
@@ -0,0 +1,76 @@
+
+Alexis' log http://blog.notmyidea.org/ 2013-11-17T23:29:00+01:00 FILENAME_METADATA example 2012-11-30T00:00:00+01:00 2012-11-30T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2012-11-30:/posts/2012/novembre/30/filename_metadata-example/ <p>Some cool stuff!</p>
+ Second article 2012-02-29T00:00:00+01:00 2012-02-29T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2012-02-29:/posts/2012/février/29/second-article/ <p>This is some article, in english</p>
+ Deuxième article 2012-02-29T00:00:00+01:00 2012-02-29T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2012-02-29:/second-article-fr.html <p>Ceci est un article, en français.</p>
+ A markdown powered article 2011-04-20T00:00:00+02:00 2011-04-20T00:00:00+02:00 Alexis Métaireau tag:blog.notmyidea.org,2011-04-20:/posts/2011/avril/20/a-markdown-powered-article/ <p>You're mutually oblivious.</p>
+<p><a href="http://blog.notmyidea.org/posts/2010/octobre/15/unbelievable/">a root-relative link to unbelievable</a>
+<a href="http://blog.notmyidea.org/posts/2010/octobre/15/unbelievable/">a file-relative link to unbelievable</a></p> Article 1 2011-02-17T00:00:00+01:00 2011-02-17T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2011-02-17:/posts/2011/février/17/article-1/ <p>Article 1</p>
+ Article 2 2011-02-17T00:00:00+01:00 2011-02-17T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2011-02-17:/posts/2011/février/17/article-2/ <p>Article 2</p>
+ Article 3 2011-02-17T00:00:00+01:00 2011-02-17T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2011-02-17:/posts/2011/février/17/article-3/ <p>Article 3</p>
+ This is a super article ! 2010-12-02T10:14:00+01:00 2013-11-17T23:29:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2010-12-02:/posts/2010/décembre/02/this-is-a-super-article/ <p class="first last">Multi-line metadata should be supported
+as well as <strong>inline markup</strong>.</p>
+ <p>Some content here !</p>
+<div class="section" id="this-is-a-simple-title">
+<h2>This is a simple title</h2>
+<p>And here comes the cool <a class="reference external" href="http://books.couchdb.org/relax/design-documents/views">stuff</a>.</p>
+<img alt="alternate text" src="http://blog.notmyidea.org/pictures/Sushi.jpg" style="width: 600px; height: 450px;" />
+<img alt="alternate text" src="http://blog.notmyidea.org/pictures/Sushi_Macro.jpg" style="width: 600px; height: 450px;" />
+<pre class="literal-block">
+>>> from ipdb import set_trace
+>>> set_trace()
+</pre>
+<p>→ And now try with some utf8 hell: ééé</p>
+</div>
+ Oh yeah ! 2010-10-20T10:14:00+02:00 2010-10-20T10:14:00+02:00 Alexis Métaireau tag:blog.notmyidea.org,2010-10-20:/posts/2010/octobre/20/oh-yeah/ <div class="section" id="why-not">
+<h2>Why not ?</h2>
+<p>After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !</p>
+<img alt="alternate text" src="http://blog.notmyidea.org/pictures/Sushi.jpg" style="width: 600px; height: 450px;" />
+</div>
+ Trop bien ! 2010-10-20T10:14:00+02:00 2010-10-20T10:14:00+02:00 Alexis Métaireau tag:blog.notmyidea.org,2010-10-20:/oh-yeah-fr.html <p>Et voila du contenu en français</p>
+ Unbelievable ! 2010-10-15T20:30:00+02:00 2010-10-15T20:30:00+02:00 Alexis Métaireau tag:blog.notmyidea.org,2010-10-15:/posts/2010/octobre/15/unbelievable/ <p>Or completely awesome. Depends the needs.</p>
+<p><a class="reference external" href="http://blog.notmyidea.org/posts/2011/avril/20/a-markdown-powered-article/">a root-relative link to markdown-article</a>
+<a class="reference external" href="http://blog.notmyidea.org/posts/2011/avril/20/a-markdown-powered-article/">a file-relative link to markdown-article</a></p>
+<div class="section" id="testing-sourcecode-directive">
+<h2>Testing sourcecode directive</h2>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table></div>
+<div class="section" id="testing-another-case">
+<h2>Testing another case</h2>
+<p>This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will …</p></div> <p>Or completely awesome. Depends the needs.</p>
+<p><a class="reference external" href="http://blog.notmyidea.org/posts/2011/avril/20/a-markdown-powered-article/">a root-relative link to markdown-article</a>
+<a class="reference external" href="http://blog.notmyidea.org/posts/2011/avril/20/a-markdown-powered-article/">a file-relative link to markdown-article</a></p>
+<div class="section" id="testing-sourcecode-directive">
+<h2>Testing sourcecode directive</h2>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table></div>
+<div class="section" id="testing-another-case">
+<h2>Testing another case</h2>
+<p>This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will have nothing in default.</p>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table><p>Lovely.</p>
+</div>
+<div class="section" id="testing-more-sourcecode-directives">
+<h2>Testing more sourcecode directives</h2>
+<div class="highlight"><pre><span></span><span id="foo-8"><a name="foo-8"></a><span class="lineno special"> 8 </span><span class="testingk">def</span> <span class="testingnf">run</span><span class="testingp">(</span><span class="testingbp">self</span><span class="testingp">):</span><br></span><span id="foo-9"><a name="foo-9"></a><span class="lineno"> </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">assert_has_content</span><span class="testingp">()</span><br></span><span id="foo-10"><a name="foo-10"></a><span class="lineno special">10 </span> <span class="testingk">try</span><span class="testingp">:</span><br></span><span id="foo-11"><a name="foo-11"></a><span class="lineno"> </span> <span class="testingn">lexer</span> <span class="testingo">=</span> <span class="testingn">get_lexer_by_name</span><span class="testingp">(</span><span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">arguments</span><span class="testingp">[</span><span class="testingmi">0</span><span class="testingp">])</span><br></span><span id="foo-12"><a name="foo-12"></a><span class="lineno special">12 </span> <span class="testingk">except</span> <span class="testingne">ValueError</span><span class="testingp">:</span><br></span><span id="foo-13"><a name="foo-13"></a><span class="lineno"> </span> <span class="testingc1"># no lexer found - use the text one instead of an exception</span><br></span><span id="foo-14"><a name="foo-14"></a><span class="lineno special">14 </span> <span class="testingn">lexer</span> <span class="testingo">=</span> <span class="testingn">TextLexer</span><span class="testingp">()</span><br></span><span id="foo-15"><a name="foo-15"></a><span class="lineno"> </span><br></span><span id="foo-16"><a name="foo-16"></a><span class="lineno special">16 </span> <span class="testingk">if</span> <span class="testingp">(</span><span class="testings1">'linenos'</span> <span class="testingow">in</span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span> <span class="testingow">and</span><br></span><span id="foo-17"><a name="foo-17"></a><span class="lineno"> </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">[</span><span class="testings1">'linenos'</span><span class="testingp">]</span> <span class="testingow">not</span> <span class="testingow">in</span> <span class="testingp">(</span><span class="testings1">'table'</span><span class="testingp">,</span> <span class="testings1">'inline'</span><span class="testingp">)):</span><br></span><span id="foo-18"><a name="foo-18"></a><span class="lineno special">18 </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">[</span><span class="testings1">'linenos'</span><span class="testingp">]</span> <span class="testingo">=</span> <span class="testings1">'table'</span><br></span><span id="foo-19"><a name="foo-19"></a><span class="lineno"> </span><br></span><span id="foo-20"><a name="foo-20"></a><span class="lineno special">20 </span> <span class="testingk">for</span> <span class="testingn">flag</span> <span class="testingow">in</span> <span class="testingp">(</span><span class="testings1">'nowrap'</span><span class="testingp">,</span> <span class="testings1">'nobackground'</span><span class="testingp">,</span> <span class="testings1">'anchorlinenos'</span><span class="testingp">):</span><br></span><span id="foo-21"><a name="foo-21"></a><span class="lineno"> </span> <span class="testingk">if</span> <span class="testingn">flag</span> <span class="testingow">in</span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">:</span><br></span><span id="foo-22"><a name="foo-22"></a><span class="lineno special">22 </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">[</span><span class="testingn">flag</span><span class="testingp">]</span> <span class="testingo">=</span> <span class="testingkc">True</span><br></span><span id="foo-23"><a name="foo-23"></a><span class="lineno"> </span><br></span><span id="foo-24"><a name="foo-24"></a><span class="lineno special">24 </span> <span class="testingc1"># noclasses should already default to False, but just in case...</span><br></span><span id="foo-25"><a name="foo-25"></a><span class="lineno"> </span> <span class="testingn">formatter</span> <span class="testingo">=</span> <span class="testingn">HtmlFormatter</span><span class="testingp">(</span><span class="testingn">noclasses</span><span class="testingo">=</span><span class="testingkc">False</span><span class="testingp">,</span> <span class="testingo">**</span><span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">)</span><br></span><span id="foo-26"><a name="foo-26"></a><span class="lineno special">26 </span> <span class="testingn">parsed</span> <span class="testingo">=</span> <span class="testingn">highlight</span><span class="testingp">(</span><span class="testings1">'</span><span class="testingse">\n</span><span class="testings1">'</span><span class="testingo">.</span><span class="testingn">join</span><span class="testingp">(</span><span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">content</span><span class="testingp">),</span> <span class="testingn">lexer</span><span class="testingp">,</span> <span class="testingn">formatter</span><span class="testingp">)</span><br></span><span id="foo-27"><a name="foo-27"></a><span class="lineno"> </span> <span class="testingk">return</span> <span class="testingp">[</span><span class="testingn">nodes</span><span class="testingo">.</span><span class="testingn">raw</span><span class="testingp">(</span><span class="testings1">''</span><span class="testingp">,</span> <span class="testingn">parsed</span><span class="testingp">,</span> <span class="testingnb">format</span><span class="testingo">=</span><span class="testings1">'html'</span><span class="testingp">)]</span><br></span></pre></div>
+<p>Lovely.</p>
+</div>
+<div class="section" id="testing-even-more-sourcecode-directives">
+<h2>Testing even more sourcecode directives</h2>
+<span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+<p>Lovely.</p>
+</div>
+<div class="section" id="testing-overriding-config-defaults">
+<h2>Testing overriding config defaults</h2>
+<p>Even if the default is line numbers, we can override it here</p>
+<div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+<p>Lovely.</p>
+</div>
+ The baz tag 2010-03-14T00:00:00+01:00 2010-03-14T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2010-03-14:/tag/baz.html <p>This article overrides the listening of the articles under the <em>baz</em> tag.</p>
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/feeds/all.rss.xml b/pelican/tests/output/custom_locale/feeds/all.rss.xml
new file mode 100644
index 00000000..8e7dd649
--- /dev/null
+++ b/pelican/tests/output/custom_locale/feeds/all.rss.xml
@@ -0,0 +1,31 @@
+
+Alexis' log http://blog.notmyidea.org/Sun, 17 Nov 2013 23:29:00 +0100 FILENAME_METADATA example http://blog.notmyidea.org/posts/2012/novembre/30/filename_metadata-example/<p>Some cool stuff!</p>
+ Alexis Métaireau Fri, 30 Nov 2012 00:00:00 +0100 tag:blog.notmyidea.org,2012-11-30:/posts/2012/novembre/30/filename_metadata-example/ misc Second article http://blog.notmyidea.org/posts/2012/f%C3%A9vrier/29/second-article/<p>This is some article, in english</p>
+ Alexis Métaireau Wed, 29 Feb 2012 00:00:00 +0100 tag:blog.notmyidea.org,2012-02-29:/posts/2012/février/29/second-article/ misc foo bar baz Deuxième article http://blog.notmyidea.org/second-article-fr.html<p>Ceci est un article, en français.</p>
+ Alexis Métaireau Wed, 29 Feb 2012 00:00:00 +0100 tag:blog.notmyidea.org,2012-02-29:/second-article-fr.html misc foo bar baz A markdown powered article http://blog.notmyidea.org/posts/2011/avril/20/a-markdown-powered-article/<p>You're mutually oblivious.</p>
+<p><a href="http://blog.notmyidea.org/posts/2010/octobre/15/unbelievable/">a root-relative link to unbelievable</a>
+<a href="http://blog.notmyidea.org/posts/2010/octobre/15/unbelievable/">a file-relative link to unbelievable</a></p> Alexis Métaireau Wed, 20 Apr 2011 00:00:00 +0200 tag:blog.notmyidea.org,2011-04-20:/posts/2011/avril/20/a-markdown-powered-article/ cat1 Article 1 http://blog.notmyidea.org/posts/2011/f%C3%A9vrier/17/article-1/<p>Article 1</p>
+ Alexis Métaireau Thu, 17 Feb 2011 00:00:00 +0100 tag:blog.notmyidea.org,2011-02-17:/posts/2011/février/17/article-1/ cat1 Article 2 http://blog.notmyidea.org/posts/2011/f%C3%A9vrier/17/article-2/<p>Article 2</p>
+ Alexis Métaireau Thu, 17 Feb 2011 00:00:00 +0100 tag:blog.notmyidea.org,2011-02-17:/posts/2011/février/17/article-2/ cat1 Article 3 http://blog.notmyidea.org/posts/2011/f%C3%A9vrier/17/article-3/<p>Article 3</p>
+ Alexis Métaireau Thu, 17 Feb 2011 00:00:00 +0100 tag:blog.notmyidea.org,2011-02-17:/posts/2011/février/17/article-3/ cat1 This is a super article ! http://blog.notmyidea.org/posts/2010/d%C3%A9cembre/02/this-is-a-super-article/<p class="first last">Multi-line metadata should be supported
+as well as <strong>inline markup</strong>.</p>
+ Alexis Métaireau Thu, 02 Dec 2010 10:14:00 +0100 tag:blog.notmyidea.org,2010-12-02:/posts/2010/décembre/02/this-is-a-super-article/ yeah foo bar foobar Oh yeah ! http://blog.notmyidea.org/posts/2010/octobre/20/oh-yeah/<div class="section" id="why-not">
+<h2>Why not ?</h2>
+<p>After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !</p>
+<img alt="alternate text" src="http://blog.notmyidea.org/pictures/Sushi.jpg" style="width: 600px; height: 450px;" />
+</div>
+ Alexis Métaireau Wed, 20 Oct 2010 10:14:00 +0200 tag:blog.notmyidea.org,2010-10-20:/posts/2010/octobre/20/oh-yeah/ bar oh bar yeah Trop bien ! http://blog.notmyidea.org/oh-yeah-fr.html<p>Et voila du contenu en français</p>
+ Alexis Métaireau Wed, 20 Oct 2010 10:14:00 +0200 tag:blog.notmyidea.org,2010-10-20:/oh-yeah-fr.html misc Unbelievable ! http://blog.notmyidea.org/posts/2010/octobre/15/unbelievable/<p>Or completely awesome. Depends the needs.</p>
+<p><a class="reference external" href="http://blog.notmyidea.org/posts/2011/avril/20/a-markdown-powered-article/">a root-relative link to markdown-article</a>
+<a class="reference external" href="http://blog.notmyidea.org/posts/2011/avril/20/a-markdown-powered-article/">a file-relative link to markdown-article</a></p>
+<div class="section" id="testing-sourcecode-directive">
+<h2>Testing sourcecode directive</h2>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table></div>
+<div class="section" id="testing-another-case">
+<h2>Testing another case</h2>
+<p>This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will …</p></div> Alexis Métaireau Fri, 15 Oct 2010 20:30:00 +0200 tag:blog.notmyidea.org,2010-10-15:/posts/2010/octobre/15/unbelievable/ misc The baz tag http://blog.notmyidea.org/tag/baz.html<p>This article overrides the listening of the articles under the <em>baz</em> tag.</p>
+ Alexis Métaireau Sun, 14 Mar 2010 00:00:00 +0100 tag:blog.notmyidea.org,2010-03-14:/tag/baz.html misc
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/feeds/bar.atom.xml b/pelican/tests/output/custom_locale/feeds/bar.atom.xml
new file mode 100644
index 00000000..d4467ea7
--- /dev/null
+++ b/pelican/tests/output/custom_locale/feeds/bar.atom.xml
@@ -0,0 +1,8 @@
+
+Alexis' log - bar http://blog.notmyidea.org/ 2010-10-20T10:14:00+02:00 Oh yeah ! 2010-10-20T10:14:00+02:00 2010-10-20T10:14:00+02:00 Alexis Métaireau tag:blog.notmyidea.org,2010-10-20:/posts/2010/octobre/20/oh-yeah/ <div class="section" id="why-not">
+<h2>Why not ?</h2>
+<p>After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !</p>
+<img alt="alternate text" src="http://blog.notmyidea.org/pictures/Sushi.jpg" style="width: 600px; height: 450px;" />
+</div>
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/feeds/bar.rss.xml b/pelican/tests/output/custom_locale/feeds/bar.rss.xml
new file mode 100644
index 00000000..d17d7703
--- /dev/null
+++ b/pelican/tests/output/custom_locale/feeds/bar.rss.xml
@@ -0,0 +1,8 @@
+
+Alexis' log - bar http://blog.notmyidea.org/Wed, 20 Oct 2010 10:14:00 +0200 Oh yeah ! http://blog.notmyidea.org/posts/2010/octobre/20/oh-yeah/<div class="section" id="why-not">
+<h2>Why not ?</h2>
+<p>After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !</p>
+<img alt="alternate text" src="http://blog.notmyidea.org/pictures/Sushi.jpg" style="width: 600px; height: 450px;" />
+</div>
+ Alexis Métaireau Wed, 20 Oct 2010 10:14:00 +0200 tag:blog.notmyidea.org,2010-10-20:/posts/2010/octobre/20/oh-yeah/ bar oh bar yeah
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/feeds/cat1.atom.xml b/pelican/tests/output/custom_locale/feeds/cat1.atom.xml
new file mode 100644
index 00000000..87a822e5
--- /dev/null
+++ b/pelican/tests/output/custom_locale/feeds/cat1.atom.xml
@@ -0,0 +1,7 @@
+
+Alexis' log - cat1 http://blog.notmyidea.org/ 2011-04-20T00:00:00+02:00 A markdown powered article 2011-04-20T00:00:00+02:00 2011-04-20T00:00:00+02:00 Alexis Métaireau tag:blog.notmyidea.org,2011-04-20:/posts/2011/avril/20/a-markdown-powered-article/ <p>You're mutually oblivious.</p>
+<p><a href="http://blog.notmyidea.org/posts/2010/octobre/15/unbelievable/">a root-relative link to unbelievable</a>
+<a href="http://blog.notmyidea.org/posts/2010/octobre/15/unbelievable/">a file-relative link to unbelievable</a></p> Article 1 2011-02-17T00:00:00+01:00 2011-02-17T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2011-02-17:/posts/2011/février/17/article-1/ <p>Article 1</p>
+ Article 2 2011-02-17T00:00:00+01:00 2011-02-17T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2011-02-17:/posts/2011/février/17/article-2/ <p>Article 2</p>
+ Article 3 2011-02-17T00:00:00+01:00 2011-02-17T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2011-02-17:/posts/2011/février/17/article-3/ <p>Article 3</p>
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/feeds/cat1.rss.xml b/pelican/tests/output/custom_locale/feeds/cat1.rss.xml
new file mode 100644
index 00000000..6b328fda
--- /dev/null
+++ b/pelican/tests/output/custom_locale/feeds/cat1.rss.xml
@@ -0,0 +1,7 @@
+
+Alexis' log - cat1 http://blog.notmyidea.org/Wed, 20 Apr 2011 00:00:00 +0200 A markdown powered article http://blog.notmyidea.org/posts/2011/avril/20/a-markdown-powered-article/<p>You're mutually oblivious.</p>
+<p><a href="http://blog.notmyidea.org/posts/2010/octobre/15/unbelievable/">a root-relative link to unbelievable</a>
+<a href="http://blog.notmyidea.org/posts/2010/octobre/15/unbelievable/">a file-relative link to unbelievable</a></p> Alexis Métaireau Wed, 20 Apr 2011 00:00:00 +0200 tag:blog.notmyidea.org,2011-04-20:/posts/2011/avril/20/a-markdown-powered-article/ cat1 Article 1 http://blog.notmyidea.org/posts/2011/f%C3%A9vrier/17/article-1/<p>Article 1</p>
+ Alexis Métaireau Thu, 17 Feb 2011 00:00:00 +0100 tag:blog.notmyidea.org,2011-02-17:/posts/2011/février/17/article-1/ cat1 Article 2 http://blog.notmyidea.org/posts/2011/f%C3%A9vrier/17/article-2/<p>Article 2</p>
+ Alexis Métaireau Thu, 17 Feb 2011 00:00:00 +0100 tag:blog.notmyidea.org,2011-02-17:/posts/2011/février/17/article-2/ cat1 Article 3 http://blog.notmyidea.org/posts/2011/f%C3%A9vrier/17/article-3/<p>Article 3</p>
+ Alexis Métaireau Thu, 17 Feb 2011 00:00:00 +0100 tag:blog.notmyidea.org,2011-02-17:/posts/2011/février/17/article-3/ cat1
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/feeds/misc.atom.xml b/pelican/tests/output/custom_locale/feeds/misc.atom.xml
new file mode 100644
index 00000000..4b514b47
--- /dev/null
+++ b/pelican/tests/output/custom_locale/feeds/misc.atom.xml
@@ -0,0 +1,49 @@
+
+Alexis' log - misc http://blog.notmyidea.org/ 2012-11-30T00:00:00+01:00 FILENAME_METADATA example 2012-11-30T00:00:00+01:00 2012-11-30T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2012-11-30:/posts/2012/novembre/30/filename_metadata-example/ <p>Some cool stuff!</p>
+ Second article 2012-02-29T00:00:00+01:00 2012-02-29T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2012-02-29:/posts/2012/février/29/second-article/ <p>This is some article, in english</p>
+ Unbelievable ! 2010-10-15T20:30:00+02:00 2010-10-15T20:30:00+02:00 Alexis Métaireau tag:blog.notmyidea.org,2010-10-15:/posts/2010/octobre/15/unbelievable/ <p>Or completely awesome. Depends the needs.</p>
+<p><a class="reference external" href="http://blog.notmyidea.org/posts/2011/avril/20/a-markdown-powered-article/">a root-relative link to markdown-article</a>
+<a class="reference external" href="http://blog.notmyidea.org/posts/2011/avril/20/a-markdown-powered-article/">a file-relative link to markdown-article</a></p>
+<div class="section" id="testing-sourcecode-directive">
+<h2>Testing sourcecode directive</h2>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table></div>
+<div class="section" id="testing-another-case">
+<h2>Testing another case</h2>
+<p>This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will …</p></div> <p>Or completely awesome. Depends the needs.</p>
+<p><a class="reference external" href="http://blog.notmyidea.org/posts/2011/avril/20/a-markdown-powered-article/">a root-relative link to markdown-article</a>
+<a class="reference external" href="http://blog.notmyidea.org/posts/2011/avril/20/a-markdown-powered-article/">a file-relative link to markdown-article</a></p>
+<div class="section" id="testing-sourcecode-directive">
+<h2>Testing sourcecode directive</h2>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table></div>
+<div class="section" id="testing-another-case">
+<h2>Testing another case</h2>
+<p>This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will have nothing in default.</p>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table><p>Lovely.</p>
+</div>
+<div class="section" id="testing-more-sourcecode-directives">
+<h2>Testing more sourcecode directives</h2>
+<div class="highlight"><pre><span></span><span id="foo-8"><a name="foo-8"></a><span class="lineno special"> 8 </span><span class="testingk">def</span> <span class="testingnf">run</span><span class="testingp">(</span><span class="testingbp">self</span><span class="testingp">):</span><br></span><span id="foo-9"><a name="foo-9"></a><span class="lineno"> </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">assert_has_content</span><span class="testingp">()</span><br></span><span id="foo-10"><a name="foo-10"></a><span class="lineno special">10 </span> <span class="testingk">try</span><span class="testingp">:</span><br></span><span id="foo-11"><a name="foo-11"></a><span class="lineno"> </span> <span class="testingn">lexer</span> <span class="testingo">=</span> <span class="testingn">get_lexer_by_name</span><span class="testingp">(</span><span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">arguments</span><span class="testingp">[</span><span class="testingmi">0</span><span class="testingp">])</span><br></span><span id="foo-12"><a name="foo-12"></a><span class="lineno special">12 </span> <span class="testingk">except</span> <span class="testingne">ValueError</span><span class="testingp">:</span><br></span><span id="foo-13"><a name="foo-13"></a><span class="lineno"> </span> <span class="testingc1"># no lexer found - use the text one instead of an exception</span><br></span><span id="foo-14"><a name="foo-14"></a><span class="lineno special">14 </span> <span class="testingn">lexer</span> <span class="testingo">=</span> <span class="testingn">TextLexer</span><span class="testingp">()</span><br></span><span id="foo-15"><a name="foo-15"></a><span class="lineno"> </span><br></span><span id="foo-16"><a name="foo-16"></a><span class="lineno special">16 </span> <span class="testingk">if</span> <span class="testingp">(</span><span class="testings1">'linenos'</span> <span class="testingow">in</span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span> <span class="testingow">and</span><br></span><span id="foo-17"><a name="foo-17"></a><span class="lineno"> </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">[</span><span class="testings1">'linenos'</span><span class="testingp">]</span> <span class="testingow">not</span> <span class="testingow">in</span> <span class="testingp">(</span><span class="testings1">'table'</span><span class="testingp">,</span> <span class="testings1">'inline'</span><span class="testingp">)):</span><br></span><span id="foo-18"><a name="foo-18"></a><span class="lineno special">18 </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">[</span><span class="testings1">'linenos'</span><span class="testingp">]</span> <span class="testingo">=</span> <span class="testings1">'table'</span><br></span><span id="foo-19"><a name="foo-19"></a><span class="lineno"> </span><br></span><span id="foo-20"><a name="foo-20"></a><span class="lineno special">20 </span> <span class="testingk">for</span> <span class="testingn">flag</span> <span class="testingow">in</span> <span class="testingp">(</span><span class="testings1">'nowrap'</span><span class="testingp">,</span> <span class="testings1">'nobackground'</span><span class="testingp">,</span> <span class="testings1">'anchorlinenos'</span><span class="testingp">):</span><br></span><span id="foo-21"><a name="foo-21"></a><span class="lineno"> </span> <span class="testingk">if</span> <span class="testingn">flag</span> <span class="testingow">in</span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">:</span><br></span><span id="foo-22"><a name="foo-22"></a><span class="lineno special">22 </span> <span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">[</span><span class="testingn">flag</span><span class="testingp">]</span> <span class="testingo">=</span> <span class="testingkc">True</span><br></span><span id="foo-23"><a name="foo-23"></a><span class="lineno"> </span><br></span><span id="foo-24"><a name="foo-24"></a><span class="lineno special">24 </span> <span class="testingc1"># noclasses should already default to False, but just in case...</span><br></span><span id="foo-25"><a name="foo-25"></a><span class="lineno"> </span> <span class="testingn">formatter</span> <span class="testingo">=</span> <span class="testingn">HtmlFormatter</span><span class="testingp">(</span><span class="testingn">noclasses</span><span class="testingo">=</span><span class="testingkc">False</span><span class="testingp">,</span> <span class="testingo">**</span><span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">options</span><span class="testingp">)</span><br></span><span id="foo-26"><a name="foo-26"></a><span class="lineno special">26 </span> <span class="testingn">parsed</span> <span class="testingo">=</span> <span class="testingn">highlight</span><span class="testingp">(</span><span class="testings1">'</span><span class="testingse">\n</span><span class="testings1">'</span><span class="testingo">.</span><span class="testingn">join</span><span class="testingp">(</span><span class="testingbp">self</span><span class="testingo">.</span><span class="testingn">content</span><span class="testingp">),</span> <span class="testingn">lexer</span><span class="testingp">,</span> <span class="testingn">formatter</span><span class="testingp">)</span><br></span><span id="foo-27"><a name="foo-27"></a><span class="lineno"> </span> <span class="testingk">return</span> <span class="testingp">[</span><span class="testingn">nodes</span><span class="testingo">.</span><span class="testingn">raw</span><span class="testingp">(</span><span class="testings1">''</span><span class="testingp">,</span> <span class="testingn">parsed</span><span class="testingp">,</span> <span class="testingnb">format</span><span class="testingo">=</span><span class="testings1">'html'</span><span class="testingp">)]</span><br></span></pre></div>
+<p>Lovely.</p>
+</div>
+<div class="section" id="testing-even-more-sourcecode-directives">
+<h2>Testing even more sourcecode directives</h2>
+<span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+<p>Lovely.</p>
+</div>
+<div class="section" id="testing-overriding-config-defaults">
+<h2>Testing overriding config defaults</h2>
+<p>Even if the default is line numbers, we can override it here</p>
+<div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+<p>Lovely.</p>
+</div>
+ The baz tag 2010-03-14T00:00:00+01:00 2010-03-14T00:00:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2010-03-14:/tag/baz.html <p>This article overrides the listening of the articles under the <em>baz</em> tag.</p>
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/feeds/misc.rss.xml b/pelican/tests/output/custom_locale/feeds/misc.rss.xml
new file mode 100644
index 00000000..041c7687
--- /dev/null
+++ b/pelican/tests/output/custom_locale/feeds/misc.rss.xml
@@ -0,0 +1,16 @@
+
+Alexis' log - misc http://blog.notmyidea.org/Fri, 30 Nov 2012 00:00:00 +0100 FILENAME_METADATA example http://blog.notmyidea.org/posts/2012/novembre/30/filename_metadata-example/<p>Some cool stuff!</p>
+ Alexis Métaireau Fri, 30 Nov 2012 00:00:00 +0100 tag:blog.notmyidea.org,2012-11-30:/posts/2012/novembre/30/filename_metadata-example/ misc Second article http://blog.notmyidea.org/posts/2012/f%C3%A9vrier/29/second-article/<p>This is some article, in english</p>
+ Alexis Métaireau Wed, 29 Feb 2012 00:00:00 +0100 tag:blog.notmyidea.org,2012-02-29:/posts/2012/février/29/second-article/ misc foo bar baz Unbelievable ! http://blog.notmyidea.org/posts/2010/octobre/15/unbelievable/<p>Or completely awesome. Depends the needs.</p>
+<p><a class="reference external" href="http://blog.notmyidea.org/posts/2011/avril/20/a-markdown-powered-article/">a root-relative link to markdown-article</a>
+<a class="reference external" href="http://blog.notmyidea.org/posts/2011/avril/20/a-markdown-powered-article/">a file-relative link to markdown-article</a></p>
+<div class="section" id="testing-sourcecode-directive">
+<h2>Testing sourcecode directive</h2>
+<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="n">formatter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">options</span> <span class="ow">and</span> <span class="n">VARIANTS</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="o">.</span><span class="n">keys</span><span class="p">()[</span><span class="mi">0</span><span class="p">]]</span>
+</pre></div>
+</td></tr></table></div>
+<div class="section" id="testing-another-case">
+<h2>Testing another case</h2>
+<p>This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will …</p></div> Alexis Métaireau Fri, 15 Oct 2010 20:30:00 +0200 tag:blog.notmyidea.org,2010-10-15:/posts/2010/octobre/15/unbelievable/ misc The baz tag http://blog.notmyidea.org/tag/baz.html<p>This article overrides the listening of the articles under the <em>baz</em> tag.</p>
+ Alexis Métaireau Sun, 14 Mar 2010 00:00:00 +0100 tag:blog.notmyidea.org,2010-03-14:/tag/baz.html misc
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/feeds/yeah.atom.xml b/pelican/tests/output/custom_locale/feeds/yeah.atom.xml
new file mode 100644
index 00000000..6f2e5f82
--- /dev/null
+++ b/pelican/tests/output/custom_locale/feeds/yeah.atom.xml
@@ -0,0 +1,16 @@
+
+Alexis' log - yeah http://blog.notmyidea.org/ 2013-11-17T23:29:00+01:00 This is a super article ! 2010-12-02T10:14:00+01:00 2013-11-17T23:29:00+01:00 Alexis Métaireau tag:blog.notmyidea.org,2010-12-02:/posts/2010/décembre/02/this-is-a-super-article/ <p class="first last">Multi-line metadata should be supported
+as well as <strong>inline markup</strong>.</p>
+ <p>Some content here !</p>
+<div class="section" id="this-is-a-simple-title">
+<h2>This is a simple title</h2>
+<p>And here comes the cool <a class="reference external" href="http://books.couchdb.org/relax/design-documents/views">stuff</a>.</p>
+<img alt="alternate text" src="http://blog.notmyidea.org/pictures/Sushi.jpg" style="width: 600px; height: 450px;" />
+<img alt="alternate text" src="http://blog.notmyidea.org/pictures/Sushi_Macro.jpg" style="width: 600px; height: 450px;" />
+<pre class="literal-block">
+>>> from ipdb import set_trace
+>>> set_trace()
+</pre>
+<p>→ And now try with some utf8 hell: ééé</p>
+</div>
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/feeds/yeah.rss.xml b/pelican/tests/output/custom_locale/feeds/yeah.rss.xml
new file mode 100644
index 00000000..b7fb81f8
--- /dev/null
+++ b/pelican/tests/output/custom_locale/feeds/yeah.rss.xml
@@ -0,0 +1,4 @@
+
+Alexis' log - yeah http://blog.notmyidea.org/Sun, 17 Nov 2013 23:29:00 +0100 This is a super article ! http://blog.notmyidea.org/posts/2010/d%C3%A9cembre/02/this-is-a-super-article/<p class="first last">Multi-line metadata should be supported
+as well as <strong>inline markup</strong>.</p>
+ Alexis Métaireau Thu, 02 Dec 2010 10:14:00 +0100 tag:blog.notmyidea.org,2010-12-02:/posts/2010/décembre/02/this-is-a-super-article/ yeah foo bar foobar
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/index.html b/pelican/tests/output/custom_locale/index.html
new file mode 100644
index 00000000..29e12b1b
--- /dev/null
+++ b/pelican/tests/output/custom_locale/index.html
@@ -0,0 +1,171 @@
+
+
+
+
+
+ Alexis' log
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: 30 novembre 2012
+
+
+
+ By Alexis Métaireau
+
+In misc .
+
+ Some cool stuff!
+There are comments .
+
+
+ Other articles
+
+
+
+
+
+
+
+
+
+ Published: 29 février 2012
+
+
+
+ By Alexis Métaireau
+
+In misc .
+tags: foo bar baz
Translations:
+ fr
+
+ This is some article, in english
+
+
read more
+
There are comments .
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: 17 février 2011
+
+
+
+ By Alexis Métaireau
+
+In cat1 .
+
+ Article 1
+
+
read more
+
There are comments .
+
+
+
+ Page 1 / 3
+ »
+ ⇉
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
diff --git a/pelican/tests/output/custom_locale/index2.html b/pelican/tests/output/custom_locale/index2.html
new file mode 100644
index 00000000..c0727385
--- /dev/null
+++ b/pelican/tests/output/custom_locale/index2.html
@@ -0,0 +1,186 @@
+
+
+
+
+
+ Alexis' log
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: 17 février 2011
+
+
+
+ By Alexis Métaireau
+
+In cat1 .
+
+ Article 2
+
+
read more
+
There are comments .
+
+
+
+
+
+
+
+
+ Published: 17 février 2011
+
+
+
+ By Alexis Métaireau
+
+In cat1 .
+
+ Article 3
+
+
read more
+
There are comments .
+
+
+
+
+
+
+
+
+ Published: 02 décembre 2010
+
+
+
+ Updated: 17 novembre 2013
+
+
+
+ By Alexis Métaireau
+
+In yeah .
+tags: foo bar foobar
+ Multi-line metadata should be supported
+as well as inline markup .
+
+
read more
+
There are comments .
+
+
+
+
+
+
+
+
+ Published: 20 octobre 2010
+
+
+
+ By Alexis Métaireau
+
+In bar .
+tags: oh bar yeah
Translations:
+ fr
+
+
+
Why not ?
+
After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !
+
+
+
+
read more
+
There are comments .
+
+
+
+ ⇇
+ «
+ Page 2 / 3
+ »
+ ⇉
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
diff --git a/pelican/tests/output/custom_locale/index3.html b/pelican/tests/output/custom_locale/index3.html
new file mode 100644
index 00000000..0d2722f2
--- /dev/null
+++ b/pelican/tests/output/custom_locale/index3.html
@@ -0,0 +1,136 @@
+
+
+
+
+
+ Alexis' log
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: 15 octobre 2010
+
+
+
+ By Alexis Métaireau
+
+In misc .
+
+ Or completely awesome. Depends the needs.
+
a root-relative link to markdown-article
+a file-relative link to markdown-article
+
+
Testing sourcecode directive
+
formatter = self . options and VARIANTS [ self . options . keys ()[ 0 ]]
+
+
+
+
Testing another case
+
This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will …
+
read more
+
There are comments .
+
+
+
+
+
+
+
+
+ Published: 14 mars 2010
+
+
+
+ By Alexis Métaireau
+
+In misc .
+
+ This article overrides the listening of the articles under the baz tag.
+
+
read more
+
There are comments .
+
+
+
+ ⇇
+ «
+ Page 3 / 3
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
diff --git a/pelican/tests/output/custom_locale/jinja2_template.html b/pelican/tests/output/custom_locale/jinja2_template.html
new file mode 100644
index 00000000..83b6e8e0
--- /dev/null
+++ b/pelican/tests/output/custom_locale/jinja2_template.html
@@ -0,0 +1,74 @@
+
+
+
+
+
+ Alexis' log
+
+
+
+
+
+
+
+
+
+
+
+Some text
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/oh-yeah-fr.html b/pelican/tests/output/custom_locale/oh-yeah-fr.html
new file mode 100644
index 00000000..58859a3a
--- /dev/null
+++ b/pelican/tests/output/custom_locale/oh-yeah-fr.html
@@ -0,0 +1,116 @@
+
+
+
+
+
+ Trop bien !
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: 20 octobre 2010
+
+
+
+ By Alexis Métaireau
+
+In misc .
+Translations:
+ en
+
+ Et voila du contenu en français
+
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/override/index.html b/pelican/tests/output/custom_locale/override/index.html
new file mode 100644
index 00000000..5c273ed6
--- /dev/null
+++ b/pelican/tests/output/custom_locale/override/index.html
@@ -0,0 +1,78 @@
+
+
+
+
+
+ Override url/save_as
+
+
+
+
+
+
+
+
+
+
+
+ Override url/save_as
+
+ Test page which overrides save_as and url so that this page will be generated
+at a custom location.
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/pages/this-is-a-test-hidden-page.html b/pelican/tests/output/custom_locale/pages/this-is-a-test-hidden-page.html
new file mode 100644
index 00000000..91372822
--- /dev/null
+++ b/pelican/tests/output/custom_locale/pages/this-is-a-test-hidden-page.html
@@ -0,0 +1,78 @@
+
+
+
+
+
+ This is a test hidden page
+
+
+
+
+
+
+
+
+
+
+
+ This is a test hidden page
+
+ This is great for things like error(404) pages
+Anyone can see this page but it's not linked to anywhere!
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/pages/this-is-a-test-page.html b/pelican/tests/output/custom_locale/pages/this-is-a-test-page.html
new file mode 100644
index 00000000..eb779355
--- /dev/null
+++ b/pelican/tests/output/custom_locale/pages/this-is-a-test-page.html
@@ -0,0 +1,79 @@
+
+
+
+
+
+ This is a test page
+
+
+
+
+
+
+
+
+
+
+
+ This is a test page
+
+ Just an image.
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/pictures/Fat_Cat.jpg b/pelican/tests/output/custom_locale/pictures/Fat_Cat.jpg
new file mode 100644
index 00000000..d8a96d35
Binary files /dev/null and b/pelican/tests/output/custom_locale/pictures/Fat_Cat.jpg differ
diff --git a/pelican/tests/output/custom_locale/pictures/Sushi.jpg b/pelican/tests/output/custom_locale/pictures/Sushi.jpg
new file mode 100644
index 00000000..e49e5f0a
Binary files /dev/null and b/pelican/tests/output/custom_locale/pictures/Sushi.jpg differ
diff --git a/pelican/tests/output/custom_locale/pictures/Sushi_Macro.jpg b/pelican/tests/output/custom_locale/pictures/Sushi_Macro.jpg
new file mode 100644
index 00000000..21f935a1
Binary files /dev/null and b/pelican/tests/output/custom_locale/pictures/Sushi_Macro.jpg differ
diff --git a/pelican/tests/output/custom_locale/posts/2010/décembre/02/this-is-a-super-article/index.html b/pelican/tests/output/custom_locale/posts/2010/décembre/02/this-is-a-super-article/index.html
new file mode 100644
index 00000000..dee44f28
--- /dev/null
+++ b/pelican/tests/output/custom_locale/posts/2010/décembre/02/this-is-a-super-article/index.html
@@ -0,0 +1,127 @@
+
+
+
+
+
+ This is a super article !
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: 02 décembre 2010
+
+
+
+ Updated: 17 novembre 2013
+
+
+
+ By Alexis Métaireau
+
+In yeah .
+tags: foo bar foobar
+ Some content here !
+
+
This is a simple title
+
And here comes the cool stuff .
+
+
+
+>>> from ipdb import set_trace
+>>> set_trace()
+
+
→ And now try with some utf8 hell: ééé
+
+
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/posts/2010/octobre/15/unbelievable/index.html b/pelican/tests/output/custom_locale/posts/2010/octobre/15/unbelievable/index.html
new file mode 100644
index 00000000..e42c70f6
--- /dev/null
+++ b/pelican/tests/output/custom_locale/posts/2010/octobre/15/unbelievable/index.html
@@ -0,0 +1,144 @@
+
+
+
+
+
+ Unbelievable !
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: 15 octobre 2010
+
+
+
+ By Alexis Métaireau
+
+In misc .
+
+ Or completely awesome. Depends the needs.
+
a root-relative link to markdown-article
+a file-relative link to markdown-article
+
+
Testing sourcecode directive
+
formatter = self . options and VARIANTS [ self . options . keys ()[ 0 ]]
+
+
+
+
Testing another case
+
This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will have nothing in default.
+
formatter = self . options and VARIANTS [ self . options . keys ()[ 0 ]]
+
+
Lovely.
+
+
+
Testing more sourcecode directives
+
8 def run ( self ): self . assert_has_content () 10 try : lexer = get_lexer_by_name ( self . arguments [ 0 ]) 12 except ValueError : # no lexer found - use the text one instead of an exception 14 lexer = TextLexer () 16 if ( 'linenos' in self . options and self . options [ 'linenos' ] not in ( 'table' , 'inline' )): 18 self . options [ 'linenos' ] = 'table' 20 for flag in ( 'nowrap' , 'nobackground' , 'anchorlinenos' ): if flag in self . options : 22 self . options [ flag ] = True 24 # noclasses should already default to False, but just in case... formatter = HtmlFormatter ( noclasses = False , ** self . options ) 26 parsed = highlight ( ' \n ' . join ( self . content ), lexer , formatter ) return [ nodes . raw ( '' , parsed , format = 'html' )]
+
Lovely.
+
+
+
Testing even more sourcecode directives
+
formatter = self . options and VARIANTS [ self . options . keys ()[ 0 ]]
+
Lovely.
+
+
+
Testing overriding config defaults
+
Even if the default is line numbers, we can override it here
+
formatter = self . options and VARIANTS [ self . options . keys ()[ 0 ]]
+
+
Lovely.
+
+
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/posts/2010/octobre/20/oh-yeah/index.html b/pelican/tests/output/custom_locale/posts/2010/octobre/20/oh-yeah/index.html
new file mode 100644
index 00000000..1bf74b57
--- /dev/null
+++ b/pelican/tests/output/custom_locale/posts/2010/octobre/20/oh-yeah/index.html
@@ -0,0 +1,121 @@
+
+
+
+
+
+ Oh yeah !
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: 20 octobre 2010
+
+
+
+ By Alexis Métaireau
+
+In bar .
+tags: oh bar yeah
Translations:
+ fr
+
+
+
Why not ?
+
After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !
+
+
+
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/posts/2011/avril/20/a-markdown-powered-article/index.html b/pelican/tests/output/custom_locale/posts/2011/avril/20/a-markdown-powered-article/index.html
new file mode 100644
index 00000000..0f35847a
--- /dev/null
+++ b/pelican/tests/output/custom_locale/posts/2011/avril/20/a-markdown-powered-article/index.html
@@ -0,0 +1,113 @@
+
+
+
+
+
+ A markdown powered article
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/posts/2011/février/17/article-1/index.html b/pelican/tests/output/custom_locale/posts/2011/février/17/article-1/index.html
new file mode 100644
index 00000000..05418036
--- /dev/null
+++ b/pelican/tests/output/custom_locale/posts/2011/février/17/article-1/index.html
@@ -0,0 +1,112 @@
+
+
+
+
+
+ Article 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: 17 février 2011
+
+
+
+ By Alexis Métaireau
+
+In cat1 .
+
+ Article 1
+
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/posts/2011/février/17/article-2/index.html b/pelican/tests/output/custom_locale/posts/2011/février/17/article-2/index.html
new file mode 100644
index 00000000..8cb67749
--- /dev/null
+++ b/pelican/tests/output/custom_locale/posts/2011/février/17/article-2/index.html
@@ -0,0 +1,112 @@
+
+
+
+
+
+ Article 2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: 17 février 2011
+
+
+
+ By Alexis Métaireau
+
+In cat1 .
+
+ Article 2
+
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/posts/2011/février/17/article-3/index.html b/pelican/tests/output/custom_locale/posts/2011/février/17/article-3/index.html
new file mode 100644
index 00000000..62423814
--- /dev/null
+++ b/pelican/tests/output/custom_locale/posts/2011/février/17/article-3/index.html
@@ -0,0 +1,112 @@
+
+
+
+
+
+ Article 3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: 17 février 2011
+
+
+
+ By Alexis Métaireau
+
+In cat1 .
+
+ Article 3
+
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/posts/2012/février/29/second-article/index.html b/pelican/tests/output/custom_locale/posts/2012/février/29/second-article/index.html
new file mode 100644
index 00000000..615a7f30
--- /dev/null
+++ b/pelican/tests/output/custom_locale/posts/2012/février/29/second-article/index.html
@@ -0,0 +1,116 @@
+
+
+
+
+
+ Second article
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: 29 février 2012
+
+
+
+ By Alexis Métaireau
+
+In misc .
+tags: foo bar baz
Translations:
+ fr
+
+ This is some article, in english
+
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/posts/2012/novembre/30/filename_metadata-example/index.html b/pelican/tests/output/custom_locale/posts/2012/novembre/30/filename_metadata-example/index.html
new file mode 100644
index 00000000..2213d174
--- /dev/null
+++ b/pelican/tests/output/custom_locale/posts/2012/novembre/30/filename_metadata-example/index.html
@@ -0,0 +1,112 @@
+
+
+
+
+
+ FILENAME_METADATA example
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: 30 novembre 2012
+
+
+
+ By Alexis Métaireau
+
+In misc .
+
+ Some cool stuff!
+
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/robots.txt b/pelican/tests/output/custom_locale/robots.txt
new file mode 100644
index 00000000..19a6e299
--- /dev/null
+++ b/pelican/tests/output/custom_locale/robots.txt
@@ -0,0 +1,2 @@
+User-agent: *
+Disallow: /pictures
diff --git a/pelican/tests/output/custom_locale/second-article-fr.html b/pelican/tests/output/custom_locale/second-article-fr.html
new file mode 100644
index 00000000..3c21ef75
--- /dev/null
+++ b/pelican/tests/output/custom_locale/second-article-fr.html
@@ -0,0 +1,116 @@
+
+
+
+
+
+ Deuxième article
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: 29 février 2012
+
+
+
+ By Alexis Métaireau
+
+In misc .
+tags: foo bar baz
Translations:
+ en
+
+ Ceci est un article, en français.
+
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/tag/bar.html b/pelican/tests/output/custom_locale/tag/bar.html
new file mode 100644
index 00000000..aaa5fb0d
--- /dev/null
+++ b/pelican/tests/output/custom_locale/tag/bar.html
@@ -0,0 +1,154 @@
+
+
+
+
+
+ Alexis' log - bar
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: 29 février 2012
+
+
+
+ By Alexis Métaireau
+
+In misc .
+tags: foo bar baz
Translations:
+ fr
+
+ This is some article, in english
+There are comments .
+
+
+ Other articles
+
+
+
+
+
+
+
+
+
+ Published: 02 décembre 2010
+
+
+
+ Updated: 17 novembre 2013
+
+
+
+ By Alexis Métaireau
+
+In yeah .
+tags: foo bar foobar
+ Multi-line metadata should be supported
+as well as inline markup .
+
+
read more
+
There are comments .
+
+
+
+
+
+
+
+
+ Published: 20 octobre 2010
+
+
+
+ By Alexis Métaireau
+
+In bar .
+tags: oh bar yeah
Translations:
+ fr
+
+
+
Why not ?
+
After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !
+
+
+
+
read more
+
There are comments .
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/tag/baz.html b/pelican/tests/output/custom_locale/tag/baz.html
new file mode 100644
index 00000000..9554bdd6
--- /dev/null
+++ b/pelican/tests/output/custom_locale/tag/baz.html
@@ -0,0 +1,112 @@
+
+
+
+
+
+ The baz tag
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: 14 mars 2010
+
+
+
+ By Alexis Métaireau
+
+In misc .
+
+ This article overrides the listening of the articles under the baz tag.
+
+
+
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/tag/foo.html b/pelican/tests/output/custom_locale/tag/foo.html
new file mode 100644
index 00000000..15523248
--- /dev/null
+++ b/pelican/tests/output/custom_locale/tag/foo.html
@@ -0,0 +1,124 @@
+
+
+
+
+
+ Alexis' log - foo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: 29 février 2012
+
+
+
+ By Alexis Métaireau
+
+In misc .
+tags: foo bar baz
Translations:
+ fr
+
+ This is some article, in english
+There are comments .
+
+
+ Other articles
+
+
+
+
+
+
+
+
+
+ Published: 02 décembre 2010
+
+
+
+ Updated: 17 novembre 2013
+
+
+
+ By Alexis Métaireau
+
+In yeah .
+tags: foo bar foobar
+ Multi-line metadata should be supported
+as well as inline markup .
+
+
read more
+
There are comments .
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/tag/foobar.html b/pelican/tests/output/custom_locale/tag/foobar.html
new file mode 100644
index 00000000..1393a76e
--- /dev/null
+++ b/pelican/tests/output/custom_locale/tag/foobar.html
@@ -0,0 +1,103 @@
+
+
+
+
+
+ Alexis' log - foobar
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: 02 décembre 2010
+
+
+
+ Updated: 17 novembre 2013
+
+
+
+ By Alexis Métaireau
+
+In yeah .
+tags: foo bar foobar
+ Some content here !
+
+
This is a simple title
+
And here comes the cool stuff .
+
+
+
+>>> from ipdb import set_trace
+>>> set_trace()
+
+
→ And now try with some utf8 hell: ééé
+
+There are comments .
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/tag/oh.html b/pelican/tests/output/custom_locale/tag/oh.html
new file mode 100644
index 00000000..b6fe445f
--- /dev/null
+++ b/pelican/tests/output/custom_locale/tag/oh.html
@@ -0,0 +1,77 @@
+
+
+
+
+
+ Oh Oh Oh
+
+
+
+
+
+
+
+
+
+
+
+ Oh Oh Oh
+
+ This page overrides the listening of the articles under the oh tag.
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/tag/yeah.html b/pelican/tests/output/custom_locale/tag/yeah.html
new file mode 100644
index 00000000..3953f1b7
--- /dev/null
+++ b/pelican/tests/output/custom_locale/tag/yeah.html
@@ -0,0 +1,95 @@
+
+
+
+
+
+ Alexis' log - yeah
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Published: 20 octobre 2010
+
+
+
+ By Alexis Métaireau
+
+In bar .
+tags: oh bar yeah
Translations:
+ fr
+
+
+
Why not ?
+
After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !
+
+
+There are comments .
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/tags.html b/pelican/tests/output/custom_locale/tags.html
new file mode 100644
index 00000000..cf932f97
--- /dev/null
+++ b/pelican/tests/output/custom_locale/tags.html
@@ -0,0 +1,84 @@
+
+
+
+
+
+ Alexis' log - Tags
+
+
+
+
+
+
+
+
+
+
+
+
+ Tags for Alexis' log
+
+
+
+
+
+
+
+ Proudly powered by Pelican , which takes great advantage of Python .
+
+
+ The theme is by Smashing Magazine , thanks!
+
+
+
+
+
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/theme/css/fonts.css b/pelican/tests/output/custom_locale/theme/css/fonts.css
new file mode 100644
index 00000000..56015076
--- /dev/null
+++ b/pelican/tests/output/custom_locale/theme/css/fonts.css
@@ -0,0 +1,12 @@
+@font-face {
+ font-family: 'Yanone Kaffeesatz';
+ font-style: normal;
+ font-weight: 400;
+ src:
+ local('Yanone Kaffeesatz Regular'),
+ local('YanoneKaffeesatz-Regular'),
+ /* from https://fonts.gstatic.com/s/yanonekaffeesatz/v8/YDAoLskQQ5MOAgvHUQCcLRTHiN2BPBirwIkMLKUspj4.woff */
+ url('../fonts/Yanone_Kaffeesatz_400.woff') format('woff'),
+ /* from https://fonts.gstatic.com/s/yanonekaffeesatz/v8/YDAoLskQQ5MOAgvHUQCcLfGwxTS8d1Q9KiDNCMKLFUM.woff2 */
+ url('../fonts/Yanone_Kaffeesatz_400.woff2') format('woff2');
+}
diff --git a/pelican/tests/output/custom_locale/theme/css/main.css b/pelican/tests/output/custom_locale/theme/css/main.css
new file mode 100644
index 00000000..63f5adc0
--- /dev/null
+++ b/pelican/tests/output/custom_locale/theme/css/main.css
@@ -0,0 +1,466 @@
+/*
+ Name: Smashing HTML5
+ Date: July 2009
+ Description: Sample layout for HTML5 and CSS3 goodness.
+ Version: 1.0
+ License: MIT
+ Licensed by: Smashing Media GmbH
+ Original author: Enrique Ramírez
+*/
+
+/* Imports */
+@import url("reset.css");
+@import url("pygment.css");
+@import url("typogrify.css");
+@import url("fonts.css");
+
+/***** Global *****/
+/* Body */
+body {
+ background: #F5F4EF;
+ color: #000305;
+ font-size: 87.5%; /* Base font size: 14px */
+ font-family: 'Trebuchet MS', Trebuchet, 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif;
+ line-height: 1.429;
+ margin: 0;
+ padding: 0;
+ text-align: left;
+}
+
+/* Headings */
+h1 {font-size: 2em }
+h2 {font-size: 1.571em} /* 22px */
+h3 {font-size: 1.429em} /* 20px */
+h4 {font-size: 1.286em} /* 18px */
+h5 {font-size: 1.143em} /* 16px */
+h6 {font-size: 1em} /* 14px */
+
+h1, h2, h3, h4, h5, h6 {
+ font-weight: 400;
+ line-height: 1.1;
+ margin-bottom: .8em;
+ font-family: 'Yanone Kaffeesatz', arial, serif;
+}
+
+h3, h4, h5, h6 { margin-top: .8em; }
+
+hr { border: 2px solid #EEEEEE; }
+
+/* Anchors */
+a {outline: 0;}
+a img {border: 0px; text-decoration: none;}
+a:link, a:visited {
+ color: #C74350;
+ padding: 0 1px;
+ text-decoration: underline;
+}
+a:hover, a:active {
+ background-color: #C74350;
+ color: #fff;
+ text-decoration: none;
+ text-shadow: 1px 1px 1px #333;
+}
+
+h1 a:hover {
+ background-color: inherit
+}
+
+/* Paragraphs */
+div.line-block,
+p { margin-top: 1em;
+ margin-bottom: 1em;}
+
+strong, b {font-weight: bold;}
+em, i {font-style: italic;}
+
+/* Lists */
+ul {
+ list-style: outside disc;
+ margin: 0em 0 0 1.5em;
+}
+
+ol {
+ list-style: outside decimal;
+ margin: 0em 0 0 1.5em;
+}
+
+li { margin-top: 0.5em;
+ margin-bottom: 1em; }
+
+.post-info {
+ float:right;
+ margin:10px;
+ padding:5px;
+}
+
+.post-info p{
+ margin-top: 1px;
+ margin-bottom: 1px;
+}
+
+.readmore { float: right }
+
+dl {margin: 0 0 1.5em 0;}
+dt {font-weight: bold;}
+dd {margin-left: 1.5em;}
+
+pre{background-color: rgb(238, 238, 238); padding: 10px; margin: 10px; overflow: auto;}
+
+/* Quotes */
+blockquote {
+ margin: 20px;
+ font-style: italic;
+}
+cite {}
+
+q {}
+
+div.note {
+ float: right;
+ margin: 5px;
+ font-size: 85%;
+ max-width: 300px;
+}
+
+/* Tables */
+table {margin: .5em auto 1.5em auto; width: 98%;}
+
+ /* Thead */
+ thead th {padding: .5em .4em; text-align: left;}
+ thead td {}
+
+ /* Tbody */
+ tbody td {padding: .5em .4em;}
+ tbody th {}
+
+ tbody .alt td {}
+ tbody .alt th {}
+
+ /* Tfoot */
+ tfoot th {}
+ tfoot td {}
+
+/* HTML5 tags */
+header, section, footer,
+aside, nav, article, figure {
+ display: block;
+}
+
+/***** Layout *****/
+.body {clear: both; margin: 0 auto; width: 800px;}
+img.right, figure.right, div.figure.align-right {
+ float: right;
+ margin: 0 0 2em 2em;
+}
+img.left, figure.left, div.figure.align-left {
+ float: left;
+ margin: 0 2em 2em 0;
+}
+
+/* .rst support */
+div.figure img, figure img { /* to fill figure exactly */
+ width: 100%;
+}
+div.figure p.caption, figure p.caption { /* margin provided by figure */
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+/*
+ Header
+*****************/
+#banner {
+ margin: 0 auto;
+ padding: 2.5em 0 0 0;
+}
+
+ /* Banner */
+ #banner h1 {font-size: 3.571em; line-height: 0;}
+ #banner h1 a:link, #banner h1 a:visited {
+ color: #000305;
+ display: block;
+ font-weight: bold;
+ margin: 0 0 .6em .2em;
+ text-decoration: none;
+ }
+ #banner h1 a:hover, #banner h1 a:active {
+ background: none;
+ color: #C74350;
+ text-shadow: none;
+ }
+
+ #banner h1 strong {font-size: 0.36em; font-weight: normal;}
+
+ /* Main Nav */
+ #banner nav {
+ background: #000305;
+ font-size: 1.143em;
+ height: 40px;
+ line-height: 30px;
+ margin: 0 auto 2em auto;
+ padding: 0;
+ text-align: center;
+ width: 800px;
+
+ border-radius: 5px;
+ -moz-border-radius: 5px;
+ -webkit-border-radius: 5px;
+ }
+
+ #banner nav ul {list-style: none; margin: 0 auto; width: 800px;}
+ #banner nav li {float: left; display: inline; margin: 0;}
+
+ #banner nav a:link, #banner nav a:visited {
+ color: #fff;
+ display: inline-block;
+ height: 30px;
+ padding: 5px 1.5em;
+ text-decoration: none;
+ }
+ #banner nav a:hover, #banner nav a:active,
+ #banner nav .active a:link, #banner nav .active a:visited {
+ background: #C74451;
+ color: #fff;
+ text-shadow: none !important;
+ }
+
+ #banner nav li:first-child a {
+ border-top-left-radius: 5px;
+ -moz-border-radius-topleft: 5px;
+ -webkit-border-top-left-radius: 5px;
+
+ border-bottom-left-radius: 5px;
+ -moz-border-radius-bottomleft: 5px;
+ -webkit-border-bottom-left-radius: 5px;
+ }
+
+/*
+ Featured
+*****************/
+#featured {
+ background: #fff;
+ margin-bottom: 2em;
+ overflow: hidden;
+ padding: 20px;
+ width: 760px;
+
+ border-radius: 10px;
+ -moz-border-radius: 10px;
+ -webkit-border-radius: 10px;
+}
+
+#featured figure {
+ border: 2px solid #eee;
+ float: right;
+ margin: 0.786em 2em 0 5em;
+ width: 248px;
+}
+#featured figure img {display: block; float: right;}
+
+#featured h2 {color: #C74451; font-size: 1.714em; margin-bottom: 0.333em;}
+#featured h3 {font-size: 1.429em; margin-bottom: .5em;}
+
+#featured h3 a:link, #featured h3 a:visited {color: #000305; text-decoration: none;}
+#featured h3 a:hover, #featured h3 a:active {color: #fff;}
+
+/*
+ Body
+*****************/
+#content {
+ background: #fff;
+ margin-bottom: 2em;
+ overflow: hidden;
+ padding: 20px 20px;
+ width: 760px;
+
+ border-radius: 10px;
+ -moz-border-radius: 10px;
+ -webkit-border-radius: 10px;
+}
+
+/*
+ Extras
+*****************/
+#extras {margin: 0 auto 3em auto; overflow: hidden;}
+
+#extras ul {list-style: none; margin: 0;}
+#extras li {border-bottom: 1px solid #fff;}
+#extras h2 {
+ color: #C74350;
+ font-size: 1.429em;
+ margin-bottom: .25em;
+ padding: 0 3px;
+}
+
+#extras a:link, #extras a:visited {
+ color: #444;
+ display: block;
+ border-bottom: 1px solid #F4E3E3;
+ text-decoration: none;
+ padding: .3em .25em;
+}
+
+#extras a:hover, #extras a:active {color: #fff;}
+
+ /* Blogroll */
+ #extras .blogroll {
+ float: left;
+ width: 615px;
+ }
+
+ #extras .blogroll li {float: left; margin: 0 20px 0 0; width: 185px;}
+
+ /* Social */
+ #extras .social {
+ float: right;
+ width: 175px;
+ }
+
+ #extras div[class='social'] a {
+ background-repeat: no-repeat;
+ background-position: 3px 6px;
+ padding-left: 25px;
+ }
+
+ /* Icons */
+ .social a[href*='about.me'] {background-image: url('../images/icons/aboutme.png');}
+ .social a[href*='bitbucket.org'] {background-image: url('../images/icons/bitbucket.png');}
+ .social a[href*='delicious.com'] {background-image: url('../images/icons/delicious.png');}
+ .social a[href*='facebook.com'] {background-image: url('../images/icons/facebook.png');}
+ .social a[href*='gitorious.org'] {background-image: url('../images/icons/gitorious.png');}
+ .social a[href*='github.com'],
+ .social a[href*='git.io'] {
+ background-image: url('../images/icons/github.png');
+ background-size: 16px 16px;
+ }
+ .social a[href*='gittip.com'] {background-image: url('../images/icons/gittip.png');}
+ .social a[href*='plus.google.com'] {background-image: url('../images/icons/google-plus.png');}
+ .social a[href*='groups.google.com'] {background-image: url('../images/icons/google-groups.png');}
+ .social a[href*='news.ycombinator.com'],
+ .social a[href*='hackernewsers.com'] {background-image: url('../images/icons/hackernews.png');}
+ .social a[href*='last.fm'], .social a[href*='lastfm.'] {background-image: url('../images/icons/lastfm.png');}
+ .social a[href*='linkedin.com'] {background-image: url('../images/icons/linkedin.png');}
+ .social a[href*='reddit.com'] {background-image: url('../images/icons/reddit.png');}
+ .social a[type$='atom+xml'], .social a[type$='rss+xml'] {background-image: url('../images/icons/rss.png');}
+ .social a[href*='slideshare.net'] {background-image: url('../images/icons/slideshare.png');}
+ .social a[href*='speakerdeck.com'] {background-image: url('../images/icons/speakerdeck.png');}
+ .social a[href*='stackoverflow.com'] {background-image: url('../images/icons/stackoverflow.png');}
+ .social a[href*='twitter.com'] {background-image: url('../images/icons/twitter.png');}
+ .social a[href*='vimeo.com'] {background-image: url('../images/icons/vimeo.png');}
+ .social a[href*='youtube.com'] {background-image: url('../images/icons/youtube.png');}
+
+/*
+ About
+*****************/
+#about {
+ background: #fff;
+ font-style: normal;
+ margin-bottom: 2em;
+ overflow: hidden;
+ padding: 20px;
+ text-align: left;
+ width: 760px;
+
+ border-radius: 10px;
+ -moz-border-radius: 10px;
+ -webkit-border-radius: 10px;
+}
+
+#about .primary {float: left; width: 165px;}
+#about .primary strong {color: #C64350; display: block; font-size: 1.286em;}
+#about .photo {float: left; margin: 5px 20px;}
+
+#about .url:link, #about .url:visited {text-decoration: none;}
+
+#about .bio {float: right; width: 500px;}
+
+/*
+ Footer
+*****************/
+#contentinfo {padding-bottom: 2em; text-align: right;}
+
+/***** Sections *****/
+/* Blog */
+.hentry {
+ display: block;
+ clear: both;
+ border-bottom: 1px solid #eee;
+ padding: 1.5em 0;
+}
+li:last-child .hentry, #content > .hentry {border: 0; margin: 0;}
+#content > .hentry {padding: 1em 0;}
+.hentry img{display : none ;}
+.entry-title {font-size: 3em; margin-bottom: 10px; margin-top: 0;}
+.entry-title a:link, .entry-title a:visited {text-decoration: none; color: #333;}
+.entry-title a:visited {background-color: #fff;}
+
+.hentry .post-info * {font-style: normal;}
+
+ /* Content */
+ .hentry footer {margin-bottom: 2em;}
+ .hentry footer address {display: inline;}
+ #posts-list footer address {display: block;}
+
+ /* Blog Index */
+ #posts-list {list-style: none; margin: 0;}
+ #posts-list .hentry {padding-left: 10px; position: relative;}
+
+ #posts-list footer {
+ left: 10px;
+ position: relative;
+ float: left;
+ top: 0.5em;
+ width: 190px;
+ }
+
+ /* About the Author */
+ #about-author {
+ background: #f9f9f9;
+ clear: both;
+ font-style: normal;
+ margin: 2em 0;
+ padding: 10px 20px 15px 20px;
+
+ border-radius: 5px;
+ -moz-border-radius: 5px;
+ -webkit-border-radius: 5px;
+ }
+
+ #about-author strong {
+ color: #C64350;
+ clear: both;
+ display: block;
+ font-size: 1.429em;
+ }
+
+ #about-author .photo {border: 1px solid #ddd; float: left; margin: 5px 1em 0 0;}
+
+ /* Comments */
+ #comments-list {list-style: none; margin: 0 1em;}
+ #comments-list blockquote {
+ background: #f8f8f8;
+ clear: both;
+ font-style: normal;
+ margin: 0;
+ padding: 15px 20px;
+
+ border-radius: 5px;
+ -moz-border-radius: 5px;
+ -webkit-border-radius: 5px;
+ }
+ #comments-list footer {color: #888; padding: .5em 1em 0 0; text-align: right;}
+
+ #comments-list li:nth-child(2n) blockquote {background: #F5f5f5;}
+
+ /* Add a Comment */
+ #add-comment label {clear: left; float: left; text-align: left; width: 150px;}
+ #add-comment input[type='text'],
+ #add-comment input[type='email'],
+ #add-comment input[type='url'] {float: left; width: 200px;}
+
+ #add-comment textarea {float: left; height: 150px; width: 495px;}
+
+ #add-comment p.req {clear: both; margin: 0 .5em 1em 0; text-align: right;}
+
+ #add-comment input[type='submit'] {float: right; margin: 0 .5em;}
+ #add-comment * {margin-bottom: .5em;}
diff --git a/pelican/tests/output/custom_locale/theme/css/pygment.css b/pelican/tests/output/custom_locale/theme/css/pygment.css
new file mode 100644
index 00000000..fdd056f6
--- /dev/null
+++ b/pelican/tests/output/custom_locale/theme/css/pygment.css
@@ -0,0 +1,205 @@
+.hll {
+background-color:#eee;
+}
+.c {
+color:#408090;
+font-style:italic;
+}
+.err {
+border:1px solid #FF0000;
+}
+.k {
+color:#007020;
+font-weight:bold;
+}
+.o {
+color:#666666;
+}
+.cm {
+color:#408090;
+font-style:italic;
+}
+.cp {
+color:#007020;
+}
+.c1 {
+color:#408090;
+font-style:italic;
+}
+.cs {
+background-color:#FFF0F0;
+color:#408090;
+}
+.gd {
+color:#A00000;
+}
+.ge {
+font-style:italic;
+}
+.gr {
+color:#FF0000;
+}
+.gh {
+color:#000080;
+font-weight:bold;
+}
+.gi {
+color:#00A000;
+}
+.go {
+color:#303030;
+}
+.gp {
+color:#C65D09;
+font-weight:bold;
+}
+.gs {
+font-weight:bold;
+}
+.gu {
+color:#800080;
+font-weight:bold;
+}
+.gt {
+color:#0040D0;
+}
+.kc {
+color:#007020;
+font-weight:bold;
+}
+.kd {
+color:#007020;
+font-weight:bold;
+}
+.kn {
+color:#007020;
+font-weight:bold;
+}
+.kp {
+color:#007020;
+}
+.kr {
+color:#007020;
+font-weight:bold;
+}
+.kt {
+color:#902000;
+}
+.m {
+color:#208050;
+}
+.s {
+color:#4070A0;
+}
+.na {
+color:#4070A0;
+}
+.nb {
+color:#007020;
+}
+.nc {
+color:#0E84B5;
+font-weight:bold;
+}
+.no {
+color:#60ADD5;
+}
+.nd {
+color:#555555;
+font-weight:bold;
+}
+.ni {
+color:#D55537;
+font-weight:bold;
+}
+.ne {
+color:#007020;
+}
+.nf {
+color:#06287E;
+}
+.nl {
+color:#002070;
+font-weight:bold;
+}
+.nn {
+color:#0E84B5;
+font-weight:bold;
+}
+.nt {
+color:#062873;
+font-weight:bold;
+}
+.nv {
+color:#BB60D5;
+}
+.ow {
+color:#007020;
+font-weight:bold;
+}
+.w {
+color:#BBBBBB;
+}
+.mf {
+color:#208050;
+}
+.mh {
+color:#208050;
+}
+.mi {
+color:#208050;
+}
+.mo {
+color:#208050;
+}
+.sb {
+color:#4070A0;
+}
+.sc {
+color:#4070A0;
+}
+.sd {
+color:#4070A0;
+font-style:italic;
+}
+.s2 {
+color:#4070A0;
+}
+.se {
+color:#4070A0;
+font-weight:bold;
+}
+.sh {
+color:#4070A0;
+}
+.si {
+color:#70A0D0;
+font-style:italic;
+}
+.sx {
+color:#C65D09;
+}
+.sr {
+color:#235388;
+}
+.s1 {
+color:#4070A0;
+}
+.ss {
+color:#517918;
+}
+.bp {
+color:#007020;
+}
+.vc {
+color:#BB60D5;
+}
+.vg {
+color:#BB60D5;
+}
+.vi {
+color:#BB60D5;
+}
+.il {
+color:#208050;
+}
diff --git a/pelican/tests/output/custom_locale/theme/css/reset.css b/pelican/tests/output/custom_locale/theme/css/reset.css
new file mode 100644
index 00000000..c88e6196
--- /dev/null
+++ b/pelican/tests/output/custom_locale/theme/css/reset.css
@@ -0,0 +1,52 @@
+/*
+ Name: Reset Stylesheet
+ Description: Resets browser's default CSS
+ Author: Eric Meyer
+ Author URI: https://meyerweb.com/eric/tools/css/reset/
+*/
+
+/* v1.0 | 20080212 */
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, font, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td {
+ background: transparent;
+ border: 0;
+ font-size: 100%;
+ margin: 0;
+ outline: 0;
+ padding: 0;
+ vertical-align: baseline;
+}
+
+body {line-height: 1;}
+
+ol, ul {list-style: none;}
+
+blockquote, q {quotes: none;}
+
+blockquote:before, blockquote:after,
+q:before, q:after {
+ content: '';
+ content: none;
+}
+
+/* remember to define focus styles! */
+:focus {
+ outline: 0;
+}
+
+/* remember to highlight inserts somehow! */
+ins {text-decoration: none;}
+del {text-decoration: line-through;}
+
+/* tables still need 'cellspacing="0"' in the markup */
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
\ No newline at end of file
diff --git a/pelican/tests/output/custom_locale/theme/css/typogrify.css b/pelican/tests/output/custom_locale/theme/css/typogrify.css
new file mode 100644
index 00000000..3bae4976
--- /dev/null
+++ b/pelican/tests/output/custom_locale/theme/css/typogrify.css
@@ -0,0 +1,3 @@
+.caps {font-size:.92em;}
+.amp {color:#666; font-size:1.05em;font-family:"Warnock Pro", "Goudy Old Style","Palatino","Book Antiqua",serif; font-style:italic;}
+.dquo {margin-left:-.38em;}
diff --git a/pelican/tests/output/custom_locale/theme/css/wide.css b/pelican/tests/output/custom_locale/theme/css/wide.css
new file mode 100644
index 00000000..88fd59ce
--- /dev/null
+++ b/pelican/tests/output/custom_locale/theme/css/wide.css
@@ -0,0 +1,48 @@
+@import url("main.css");
+
+body {
+ font:1.3em/1.3 "Hoefler Text","Georgia",Georgia,serif,sans-serif;
+}
+
+.post-info{
+ display: none;
+}
+
+#banner nav {
+ display: none;
+ -moz-border-radius: 0px;
+ margin-bottom: 20px;
+ overflow: hidden;
+ font-size: 1em;
+ background: #F5F4EF;
+}
+
+#banner nav ul{
+ padding-right: 50px;
+}
+
+#banner nav li{
+ float: right;
+ color: #000;
+}
+
+#banner nav li a {
+ color: #000;
+}
+
+#banner h1 {
+ margin-bottom: -18px;
+}
+
+#featured, #extras {
+ padding: 50px;
+}
+
+#featured {
+ padding-top: 20px;
+}
+
+#extras {
+ padding-top: 0px;
+ padding-bottom: 0px;
+}
diff --git a/pelican/tests/output/custom_locale/theme/fonts/Yanone_Kaffeesatz_400.eot b/pelican/tests/output/custom_locale/theme/fonts/Yanone_Kaffeesatz_400.eot
new file mode 100644
index 00000000..b3b90dbc
Binary files /dev/null and b/pelican/tests/output/custom_locale/theme/fonts/Yanone_Kaffeesatz_400.eot differ
diff --git a/pelican/tests/output/custom_locale/theme/fonts/Yanone_Kaffeesatz_400.svg b/pelican/tests/output/custom_locale/theme/fonts/Yanone_Kaffeesatz_400.svg
new file mode 100644
index 00000000..a69669b5
--- /dev/null
+++ b/pelican/tests/output/custom_locale/theme/fonts/Yanone_Kaffeesatz_400.svg
@@ -0,0 +1,407 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pelican/tests/output/custom_locale/theme/fonts/Yanone_Kaffeesatz_400.ttf b/pelican/tests/output/custom_locale/theme/fonts/Yanone_Kaffeesatz_400.ttf
new file mode 100644
index 00000000..6f4feb02
Binary files /dev/null and b/pelican/tests/output/custom_locale/theme/fonts/Yanone_Kaffeesatz_400.ttf differ
diff --git a/pelican/tests/output/custom_locale/theme/fonts/Yanone_Kaffeesatz_400.woff b/pelican/tests/output/custom_locale/theme/fonts/Yanone_Kaffeesatz_400.woff
new file mode 100644
index 00000000..ddccf765
Binary files /dev/null and b/pelican/tests/output/custom_locale/theme/fonts/Yanone_Kaffeesatz_400.woff differ
diff --git a/pelican/tests/output/custom_locale/theme/fonts/Yanone_Kaffeesatz_400.woff2 b/pelican/tests/output/custom_locale/theme/fonts/Yanone_Kaffeesatz_400.woff2
new file mode 100644
index 00000000..7b18f7ea
Binary files /dev/null and b/pelican/tests/output/custom_locale/theme/fonts/Yanone_Kaffeesatz_400.woff2 differ
diff --git a/pelican/tests/output/custom_locale/theme/fonts/font.css b/pelican/tests/output/custom_locale/theme/fonts/font.css
new file mode 100644
index 00000000..a9eb87e6
--- /dev/null
+++ b/pelican/tests/output/custom_locale/theme/fonts/font.css
@@ -0,0 +1,12 @@
+@font-face {
+ font-family: 'Yanone Kaffeesatz';
+ font-style: normal;
+ font-weight: 400;
+ src:
+ local('Yanone Kaffeesatz Regular'),
+ local('YanoneKaffeesatz-Regular'),
+ /* from https://fonts.gstatic.com/s/yanonekaffeesatz/v8/YDAoLskQQ5MOAgvHUQCcLRTHiN2BPBirwIkMLKUspj4.woff */
+ url('Yanone_Kaffeesatz_400.woff') format('woff'),
+ /* from https://fonts.gstatic.com/s/yanonekaffeesatz/v8/YDAoLskQQ5MOAgvHUQCcLfGwxTS8d1Q9KiDNCMKLFUM.woff2 */
+ url('Yanone_Kaffeesatz_400.woff2') format('woff2');
+}
diff --git a/pelican/tests/output/custom_locale/theme/images/icons/aboutme.png b/pelican/tests/output/custom_locale/theme/images/icons/aboutme.png
new file mode 100644
index 00000000..600110f2
Binary files /dev/null and b/pelican/tests/output/custom_locale/theme/images/icons/aboutme.png differ
diff --git a/pelican/tests/output/custom_locale/theme/images/icons/bitbucket.png b/pelican/tests/output/custom_locale/theme/images/icons/bitbucket.png
new file mode 100644
index 00000000..277a7dfb
Binary files /dev/null and b/pelican/tests/output/custom_locale/theme/images/icons/bitbucket.png differ
diff --git a/pelican/tests/output/custom_locale/theme/images/icons/delicious.png b/pelican/tests/output/custom_locale/theme/images/icons/delicious.png
new file mode 100644
index 00000000..34868c5c
Binary files /dev/null and b/pelican/tests/output/custom_locale/theme/images/icons/delicious.png differ
diff --git a/pelican/tests/output/custom_locale/theme/images/icons/facebook.png b/pelican/tests/output/custom_locale/theme/images/icons/facebook.png
new file mode 100644
index 00000000..1d8a4327
Binary files /dev/null and b/pelican/tests/output/custom_locale/theme/images/icons/facebook.png differ
diff --git a/pelican/tests/output/custom_locale/theme/images/icons/github.png b/pelican/tests/output/custom_locale/theme/images/icons/github.png
new file mode 100644
index 00000000..5d9109de
Binary files /dev/null and b/pelican/tests/output/custom_locale/theme/images/icons/github.png differ
diff --git a/pelican/tests/output/custom_locale/theme/images/icons/gitorious.png b/pelican/tests/output/custom_locale/theme/images/icons/gitorious.png
new file mode 100644
index 00000000..a6705d0f
Binary files /dev/null and b/pelican/tests/output/custom_locale/theme/images/icons/gitorious.png differ
diff --git a/pelican/tests/output/custom_locale/theme/images/icons/gittip.png b/pelican/tests/output/custom_locale/theme/images/icons/gittip.png
new file mode 100644
index 00000000..b9f67aaa
Binary files /dev/null and b/pelican/tests/output/custom_locale/theme/images/icons/gittip.png differ
diff --git a/pelican/tests/output/custom_locale/theme/images/icons/google-groups.png b/pelican/tests/output/custom_locale/theme/images/icons/google-groups.png
new file mode 100644
index 00000000..bbd0a0fd
Binary files /dev/null and b/pelican/tests/output/custom_locale/theme/images/icons/google-groups.png differ
diff --git a/pelican/tests/output/custom_locale/theme/images/icons/google-plus.png b/pelican/tests/output/custom_locale/theme/images/icons/google-plus.png
new file mode 100644
index 00000000..f8553d45
Binary files /dev/null and b/pelican/tests/output/custom_locale/theme/images/icons/google-plus.png differ
diff --git a/pelican/tests/output/custom_locale/theme/images/icons/hackernews.png b/pelican/tests/output/custom_locale/theme/images/icons/hackernews.png
new file mode 100644
index 00000000..8e05e3ee
Binary files /dev/null and b/pelican/tests/output/custom_locale/theme/images/icons/hackernews.png differ
diff --git a/pelican/tests/output/custom_locale/theme/images/icons/lastfm.png b/pelican/tests/output/custom_locale/theme/images/icons/lastfm.png
new file mode 100644
index 00000000..2eedd2da
Binary files /dev/null and b/pelican/tests/output/custom_locale/theme/images/icons/lastfm.png differ
diff --git a/pelican/tests/output/custom_locale/theme/images/icons/linkedin.png b/pelican/tests/output/custom_locale/theme/images/icons/linkedin.png
new file mode 100644
index 00000000..06a88016
Binary files /dev/null and b/pelican/tests/output/custom_locale/theme/images/icons/linkedin.png differ
diff --git a/pelican/tests/output/custom_locale/theme/images/icons/reddit.png b/pelican/tests/output/custom_locale/theme/images/icons/reddit.png
new file mode 100644
index 00000000..d826d3e7
Binary files /dev/null and b/pelican/tests/output/custom_locale/theme/images/icons/reddit.png differ
diff --git a/pelican/tests/output/custom_locale/theme/images/icons/rss.png b/pelican/tests/output/custom_locale/theme/images/icons/rss.png
new file mode 100644
index 00000000..12448f53
Binary files /dev/null and b/pelican/tests/output/custom_locale/theme/images/icons/rss.png differ
diff --git a/pelican/tests/output/custom_locale/theme/images/icons/slideshare.png b/pelican/tests/output/custom_locale/theme/images/icons/slideshare.png
new file mode 100644
index 00000000..9cbe8588
Binary files /dev/null and b/pelican/tests/output/custom_locale/theme/images/icons/slideshare.png differ
diff --git a/pelican/tests/output/custom_locale/theme/images/icons/speakerdeck.png b/pelican/tests/output/custom_locale/theme/images/icons/speakerdeck.png
new file mode 100644
index 00000000..7281ec48
Binary files /dev/null and b/pelican/tests/output/custom_locale/theme/images/icons/speakerdeck.png differ
diff --git a/pelican/tests/output/custom_locale/theme/images/icons/stackoverflow.png b/pelican/tests/output/custom_locale/theme/images/icons/stackoverflow.png
new file mode 100644
index 00000000..3c6862e9
Binary files /dev/null and b/pelican/tests/output/custom_locale/theme/images/icons/stackoverflow.png differ
diff --git a/pelican/tests/output/custom_locale/theme/images/icons/twitter.png b/pelican/tests/output/custom_locale/theme/images/icons/twitter.png
new file mode 100644
index 00000000..cef1cef9
Binary files /dev/null and b/pelican/tests/output/custom_locale/theme/images/icons/twitter.png differ
diff --git a/pelican/tests/output/custom_locale/theme/images/icons/vimeo.png b/pelican/tests/output/custom_locale/theme/images/icons/vimeo.png
new file mode 100644
index 00000000..4b9d7212
Binary files /dev/null and b/pelican/tests/output/custom_locale/theme/images/icons/vimeo.png differ
diff --git a/pelican/tests/output/custom_locale/theme/images/icons/youtube.png b/pelican/tests/output/custom_locale/theme/images/icons/youtube.png
new file mode 100644
index 00000000..e334e68e
Binary files /dev/null and b/pelican/tests/output/custom_locale/theme/images/icons/youtube.png differ
diff --git a/pelican/tests/parse_error/parse_error.rst b/pelican/tests/parse_error/parse_error.rst
new file mode 100644
index 00000000..7aee68e1
--- /dev/null
+++ b/pelican/tests/parse_error/parse_error.rst
@@ -0,0 +1,4 @@
+Page with a parse error
+#############
+
+The underline is too short.
diff --git a/pelican/tests/support.py b/pelican/tests/support.py
new file mode 100644
index 00000000..55ddf625
--- /dev/null
+++ b/pelican/tests/support.py
@@ -0,0 +1,238 @@
+import locale
+import logging
+import os
+import re
+import subprocess
+import sys
+import unittest
+from contextlib import contextmanager
+from functools import wraps
+from io import StringIO
+from logging.handlers import BufferingHandler
+from shutil import rmtree
+from tempfile import mkdtemp
+
+from pelican.contents import Article
+from pelican.readers import default_metadata
+from pelican.settings import DEFAULT_CONFIG
+
+__all__ = ['get_article', 'unittest', ]
+
+
+@contextmanager
+def temporary_folder():
+ """creates a temporary folder, return it and delete it afterwards.
+
+ This allows to do something like this in tests:
+
+ >>> with temporary_folder() as d:
+ # do whatever you want
+ """
+ tempdir = mkdtemp()
+ try:
+ yield tempdir
+ finally:
+ rmtree(tempdir)
+
+
+def isplit(s, sep=None):
+ """Behaves like str.split but returns a generator instead of a list.
+
+ >>> list(isplit('\tUse the force\n')) == '\tUse the force\n'.split()
+ True
+ >>> list(isplit('\tUse the force\n')) == ['Use', 'the', 'force']
+ True
+ >>> (list(isplit('\tUse the force\n', "e"))
+ == '\tUse the force\n'.split("e"))
+ True
+ >>> list(isplit('Use the force', "e")) == 'Use the force'.split("e")
+ True
+ >>> list(isplit('Use the force', "e")) == ['Us', ' th', ' forc', '']
+ True
+
+ """
+ sep, hardsep = r'\s+' if sep is None else re.escape(sep), sep is not None
+ exp, pos, length = re.compile(sep), 0, len(s)
+ while True:
+ m = exp.search(s, pos)
+ if not m:
+ if pos < length or hardsep:
+ # ^ mimic "split()": ''.split() returns []
+ yield s[pos:]
+ break
+ start = m.start()
+ if pos < start or hardsep:
+ # ^ mimic "split()": includes trailing empty string
+ yield s[pos:start]
+ pos = m.end()
+
+
+def mute(returns_output=False):
+ """Decorate a function that prints to stdout, intercepting the output.
+ If "returns_output" is True, the function will return a generator
+ yielding the printed lines instead of the return values.
+
+ The decorator literally hijack sys.stdout during each function
+ execution, so be careful with what you apply it to.
+
+ >>> def numbers():
+ print "42"
+ print "1984"
+ ...
+ >>> numbers()
+ 42
+ 1984
+ >>> mute()(numbers)()
+ >>> list(mute(True)(numbers)())
+ ['42', '1984']
+
+ """
+
+ def decorator(func):
+
+ @wraps(func)
+ def wrapper(*args, **kwargs):
+
+ saved_stdout = sys.stdout
+ sys.stdout = StringIO()
+
+ try:
+ out = func(*args, **kwargs)
+ if returns_output:
+ out = isplit(sys.stdout.getvalue().strip())
+ finally:
+ sys.stdout = saved_stdout
+
+ return out
+
+ return wrapper
+
+ return decorator
+
+
+def get_article(title, content, **extra_metadata):
+ metadata = default_metadata(settings=DEFAULT_CONFIG)
+ metadata['title'] = title
+ if extra_metadata:
+ metadata.update(extra_metadata)
+ return Article(content, metadata=metadata)
+
+
+def skipIfNoExecutable(executable):
+ """Skip test if `executable` is not found
+
+ Tries to run `executable` with subprocess to make sure it's in the path,
+ and skips the tests if not found (if subprocess raises a `OSError`).
+ """
+
+ with open(os.devnull, 'w') as fnull:
+ try:
+ res = subprocess.call(executable, stdout=fnull, stderr=fnull)
+ except OSError:
+ res = None
+
+ if res is None:
+ return unittest.skip('{} executable not found'.format(executable))
+
+ return lambda func: func
+
+
+def module_exists(module_name):
+ """Test if a module is importable."""
+
+ try:
+ __import__(module_name)
+ except ImportError:
+ return False
+ else:
+ return True
+
+
+def locale_available(locale_):
+ old_locale = locale.setlocale(locale.LC_TIME)
+
+ try:
+ locale.setlocale(locale.LC_TIME, str(locale_))
+ except locale.Error:
+ return False
+ else:
+ locale.setlocale(locale.LC_TIME, old_locale)
+ return True
+
+
+def can_symlink():
+ res = True
+ try:
+ with temporary_folder() as f:
+ os.symlink(
+ f,
+ os.path.join(f, 'symlink')
+ )
+ except OSError:
+ res = False
+ return res
+
+
+def get_settings(**kwargs):
+ """Provide tweaked setting dictionaries for testing
+
+ Set keyword arguments to override specific settings.
+ """
+ settings = DEFAULT_CONFIG.copy()
+ for key, value in kwargs.items():
+ settings[key] = value
+ return settings
+
+
+def get_context(settings=None, **kwargs):
+ context = settings.copy() if settings else {}
+ context['generated_content'] = {}
+ context['static_links'] = set()
+ context['static_content'] = {}
+ context.update(kwargs)
+ return context
+
+
+class LogCountHandler(BufferingHandler):
+ """Capturing and counting logged messages."""
+
+ def __init__(self, capacity=1000):
+ super().__init__(capacity)
+
+ def count_logs(self, msg=None, level=None):
+ return len([
+ rec
+ for rec
+ in self.buffer
+ if (msg is None or re.match(msg, rec.getMessage())) and
+ (level is None or rec.levelno == level)
+ ])
+
+ def count_formatted_logs(self, msg=None, level=None):
+ return len([
+ rec
+ for rec
+ in self.buffer
+ if (msg is None or re.search(msg, self.format(rec))) and
+ (level is None or rec.levelno == level)
+ ])
+
+
+class LoggedTestCase(unittest.TestCase):
+ """A test case that captures log messages."""
+
+ def setUp(self):
+ super().setUp()
+ self._logcount_handler = LogCountHandler()
+ logging.getLogger().addHandler(self._logcount_handler)
+
+ def tearDown(self):
+ logging.getLogger().removeHandler(self._logcount_handler)
+ super().tearDown()
+
+ def assertLogCountEqual(self, count=None, msg=None, **kwargs):
+ actual = self._logcount_handler.count_logs(msg=msg, **kwargs)
+ self.assertEqual(
+ actual, count,
+ msg='expected {} occurrences of {!r}, but found {}'.format(
+ count, msg, actual))
diff --git a/pelican/tests/test_cache.py b/pelican/tests/test_cache.py
new file mode 100644
index 00000000..564f1d31
--- /dev/null
+++ b/pelican/tests/test_cache.py
@@ -0,0 +1,279 @@
+import os
+from shutil import rmtree
+from tempfile import mkdtemp
+from unittest.mock import MagicMock
+
+from pelican.generators import ArticlesGenerator, PagesGenerator
+from pelican.tests.support import get_context, get_settings, unittest
+
+
+CUR_DIR = os.path.dirname(__file__)
+CONTENT_DIR = os.path.join(CUR_DIR, 'content')
+
+
+class TestCache(unittest.TestCase):
+
+ def setUp(self):
+ self.temp_cache = mkdtemp(prefix='pelican_cache.')
+
+ def tearDown(self):
+ rmtree(self.temp_cache)
+
+ def _get_cache_enabled_settings(self):
+ settings = get_settings()
+ settings['CACHE_CONTENT'] = True
+ settings['LOAD_CONTENT_CACHE'] = True
+ settings['CACHE_PATH'] = self.temp_cache
+ return settings
+
+ def test_generator_caching(self):
+ """Test that cached and uncached content is same in generator level"""
+ settings = self._get_cache_enabled_settings()
+ settings['CONTENT_CACHING_LAYER'] = 'generator'
+ settings['PAGE_PATHS'] = ['TestPages']
+ settings['DEFAULT_DATE'] = (1970, 1, 1)
+ settings['READERS'] = {'asc': None}
+ context = get_context(settings)
+
+ def sorted_titles(items):
+ return sorted(item.title for item in items)
+
+ # Articles
+ generator = ArticlesGenerator(
+ context=context.copy(), settings=settings,
+ path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
+ generator.generate_context()
+ uncached_articles = sorted_titles(generator.articles)
+ uncached_drafts = sorted_titles(generator.drafts)
+
+ generator = ArticlesGenerator(
+ context=context.copy(), settings=settings,
+ path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
+ generator.generate_context()
+ cached_articles = sorted_titles(generator.articles)
+ cached_drafts = sorted_titles(generator.drafts)
+
+ self.assertEqual(uncached_articles, cached_articles)
+ self.assertEqual(uncached_drafts, cached_drafts)
+
+ # Pages
+ generator = PagesGenerator(
+ context=context.copy(), settings=settings,
+ path=CUR_DIR, theme=settings['THEME'], output_path=None)
+ generator.generate_context()
+ uncached_pages = sorted_titles(generator.pages)
+ uncached_hidden_pages = sorted_titles(generator.hidden_pages)
+ uncached_draft_pages = sorted_titles(generator.draft_pages)
+
+ generator = PagesGenerator(
+ context=context.copy(), settings=settings,
+ path=CUR_DIR, theme=settings['THEME'], output_path=None)
+ generator.generate_context()
+ cached_pages = sorted_titles(generator.pages)
+ cached_hidden_pages = sorted_titles(generator.hidden_pages)
+ cached_draft_pages = sorted_titles(generator.draft_pages)
+
+ self.assertEqual(uncached_pages, cached_pages)
+ self.assertEqual(uncached_hidden_pages, cached_hidden_pages)
+ self.assertEqual(uncached_draft_pages, cached_draft_pages)
+
+ def test_reader_caching(self):
+ """Test that cached and uncached content is same in reader level"""
+ settings = self._get_cache_enabled_settings()
+ settings['CONTENT_CACHING_LAYER'] = 'reader'
+ settings['PAGE_PATHS'] = ['TestPages']
+ settings['DEFAULT_DATE'] = (1970, 1, 1)
+ settings['READERS'] = {'asc': None}
+ context = get_context(settings)
+
+ def sorted_titles(items):
+ return sorted(item.title for item in items)
+
+ # Articles
+ generator = ArticlesGenerator(
+ context=context.copy(), settings=settings,
+ path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
+ generator.generate_context()
+ uncached_articles = sorted_titles(generator.articles)
+ uncached_drafts = sorted_titles(generator.drafts)
+
+ generator = ArticlesGenerator(
+ context=context.copy(), settings=settings,
+ path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
+ generator.generate_context()
+ cached_articles = sorted_titles(generator.articles)
+ cached_drafts = sorted_titles(generator.drafts)
+
+ self.assertEqual(uncached_articles, cached_articles)
+ self.assertEqual(uncached_drafts, cached_drafts)
+
+ # Pages
+ generator = PagesGenerator(
+ context=context.copy(), settings=settings,
+ path=CUR_DIR, theme=settings['THEME'], output_path=None)
+ generator.generate_context()
+ uncached_pages = sorted_titles(generator.pages)
+ uncached_hidden_pages = sorted_titles(generator.hidden_pages)
+
+ generator = PagesGenerator(
+ context=context.copy(), settings=settings,
+ path=CUR_DIR, theme=settings['THEME'], output_path=None)
+ generator.generate_context()
+ cached_pages = sorted_titles(generator.pages)
+ cached_hidden_pages = sorted_titles(generator.hidden_pages)
+
+ self.assertEqual(uncached_pages, cached_pages)
+ self.assertEqual(uncached_hidden_pages, cached_hidden_pages)
+
+ def test_article_object_caching(self):
+ """Test Article objects caching at the generator level"""
+ settings = self._get_cache_enabled_settings()
+ settings['CONTENT_CACHING_LAYER'] = 'generator'
+ settings['DEFAULT_DATE'] = (1970, 1, 1)
+ settings['READERS'] = {'asc': None}
+ context = get_context(settings)
+
+ generator = ArticlesGenerator(
+ context=context.copy(), settings=settings,
+ path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
+ generator.generate_context()
+ self.assertTrue(hasattr(generator, '_cache'))
+
+ generator = ArticlesGenerator(
+ context=context.copy(), settings=settings,
+ path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
+ generator.readers.read_file = MagicMock()
+ generator.generate_context()
+ """
+ 6 files don't get cached because they were not valid
+ - article_with_attributes_containing_double_quotes.html
+ - article_with_comments.html
+ - article_with_null_attributes.html
+ - 2012-11-30_md_w_filename_meta#foo-bar.md
+ - empty.md
+ - empty_with_bom.md
+ """
+ self.assertEqual(generator.readers.read_file.call_count, 6)
+
+ def test_article_reader_content_caching(self):
+ """Test raw article content caching at the reader level"""
+ settings = self._get_cache_enabled_settings()
+ settings['READERS'] = {'asc': None}
+ context = get_context(settings)
+
+ generator = ArticlesGenerator(
+ context=context.copy(), settings=settings,
+ path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
+ generator.generate_context()
+ self.assertTrue(hasattr(generator.readers, '_cache'))
+
+ generator = ArticlesGenerator(
+ context=context.copy(), settings=settings,
+ path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
+ readers = generator.readers.readers
+ for reader in readers.values():
+ reader.read = MagicMock()
+ generator.generate_context()
+ for reader in readers.values():
+ self.assertEqual(reader.read.call_count, 0)
+
+ def test_article_ignore_cache(self):
+ """Test that all the articles are read again when not loading cache
+
+ used in --ignore-cache or autoreload mode"""
+ settings = self._get_cache_enabled_settings()
+ settings['READERS'] = {'asc': None}
+ context = get_context(settings)
+
+ generator = ArticlesGenerator(
+ context=context.copy(), settings=settings,
+ path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
+ generator.readers.read_file = MagicMock()
+ generator.generate_context()
+ self.assertTrue(hasattr(generator, '_cache_open'))
+ orig_call_count = generator.readers.read_file.call_count
+
+ settings['LOAD_CONTENT_CACHE'] = False
+ generator = ArticlesGenerator(
+ context=context.copy(), settings=settings,
+ path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
+ generator.readers.read_file = MagicMock()
+ generator.generate_context()
+ self.assertEqual(
+ generator.readers.read_file.call_count,
+ orig_call_count)
+
+ def test_page_object_caching(self):
+ """Test Page objects caching at the generator level"""
+ settings = self._get_cache_enabled_settings()
+ settings['CONTENT_CACHING_LAYER'] = 'generator'
+ settings['PAGE_PATHS'] = ['TestPages']
+ settings['READERS'] = {'asc': None}
+ context = get_context(settings)
+
+ generator = PagesGenerator(
+ context=context.copy(), settings=settings,
+ path=CUR_DIR, theme=settings['THEME'], output_path=None)
+ generator.generate_context()
+ self.assertTrue(hasattr(generator, '_cache'))
+
+ generator = PagesGenerator(
+ context=context.copy(), settings=settings,
+ path=CUR_DIR, theme=settings['THEME'], output_path=None)
+ generator.readers.read_file = MagicMock()
+ generator.generate_context()
+ """
+ 1 File doesn't get cached because it was not valid
+ - bad_page.rst
+ """
+ self.assertEqual(generator.readers.read_file.call_count, 1)
+
+ def test_page_reader_content_caching(self):
+ """Test raw page content caching at the reader level"""
+ settings = self._get_cache_enabled_settings()
+ settings['PAGE_PATHS'] = ['TestPages']
+ settings['READERS'] = {'asc': None}
+ context = get_context(settings)
+
+ generator = PagesGenerator(
+ context=context.copy(), settings=settings,
+ path=CUR_DIR, theme=settings['THEME'], output_path=None)
+ generator.generate_context()
+ self.assertTrue(hasattr(generator.readers, '_cache'))
+
+ generator = PagesGenerator(
+ context=context.copy(), settings=settings,
+ path=CUR_DIR, theme=settings['THEME'], output_path=None)
+ readers = generator.readers.readers
+ for reader in readers.values():
+ reader.read = MagicMock()
+ generator.generate_context()
+ for reader in readers.values():
+ self.assertEqual(reader.read.call_count, 0)
+
+ def test_page_ignore_cache(self):
+ """Test that all the pages are read again when not loading cache
+
+ used in --ignore_cache or autoreload mode"""
+ settings = self._get_cache_enabled_settings()
+ settings['PAGE_PATHS'] = ['TestPages']
+ settings['READERS'] = {'asc': None}
+ context = get_context(settings)
+
+ generator = PagesGenerator(
+ context=context.copy(), settings=settings,
+ path=CUR_DIR, theme=settings['THEME'], output_path=None)
+ generator.readers.read_file = MagicMock()
+ generator.generate_context()
+ self.assertTrue(hasattr(generator, '_cache_open'))
+ orig_call_count = generator.readers.read_file.call_count
+
+ settings['LOAD_CONTENT_CACHE'] = False
+ generator = PagesGenerator(
+ context=context.copy(), settings=settings,
+ path=CUR_DIR, theme=settings['THEME'], output_path=None)
+ generator.readers.read_file = MagicMock()
+ generator.generate_context()
+ self.assertEqual(
+ generator.readers.read_file.call_count,
+ orig_call_count)
diff --git a/pelican/tests/test_contents.py b/pelican/tests/test_contents.py
new file mode 100644
index 00000000..1a520bc7
--- /dev/null
+++ b/pelican/tests/test_contents.py
@@ -0,0 +1,981 @@
+import datetime
+import locale
+import logging
+import os.path
+from posixpath import join as posix_join
+from sys import platform
+
+from jinja2.utils import generate_lorem_ipsum
+
+from pelican.contents import Article, Author, Category, Page, Static
+from pelican.plugins.signals import content_object_init
+from pelican.settings import DEFAULT_CONFIG
+from pelican.tests.support import (LoggedTestCase, get_context, get_settings,
+ unittest)
+from pelican.utils import (path_to_url, posixize_path, truncate_html_words)
+
+
+# generate one paragraph, enclosed with
+TEST_CONTENT = str(generate_lorem_ipsum(n=1))
+TEST_SUMMARY = generate_lorem_ipsum(n=1, html=False)
+
+
+class TestBase(LoggedTestCase):
+
+ def setUp(self):
+ super().setUp()
+ self.old_locale = locale.setlocale(locale.LC_ALL)
+ locale.setlocale(locale.LC_ALL, 'C')
+ self.page_kwargs = {
+ 'content': TEST_CONTENT,
+ 'context': {
+ 'localsiteurl': '',
+ },
+ 'metadata': {
+ 'summary': TEST_SUMMARY,
+ 'title': 'foo bar',
+ 'author': Author('Blogger', DEFAULT_CONFIG),
+ },
+ 'source_path': '/path/to/file/foo.ext'
+ }
+ self._disable_limit_filter()
+
+ def tearDown(self):
+ locale.setlocale(locale.LC_ALL, self.old_locale)
+ self._enable_limit_filter()
+
+ def _disable_limit_filter(self):
+ from pelican.contents import logger
+ logger.disable_filter()
+
+ def _enable_limit_filter(self):
+ from pelican.contents import logger
+ logger.enable_filter()
+
+ def _copy_page_kwargs(self):
+ # make a deep copy of page_kwargs
+ page_kwargs = {key: self.page_kwargs[key] for key in self.page_kwargs}
+ for key in page_kwargs:
+ if not isinstance(page_kwargs[key], dict):
+ break
+ page_kwargs[key] = {
+ subkey: page_kwargs[key][subkey] for subkey in page_kwargs[key]
+ }
+
+ return page_kwargs
+
+
+class TestPage(TestBase):
+ def test_use_args(self):
+ # Creating a page with arguments passed to the constructor should use
+ # them to initialise object's attributes.
+ metadata = {'foo': 'bar', 'foobar': 'baz', 'title': 'foobar', }
+ page = Page(TEST_CONTENT, metadata=metadata,
+ context={'localsiteurl': ''})
+ for key, value in metadata.items():
+ self.assertTrue(hasattr(page, key))
+ self.assertEqual(value, getattr(page, key))
+ self.assertEqual(page.content, TEST_CONTENT)
+
+ def test_mandatory_properties(self):
+ # If the title is not set, must throw an exception.
+ page = Page('content')
+ self.assertFalse(page._has_valid_mandatory_properties())
+ self.assertLogCountEqual(
+ count=1,
+ msg="Skipping .*: could not find information about 'title'",
+ level=logging.ERROR)
+ page = Page('content', metadata={'title': 'foobar'})
+ self.assertTrue(page._has_valid_mandatory_properties())
+
+ def test_summary_from_metadata(self):
+ # If a :summary: metadata is given, it should be used
+ page = Page(**self.page_kwargs)
+ self.assertEqual(page.summary, TEST_SUMMARY)
+
+ def test_summary_max_length(self):
+ # If a :SUMMARY_MAX_LENGTH: is set, and there is no other summary,
+ # generated summary should not exceed the given length.
+ page_kwargs = self._copy_page_kwargs()
+ settings = get_settings()
+ page_kwargs['settings'] = settings
+ del page_kwargs['metadata']['summary']
+ settings['SUMMARY_MAX_LENGTH'] = None
+ page = Page(**page_kwargs)
+ self.assertEqual(page.summary, TEST_CONTENT)
+ settings['SUMMARY_MAX_LENGTH'] = 10
+ page = Page(**page_kwargs)
+ self.assertEqual(page.summary, truncate_html_words(TEST_CONTENT, 10))
+ settings['SUMMARY_MAX_LENGTH'] = 0
+ page = Page(**page_kwargs)
+ self.assertEqual(page.summary, '')
+
+ def test_summary_end_suffix(self):
+ # If a :SUMMARY_END_SUFFIX: is set, and there is no other summary,
+ # generated summary should contain the specified marker at the end.
+ page_kwargs = self._copy_page_kwargs()
+ settings = get_settings()
+ page_kwargs['settings'] = settings
+ del page_kwargs['metadata']['summary']
+ settings['SUMMARY_END_SUFFIX'] = 'test_marker'
+ settings['SUMMARY_MAX_LENGTH'] = 10
+ page = Page(**page_kwargs)
+ self.assertEqual(page.summary, truncate_html_words(TEST_CONTENT, 10,
+ 'test_marker'))
+ self.assertIn('test_marker', page.summary)
+
+ def test_summary_get_summary_warning(self):
+ """calling ._get_summary() should issue a warning"""
+ page_kwargs = self._copy_page_kwargs()
+ page = Page(**page_kwargs)
+ self.assertEqual(page.summary, TEST_SUMMARY)
+ self.assertEqual(page._get_summary(), TEST_SUMMARY)
+ self.assertLogCountEqual(
+ count=1,
+ msg=r"_get_summary\(\) has been deprecated since 3\.6\.4\. "
+ "Use the summary decorator instead",
+ level=logging.WARNING)
+
+ def test_slug(self):
+ page_kwargs = self._copy_page_kwargs()
+ settings = get_settings()
+ page_kwargs['settings'] = settings
+ settings['SLUGIFY_SOURCE'] = "title"
+ page = Page(**page_kwargs)
+ self.assertEqual(page.slug, 'foo-bar')
+ settings['SLUGIFY_SOURCE'] = "basename"
+ page = Page(**page_kwargs)
+ self.assertEqual(page.slug, 'foo')
+
+ # test slug from title with unicode and case
+
+ inputs = (
+ # (title, expected, preserve_case, use_unicode)
+ ('指導書', 'zhi-dao-shu', False, False),
+ ('指導書', 'Zhi-Dao-Shu', True, False),
+ ('指導書', '指導書', False, True),
+ ('指導書', '指導書', True, True),
+ ('Çığ', 'cig', False, False),
+ ('Çığ', 'Cig', True, False),
+ ('Çığ', 'çığ', False, True),
+ ('Çığ', 'Çığ', True, True),
+ )
+
+ settings = get_settings()
+ page_kwargs = self._copy_page_kwargs()
+ page_kwargs['settings'] = settings
+
+ for title, expected, preserve_case, use_unicode in inputs:
+ settings['SLUGIFY_PRESERVE_CASE'] = preserve_case
+ settings['SLUGIFY_USE_UNICODE'] = use_unicode
+ page_kwargs['metadata']['title'] = title
+ page = Page(**page_kwargs)
+ self.assertEqual(page.slug, expected,
+ (title, preserve_case, use_unicode))
+
+ def test_defaultlang(self):
+ # If no lang is given, default to the default one.
+ page = Page(**self.page_kwargs)
+ self.assertEqual(page.lang, DEFAULT_CONFIG['DEFAULT_LANG'])
+
+ # it is possible to specify the lang in the metadata infos
+ self.page_kwargs['metadata'].update({'lang': 'fr', })
+ page = Page(**self.page_kwargs)
+ self.assertEqual(page.lang, 'fr')
+
+ def test_save_as(self):
+ # If a lang is not the default lang, save_as should be set
+ # accordingly.
+
+ # if a title is defined, save_as should be set
+ page = Page(**self.page_kwargs)
+ self.assertEqual(page.save_as, "pages/foo-bar.html")
+
+ # if a language is defined, save_as should include it accordingly
+ self.page_kwargs['metadata'].update({'lang': 'fr', })
+ page = Page(**self.page_kwargs)
+ self.assertEqual(page.save_as, "pages/foo-bar-fr.html")
+
+ def test_relative_source_path(self):
+ # 'relative_source_path' should be the relative path
+ # from 'PATH' to 'source_path'
+ page_kwargs = self._copy_page_kwargs()
+
+ # If 'source_path' is None, 'relative_source_path' should
+ # also return None
+ page_kwargs['source_path'] = None
+ page = Page(**page_kwargs)
+ self.assertIsNone(page.relative_source_path)
+
+ page_kwargs = self._copy_page_kwargs()
+ settings = get_settings()
+ full_path = page_kwargs['source_path']
+
+ settings['PATH'] = os.path.dirname(full_path)
+ page_kwargs['settings'] = settings
+ page = Page(**page_kwargs)
+
+ # if 'source_path' is set, 'relative_source_path' should
+ # return the relative path from 'PATH' to 'source_path'
+ self.assertEqual(
+ page.relative_source_path,
+ os.path.relpath(
+ full_path,
+ os.path.dirname(full_path)
+ ))
+
+ def test_metadata_url_format(self):
+ # Arbitrary metadata should be passed through url_format()
+ page = Page(**self.page_kwargs)
+ self.assertIn('summary', page.url_format.keys())
+ page.metadata['directory'] = 'test-dir'
+ page.settings = get_settings(PAGE_SAVE_AS='{directory}/{slug}')
+ self.assertEqual(page.save_as, 'test-dir/foo-bar')
+
+ def test_datetime(self):
+ # If DATETIME is set to a tuple, it should be used to override LOCALE
+ dt = datetime.datetime(2015, 9, 13)
+
+ page_kwargs = self._copy_page_kwargs()
+
+ # set its date to dt
+ page_kwargs['metadata']['date'] = dt
+ page = Page(**page_kwargs)
+
+ # page.locale_date is a unicode string in both python2 and python3
+ dt_date = dt.strftime(DEFAULT_CONFIG['DEFAULT_DATE_FORMAT'])
+
+ self.assertEqual(page.locale_date, dt_date)
+ page_kwargs['settings'] = get_settings()
+
+ # I doubt this can work on all platforms ...
+ if platform == "win32":
+ locale = 'jpn'
+ else:
+ locale = 'ja_JP.utf8'
+ page_kwargs['settings']['DATE_FORMATS'] = {'jp': (locale,
+ '%Y-%m-%d(%a)')}
+ page_kwargs['metadata']['lang'] = 'jp'
+
+ import locale as locale_module
+ try:
+ page = Page(**page_kwargs)
+ self.assertEqual(page.locale_date, '2015-09-13(\u65e5)')
+ except locale_module.Error:
+ # The constructor of ``Page`` will try to set the locale to
+ # ``ja_JP.utf8``. But this attempt will failed when there is no
+ # such locale in the system. You can see which locales there are
+ # in your system with ``locale -a`` command.
+ #
+ # Until we find some other method to test this functionality, we
+ # will simply skip this test.
+ unittest.skip("There is no locale %s in this system." % locale)
+
+ def test_template(self):
+ # Pages default to page, metadata overwrites
+ default_page = Page(**self.page_kwargs)
+ self.assertEqual('page', default_page.template)
+ page_kwargs = self._copy_page_kwargs()
+ page_kwargs['metadata']['template'] = 'custom'
+ custom_page = Page(**page_kwargs)
+ self.assertEqual('custom', custom_page.template)
+
+ def test_signal(self):
+ def receiver_test_function(sender):
+ receiver_test_function.has_been_called = True
+ pass
+ receiver_test_function.has_been_called = False
+
+ content_object_init.connect(receiver_test_function)
+ self.assertIn(
+ receiver_test_function,
+ content_object_init.receivers_for(Page))
+
+ self.assertFalse(receiver_test_function.has_been_called)
+ Page(**self.page_kwargs)
+ self.assertTrue(receiver_test_function.has_been_called)
+
+ def test_get_content(self):
+ # Test that the content is updated with the relative links to
+ # filenames, tags and categories.
+ settings = get_settings()
+ args = self.page_kwargs.copy()
+ args['settings'] = settings
+
+ # Tag
+ args['content'] = ('A simple test, with a '
+ 'link ')
+ page = Page(**args)
+ content = page.get_content('http://notmyidea.org')
+ self.assertEqual(
+ content,
+ ('A simple test, with a '
+ 'link '))
+
+ # Category
+ args['content'] = ('A simple test, with a '
+ 'link ')
+ page = Page(**args)
+ content = page.get_content('http://notmyidea.org')
+ self.assertEqual(
+ content,
+ ('A simple test, with a '
+ 'link '))
+
+ def test_intrasite_link(self):
+ cls_name = '_DummyArticle'
+ article = type(cls_name, (object,), {'url': 'article.html'})
+
+ args = self.page_kwargs.copy()
+ args['settings'] = get_settings()
+ args['source_path'] = 'content'
+ args['context']['generated_content'] = {'article.rst': article}
+
+ # Classic intrasite link via filename
+ args['content'] = (
+ 'A simple test, with a '
+ 'link '
+ )
+ content = Page(**args).get_content('http://notmyidea.org')
+ self.assertEqual(
+ content,
+ 'A simple test, with a '
+ 'link '
+ )
+
+ # fragment
+ args['content'] = (
+ 'A simple test, with a '
+ 'link '
+ )
+ content = Page(**args).get_content('http://notmyidea.org')
+ self.assertEqual(
+ content,
+ 'A simple test, with a '
+ 'link '
+ )
+
+ # query
+ args['content'] = (
+ 'A simple test, with a '
+ 'link '
+ )
+ content = Page(**args).get_content('http://notmyidea.org')
+ self.assertEqual(
+ content,
+ 'A simple test, with a '
+ 'link '
+ )
+
+ # combination
+ args['content'] = (
+ 'A simple test, with a '
+ 'link '
+ )
+ content = Page(**args).get_content('http://notmyidea.org')
+ self.assertEqual(
+ content,
+ 'A simple test, with a '
+ 'link '
+ )
+
+ # also test for summary in metadata
+ parsed = (
+ 'A simple summary test, with a '
+ 'link '
+ )
+ linked = (
+ 'A simple summary test, with a '
+ 'link '
+ )
+ args['settings']['FORMATTED_FIELDS'] = ['summary', 'custom']
+ args['metadata']['summary'] = parsed
+ args['metadata']['custom'] = parsed
+ args['context']['localsiteurl'] = 'http://notmyidea.org'
+ p = Page(**args)
+ # This is called implicitly from all generators and Pelican.run() once
+ # all files are processed. Here we process just one page so it needs
+ # to be called explicitly.
+ p.refresh_metadata_intersite_links()
+ self.assertEqual(p.summary, linked)
+ self.assertEqual(p.custom, linked)
+
+ def test_intrasite_link_more(self):
+ cls_name = '_DummyAsset'
+
+ args = self.page_kwargs.copy()
+ args['settings'] = get_settings()
+ args['source_path'] = 'content'
+ args['context']['static_content'] = {
+ 'images/poster.jpg':
+ type(cls_name, (object,), {'url': 'images/poster.jpg'}),
+ 'assets/video.mp4':
+ type(cls_name, (object,), {'url': 'assets/video.mp4'}),
+ 'images/graph.svg':
+ type(cls_name, (object,), {'url': 'images/graph.svg'}),
+ }
+ args['context']['generated_content'] = {
+ 'reference.rst':
+ type(cls_name, (object,), {'url': 'reference.html'}),
+ }
+
+ # video.poster
+ args['content'] = (
+ 'There is a video with poster '
+ ''
+ ''
+ ' '
+ )
+ content = Page(**args).get_content('http://notmyidea.org')
+ self.assertEqual(
+ content,
+ 'There is a video with poster '
+ ''
+ ''
+ ' '
+ )
+
+ # object.data
+ args['content'] = (
+ 'There is a svg object '
+ ''
+ ' '
+ )
+ content = Page(**args).get_content('http://notmyidea.org')
+ self.assertEqual(
+ content,
+ 'There is a svg object '
+ ''
+ ' '
+ )
+
+ # blockquote.cite
+ args['content'] = (
+ 'There is a blockquote with cite attribute '
+ '
blah blah '
+ )
+ content = Page(**args).get_content('http://notmyidea.org')
+ self.assertEqual(
+ content,
+ 'There is a blockquote with cite attribute '
+ ''
+ 'blah blah'
+ ' '
+ )
+
+ def test_intrasite_link_absolute(self):
+ """Test that absolute URLs are merged properly."""
+
+ args = self.page_kwargs.copy()
+ args['settings'] = get_settings(
+ STATIC_URL='http://static.cool.site/{path}',
+ ARTICLE_URL='http://blog.cool.site/{slug}.html')
+ args['source_path'] = 'content'
+ args['context']['static_content'] = {
+ 'images/poster.jpg':
+ Static('', settings=args['settings'],
+ source_path='images/poster.jpg'),
+ }
+ args['context']['generated_content'] = {
+ 'article.rst':
+ Article('', settings=args['settings'], metadata={
+ 'slug': 'article', 'title': 'Article'})
+ }
+
+ # Article link will go to blog
+ args['content'] = (
+ 'Article '
+ )
+ content = Page(**args).get_content('http://cool.site')
+ self.assertEqual(
+ content,
+ 'Article '
+ )
+
+ # Page link will go to the main site
+ args['content'] = (
+ 'Index '
+ )
+ content = Page(**args).get_content('http://cool.site')
+ self.assertEqual(
+ content,
+ 'Index '
+ )
+
+ # Image link will go to static
+ args['content'] = (
+ ' '
+ )
+ content = Page(**args).get_content('http://cool.site')
+ self.assertEqual(
+ content,
+ ' '
+ )
+
+ def test_intrasite_link_markdown_spaces(self):
+ cls_name = '_DummyArticle'
+ article = type(cls_name, (object,), {'url': 'article-spaces.html'})
+
+ args = self.page_kwargs.copy()
+ args['settings'] = get_settings()
+ args['source_path'] = 'content'
+ args['context']['generated_content'] = {'article spaces.rst': article}
+
+ # An intrasite link via filename with %20 as a space
+ args['content'] = (
+ 'A simple test, with a '
+ 'link '
+ )
+ content = Page(**args).get_content('http://notmyidea.org')
+ self.assertEqual(
+ content,
+ 'A simple test, with a '
+ 'link '
+ )
+
+ def test_intrasite_link_source_and_generated(self):
+ """Test linking both to the source and the generated article
+ """
+ cls_name = '_DummyAsset'
+
+ args = self.page_kwargs.copy()
+ args['settings'] = get_settings()
+ args['source_path'] = 'content'
+ args['context']['generated_content'] = {
+ 'article.rst': type(cls_name, (object,), {'url': 'article.html'})}
+ args['context']['static_content'] = {
+ 'article.rst': type(cls_name, (object,), {'url': 'article.rst'})}
+
+ args['content'] = (
+ 'A simple test, with a link to an'
+ 'article and its'
+ 'source '
+ )
+ content = Page(**args).get_content('http://notmyidea.org')
+ self.assertEqual(
+ content,
+ 'A simple test, with a link to an'
+ 'article and its'
+ 'source '
+ )
+
+ def test_intrasite_link_to_static_content_with_filename(self):
+ """Test linking to a static resource with deprecated {filename}
+ """
+ cls_name = '_DummyAsset'
+
+ args = self.page_kwargs.copy()
+ args['settings'] = get_settings()
+ args['source_path'] = 'content'
+ args['context']['static_content'] = {
+ 'poster.jpg':
+ type(cls_name, (object,), {'url': 'images/poster.jpg'})}
+
+ args['content'] = (
+ 'A simple test, with a link to a'
+ 'poster '
+ )
+ content = Page(**args).get_content('http://notmyidea.org')
+ self.assertEqual(
+ content,
+ 'A simple test, with a link to a'
+ 'poster '
+ )
+
+ def test_multiple_authors(self):
+ """Test article with multiple authors."""
+ args = self.page_kwargs.copy()
+ content = Page(**args)
+ assert content.authors == [content.author]
+ args['metadata'].pop('author')
+ args['metadata']['authors'] = [Author('First Author', DEFAULT_CONFIG),
+ Author('Second Author', DEFAULT_CONFIG)]
+ content = Page(**args)
+ assert content.authors
+ assert content.author == content.authors[0]
+
+
+class TestArticle(TestBase):
+ def test_template(self):
+ # Articles default to article, metadata overwrites
+ default_article = Article(**self.page_kwargs)
+ self.assertEqual('article', default_article.template)
+ article_kwargs = self._copy_page_kwargs()
+ article_kwargs['metadata']['template'] = 'custom'
+ custom_article = Article(**article_kwargs)
+ self.assertEqual('custom', custom_article.template)
+
+ def test_slugify_category_author(self):
+ settings = get_settings()
+ settings['SLUG_REGEX_SUBSTITUTIONS'] = [
+ (r'C#', 'csharp'),
+ (r'[^\w\s-]', ''),
+ (r'(?u)\A\s*', ''),
+ (r'(?u)\s*\Z', ''),
+ (r'[-\s]+', '-'),
+ ]
+ settings['ARTICLE_URL'] = '{author}/{category}/{slug}/'
+ settings['ARTICLE_SAVE_AS'] = '{author}/{category}/{slug}/index.html'
+ article_kwargs = self._copy_page_kwargs()
+ article_kwargs['metadata']['author'] = Author("O'Brien", settings)
+ article_kwargs['metadata']['category'] = Category(
+ 'C# & stuff', settings)
+ article_kwargs['metadata']['title'] = 'fnord'
+ article_kwargs['settings'] = settings
+ article = Article(**article_kwargs)
+ self.assertEqual(article.url, 'obrien/csharp-stuff/fnord/')
+ self.assertEqual(
+ article.save_as, 'obrien/csharp-stuff/fnord/index.html')
+
+ def test_slugify_with_author_substitutions(self):
+ settings = get_settings()
+ settings['AUTHOR_REGEX_SUBSTITUTIONS'] = [
+ ('Alexander Todorov', 'atodorov'),
+ ('Krasimir Tsonev', 'krasimir'),
+ (r'[^\w\s-]', ''),
+ (r'(?u)\A\s*', ''),
+ (r'(?u)\s*\Z', ''),
+ (r'[-\s]+', '-'),
+ ]
+ settings['ARTICLE_URL'] = 'blog/{author}/{slug}/'
+ settings['ARTICLE_SAVE_AS'] = 'blog/{author}/{slug}/index.html'
+ article_kwargs = self._copy_page_kwargs()
+ article_kwargs['metadata']['author'] = Author('Alexander Todorov',
+ settings)
+ article_kwargs['metadata']['title'] = 'fnord'
+ article_kwargs['settings'] = settings
+ article = Article(**article_kwargs)
+ self.assertEqual(article.url, 'blog/atodorov/fnord/')
+ self.assertEqual(article.save_as, 'blog/atodorov/fnord/index.html')
+
+ def test_slugify_category_with_dots(self):
+ settings = get_settings()
+ settings['CATEGORY_REGEX_SUBSTITUTIONS'] = [
+ ('Fedora QA', 'fedora.qa'),
+ ]
+ settings['ARTICLE_URL'] = '{category}/{slug}/'
+ article_kwargs = self._copy_page_kwargs()
+ article_kwargs['metadata']['category'] = Category('Fedora QA',
+ settings)
+ article_kwargs['metadata']['title'] = 'This Week in Fedora QA'
+ article_kwargs['settings'] = settings
+ article = Article(**article_kwargs)
+ self.assertEqual(article.url, 'fedora.qa/this-week-in-fedora-qa/')
+
+ def test_valid_save_as_detects_breakout(self):
+ settings = get_settings()
+ article_kwargs = self._copy_page_kwargs()
+ article_kwargs['metadata']['slug'] = '../foo'
+ article_kwargs['settings'] = settings
+ article = Article(**article_kwargs)
+ self.assertFalse(article._has_valid_save_as())
+
+ def test_valid_save_as_detects_breakout_to_root(self):
+ settings = get_settings()
+ article_kwargs = self._copy_page_kwargs()
+ article_kwargs['metadata']['slug'] = '/foo'
+ article_kwargs['settings'] = settings
+ article = Article(**article_kwargs)
+ self.assertFalse(article._has_valid_save_as())
+
+ def test_valid_save_as_passes_valid(self):
+ settings = get_settings()
+ article_kwargs = self._copy_page_kwargs()
+ article_kwargs['metadata']['slug'] = 'foo'
+ article_kwargs['settings'] = settings
+ article = Article(**article_kwargs)
+ self.assertTrue(article._has_valid_save_as())
+
+
+class TestStatic(LoggedTestCase):
+
+ def setUp(self):
+ super().setUp()
+ self.settings = get_settings(
+ STATIC_SAVE_AS='{path}',
+ STATIC_URL='{path}',
+ PAGE_SAVE_AS=os.path.join('outpages', '{slug}.html'),
+ PAGE_URL='outpages/{slug}.html')
+ self.context = get_context(self.settings)
+
+ self.static = Static(content=None, metadata={}, settings=self.settings,
+ source_path=posix_join('dir', 'foo.jpg'),
+ context=self.context)
+
+ self.context['static_content'][self.static.source_path] = self.static
+
+ def tearDown(self):
+ pass
+
+ def test_attach_to_same_dir(self):
+ """attach_to() overrides a static file's save_as and url.
+ """
+ page = Page(
+ content="fake page",
+ metadata={'title': 'fakepage'},
+ settings=self.settings,
+ source_path=os.path.join('dir', 'fakepage.md'))
+ self.static.attach_to(page)
+
+ expected_save_as = os.path.join('outpages', 'foo.jpg')
+ self.assertEqual(self.static.save_as, expected_save_as)
+ self.assertEqual(self.static.url, path_to_url(expected_save_as))
+
+ def test_attach_to_parent_dir(self):
+ """attach_to() preserves dirs inside the linking document dir.
+ """
+ page = Page(content="fake page", metadata={'title': 'fakepage'},
+ settings=self.settings, source_path='fakepage.md')
+ self.static.attach_to(page)
+
+ expected_save_as = os.path.join('outpages', 'dir', 'foo.jpg')
+ self.assertEqual(self.static.save_as, expected_save_as)
+ self.assertEqual(self.static.url, path_to_url(expected_save_as))
+
+ def test_attach_to_other_dir(self):
+ """attach_to() ignores dirs outside the linking document dir.
+ """
+ page = Page(content="fake page",
+ metadata={'title': 'fakepage'}, settings=self.settings,
+ source_path=os.path.join('dir', 'otherdir', 'fakepage.md'))
+ self.static.attach_to(page)
+
+ expected_save_as = os.path.join('outpages', 'foo.jpg')
+ self.assertEqual(self.static.save_as, expected_save_as)
+ self.assertEqual(self.static.url, path_to_url(expected_save_as))
+
+ def test_attach_to_ignores_subsequent_calls(self):
+ """attach_to() does nothing when called a second time.
+ """
+ page = Page(content="fake page",
+ metadata={'title': 'fakepage'}, settings=self.settings,
+ source_path=os.path.join('dir', 'fakepage.md'))
+
+ self.static.attach_to(page)
+
+ otherdir_settings = self.settings.copy()
+ otherdir_settings.update(dict(
+ PAGE_SAVE_AS=os.path.join('otherpages', '{slug}.html'),
+ PAGE_URL='otherpages/{slug}.html'))
+ otherdir_page = Page(
+ content="other page",
+ metadata={'title': 'otherpage'},
+ settings=otherdir_settings,
+ source_path=os.path.join('dir', 'otherpage.md'))
+
+ self.static.attach_to(otherdir_page)
+
+ otherdir_save_as = os.path.join('otherpages', 'foo.jpg')
+ self.assertNotEqual(self.static.save_as, otherdir_save_as)
+ self.assertNotEqual(self.static.url, path_to_url(otherdir_save_as))
+
+ def test_attach_to_does_nothing_after_save_as_referenced(self):
+ """attach_to() does nothing if the save_as was already referenced.
+ (For example, by a {static} link an a document processed earlier.)
+ """
+ original_save_as = self.static.save_as
+
+ page = Page(
+ content="fake page",
+ metadata={'title': 'fakepage'},
+ settings=self.settings,
+ source_path=os.path.join('dir', 'fakepage.md'))
+ self.static.attach_to(page)
+
+ self.assertEqual(self.static.save_as, original_save_as)
+ self.assertEqual(self.static.url, path_to_url(original_save_as))
+
+ def test_attach_to_does_nothing_after_url_referenced(self):
+ """attach_to() does nothing if the url was already referenced.
+ (For example, by a {static} link an a document processed earlier.)
+ """
+ original_url = self.static.url
+
+ page = Page(
+ content="fake page",
+ metadata={'title': 'fakepage'},
+ settings=self.settings,
+ source_path=os.path.join('dir', 'fakepage.md'))
+ self.static.attach_to(page)
+
+ self.assertEqual(self.static.save_as, self.static.source_path)
+ self.assertEqual(self.static.url, original_url)
+
+ def test_attach_to_does_not_override_an_override(self):
+ """attach_to() does not override paths that were overridden elsewhere.
+ (For example, by the user with EXTRA_PATH_METADATA)
+ """
+ customstatic = Static(
+ content=None,
+ metadata=dict(save_as='customfoo.jpg', url='customfoo.jpg'),
+ settings=self.settings,
+ source_path=os.path.join('dir', 'foo.jpg'),
+ context=self.settings.copy())
+
+ page = Page(
+ content="fake page",
+ metadata={'title': 'fakepage'}, settings=self.settings,
+ source_path=os.path.join('dir', 'fakepage.md'))
+
+ customstatic.attach_to(page)
+
+ self.assertEqual(customstatic.save_as, 'customfoo.jpg')
+ self.assertEqual(customstatic.url, 'customfoo.jpg')
+
+ def test_attach_link_syntax(self):
+ """{attach} link syntax triggers output path override & url replacement.
+ """
+ html = 'link '
+ page = Page(
+ content=html,
+ metadata={'title': 'fakepage'},
+ settings=self.settings,
+ source_path=os.path.join('dir', 'otherdir', 'fakepage.md'),
+ context=self.context)
+ content = page.get_content('')
+
+ self.assertNotEqual(
+ content, html,
+ "{attach} link syntax did not trigger URL replacement.")
+
+ expected_save_as = os.path.join('outpages', 'foo.jpg')
+ self.assertEqual(self.static.save_as, expected_save_as)
+ self.assertEqual(self.static.url, path_to_url(expected_save_as))
+
+ def test_tag_link_syntax(self):
+ "{tag} link syntax triggers url replacement."
+
+ html = 'link '
+ page = Page(
+ content=html,
+ metadata={'title': 'fakepage'},
+ settings=self.settings,
+ source_path=os.path.join('dir', 'otherdir', 'fakepage.md'),
+ context=self.context)
+ content = page.get_content('')
+
+ self.assertNotEqual(content, html)
+
+ def test_category_link_syntax(self):
+ "{category} link syntax triggers url replacement."
+
+ html = 'link '
+ page = Page(
+ content=html,
+ metadata={'title': 'fakepage'},
+ settings=self.settings,
+ source_path=os.path.join('dir', 'otherdir', 'fakepage.md'),
+ context=self.context)
+ content = page.get_content('')
+
+ self.assertNotEqual(content, html)
+
+ def test_author_link_syntax(self):
+ "{author} link syntax triggers url replacement."
+
+ html = 'link '
+ page = Page(
+ content=html,
+ metadata={'title': 'fakepage'},
+ settings=self.settings,
+ source_path=os.path.join('dir', 'otherdir', 'fakepage.md'),
+ context=self.context)
+ content = page.get_content('')
+
+ self.assertNotEqual(content, html)
+
+ def test_index_link_syntax(self):
+ "{index} link syntax triggers url replacement."
+
+ html = 'link '
+ page = Page(
+ content=html,
+ metadata={'title': 'fakepage'},
+ settings=self.settings,
+ source_path=os.path.join('dir', 'otherdir', 'fakepage.md'),
+ context=self.context)
+ content = page.get_content('')
+
+ self.assertNotEqual(content, html)
+
+ expected_html = ('link ')
+ self.assertEqual(content, expected_html)
+
+ def test_unknown_link_syntax(self):
+ "{unknown} link syntax should trigger warning."
+
+ html = 'link '
+ page = Page(content=html,
+ metadata={'title': 'fakepage'}, settings=self.settings,
+ source_path=os.path.join('dir', 'otherdir', 'fakepage.md'),
+ context=self.context)
+ content = page.get_content('')
+
+ self.assertEqual(content, html)
+ self.assertLogCountEqual(
+ count=1,
+ msg="Replacement Indicator 'unknown' not recognized, "
+ "skipping replacement",
+ level=logging.WARNING)
+
+ def test_link_to_unknown_file(self):
+ "{filename} link to unknown file should trigger warning."
+
+ html = 'link '
+ page = Page(content=html,
+ metadata={'title': 'fakepage'}, settings=self.settings,
+ source_path=os.path.join('dir', 'otherdir', 'fakepage.md'),
+ context=self.context)
+ content = page.get_content('')
+
+ self.assertEqual(content, html)
+ self.assertLogCountEqual(
+ count=1,
+ msg="Unable to find 'foo', skipping url replacement.",
+ level=logging.WARNING)
+
+ def test_index_link_syntax_with_spaces(self):
+ """{index} link syntax triggers url replacement
+ with spaces around the equal sign."""
+
+ html = 'link '
+ page = Page(
+ content=html,
+ metadata={'title': 'fakepage'},
+ settings=self.settings,
+ source_path=os.path.join('dir', 'otherdir', 'fakepage.md'),
+ context=self.context)
+ content = page.get_content('')
+
+ self.assertNotEqual(content, html)
+
+ expected_html = ('link ')
+ self.assertEqual(content, expected_html)
+
+ def test_not_save_as_draft(self):
+ """Static.save_as is not affected by draft status."""
+
+ static = Static(
+ content=None,
+ metadata=dict(status='draft',),
+ settings=self.settings,
+ source_path=os.path.join('dir', 'foo.jpg'),
+ context=self.settings.copy())
+
+ expected_save_as = posixize_path(os.path.join('dir', 'foo.jpg'))
+ self.assertEqual(static.status, 'draft')
+ self.assertEqual(static.save_as, expected_save_as)
+ self.assertEqual(static.url, path_to_url(expected_save_as))
diff --git a/pelican/tests/test_generators.py b/pelican/tests/test_generators.py
new file mode 100644
index 00000000..169765ac
--- /dev/null
+++ b/pelican/tests/test_generators.py
@@ -0,0 +1,1227 @@
+import locale
+import os
+import sys
+from shutil import copy, rmtree
+from tempfile import mkdtemp
+from unittest.mock import MagicMock
+
+from pelican.generators import (ArticlesGenerator, Generator, PagesGenerator,
+ PelicanTemplateNotFound, StaticGenerator,
+ TemplatePagesGenerator)
+from pelican.tests.support import (can_symlink, get_context, get_settings,
+ unittest)
+from pelican.writers import Writer
+
+
+CUR_DIR = os.path.dirname(__file__)
+CONTENT_DIR = os.path.join(CUR_DIR, 'content')
+
+
+class TestGenerator(unittest.TestCase):
+ def setUp(self):
+ self.old_locale = locale.setlocale(locale.LC_ALL)
+ locale.setlocale(locale.LC_ALL, 'C')
+ self.settings = get_settings()
+ self.settings['READERS'] = {'asc': None}
+ self.generator = Generator(self.settings.copy(), self.settings,
+ CUR_DIR, self.settings['THEME'], None)
+
+ def tearDown(self):
+ locale.setlocale(locale.LC_ALL, self.old_locale)
+
+ def test_include_path(self):
+ self.settings['IGNORE_FILES'] = {'ignored1.rst', 'ignored2.rst'}
+
+ filename = os.path.join(CUR_DIR, 'content', 'article.rst')
+ include_path = self.generator._include_path
+ self.assertTrue(include_path(filename))
+ self.assertTrue(include_path(filename, extensions=('rst',)))
+ self.assertFalse(include_path(filename, extensions=('md',)))
+
+ ignored_file = os.path.join(CUR_DIR, 'content', 'ignored1.rst')
+ self.assertFalse(include_path(ignored_file))
+
+ def test_get_files_exclude(self):
+ """Test that Generator.get_files() properly excludes directories.
+ """
+ # We use our own Generator so we can give it our own content path
+ generator = Generator(
+ context=self.settings.copy(),
+ settings=self.settings,
+ path=os.path.join(CUR_DIR, 'nested_content'),
+ theme=self.settings['THEME'], output_path=None)
+
+ filepaths = generator.get_files(paths=['maindir'])
+ found_files = {os.path.basename(f) for f in filepaths}
+ expected_files = {'maindir.md', 'subdir.md'}
+ self.assertFalse(
+ expected_files - found_files,
+ "get_files() failed to find one or more files")
+
+ # Test string as `paths` argument rather than list
+ filepaths = generator.get_files(paths='maindir')
+ found_files = {os.path.basename(f) for f in filepaths}
+ expected_files = {'maindir.md', 'subdir.md'}
+ self.assertFalse(
+ expected_files - found_files,
+ "get_files() failed to find one or more files")
+
+ filepaths = generator.get_files(paths=[''], exclude=['maindir'])
+ found_files = {os.path.basename(f) for f in filepaths}
+ self.assertNotIn(
+ 'maindir.md', found_files,
+ "get_files() failed to exclude a top-level directory")
+ self.assertNotIn(
+ 'subdir.md', found_files,
+ "get_files() failed to exclude a subdir of an excluded directory")
+
+ filepaths = generator.get_files(
+ paths=[''],
+ exclude=[os.path.join('maindir', 'subdir')])
+ found_files = {os.path.basename(f) for f in filepaths}
+ self.assertNotIn(
+ 'subdir.md', found_files,
+ "get_files() failed to exclude a subdirectory")
+
+ filepaths = generator.get_files(paths=[''], exclude=['subdir'])
+ found_files = {os.path.basename(f) for f in filepaths}
+ self.assertIn(
+ 'subdir.md', found_files,
+ "get_files() excluded a subdirectory by name, ignoring its path")
+
+ def test_custom_jinja_environment(self):
+ """
+ Test that setting the JINJA_ENVIRONMENT
+ properly gets set from the settings config
+ """
+ settings = get_settings()
+ comment_start_string = 'abc'
+ comment_end_string = '/abc'
+ settings['JINJA_ENVIRONMENT'] = {
+ 'comment_start_string': comment_start_string,
+ 'comment_end_string': comment_end_string
+ }
+ generator = Generator(settings.copy(), settings,
+ CUR_DIR, settings['THEME'], None)
+ self.assertEqual(comment_start_string,
+ generator.env.comment_start_string)
+ self.assertEqual(comment_end_string,
+ generator.env.comment_end_string)
+
+ def test_theme_overrides(self):
+ """
+ Test that the THEME_TEMPLATES_OVERRIDES configuration setting is
+ utilized correctly in the Generator.
+ """
+ override_dirs = (os.path.join(CUR_DIR, 'theme_overrides', 'level1'),
+ os.path.join(CUR_DIR, 'theme_overrides', 'level2'))
+ self.settings['THEME_TEMPLATES_OVERRIDES'] = override_dirs
+ generator = Generator(
+ context=self.settings.copy(),
+ settings=self.settings,
+ path=CUR_DIR,
+ theme=self.settings['THEME'],
+ output_path=None)
+
+ filename = generator.get_template('article').filename
+ self.assertEqual(override_dirs[0], os.path.dirname(filename))
+ self.assertEqual('article.html', os.path.basename(filename))
+
+ filename = generator.get_template('authors').filename
+ self.assertEqual(override_dirs[1], os.path.dirname(filename))
+ self.assertEqual('authors.html', os.path.basename(filename))
+
+ filename = generator.get_template('taglist').filename
+ self.assertEqual(os.path.join(self.settings['THEME'], 'templates'),
+ os.path.dirname(filename))
+ self.assertNotIn(os.path.dirname(filename), override_dirs)
+ self.assertEqual('taglist.html', os.path.basename(filename))
+
+ def test_simple_prefix(self):
+ """
+ Test `!simple` theme prefix.
+ """
+ filename = self.generator.get_template('!simple/authors').filename
+ expected_path = os.path.join(
+ os.path.dirname(CUR_DIR), 'themes', 'simple', 'templates')
+ self.assertEqual(expected_path, os.path.dirname(filename))
+ self.assertEqual('authors.html', os.path.basename(filename))
+
+ def test_theme_prefix(self):
+ """
+ Test `!theme` theme prefix.
+ """
+ filename = self.generator.get_template('!theme/authors').filename
+ expected_path = os.path.join(
+ os.path.dirname(CUR_DIR), 'themes', 'notmyidea', 'templates')
+ self.assertEqual(expected_path, os.path.dirname(filename))
+ self.assertEqual('authors.html', os.path.basename(filename))
+
+ def test_bad_prefix(self):
+ """
+ Test unknown/bad theme prefix throws exception.
+ """
+ self.assertRaises(PelicanTemplateNotFound, self.generator.get_template,
+ '!UNKNOWN/authors')
+
+
+class TestArticlesGenerator(unittest.TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ settings = get_settings()
+ settings['DEFAULT_CATEGORY'] = 'Default'
+ settings['DEFAULT_DATE'] = (1970, 1, 1)
+ settings['READERS'] = {'asc': None}
+ settings['CACHE_CONTENT'] = False
+ context = get_context(settings)
+
+ cls.generator = ArticlesGenerator(
+ context=context, settings=settings,
+ path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
+ cls.generator.generate_context()
+ cls.articles = cls.distill_articles(cls.generator.articles)
+
+ def setUp(self):
+ self.temp_cache = mkdtemp(prefix='pelican_cache.')
+
+ def tearDown(self):
+ rmtree(self.temp_cache)
+
+ @staticmethod
+ def distill_articles(articles):
+ return [[article.title, article.status, article.category.name,
+ article.template] for article in articles]
+
+ def test_generate_feeds(self):
+ settings = get_settings()
+ settings['CACHE_PATH'] = self.temp_cache
+ generator = ArticlesGenerator(
+ context=settings, settings=settings,
+ path=None, theme=settings['THEME'], output_path=None)
+ writer = MagicMock()
+ generator.generate_feeds(writer)
+ writer.write_feed.assert_called_with([], settings,
+ 'feeds/all.atom.xml',
+ 'feeds/all.atom.xml')
+
+ generator = ArticlesGenerator(
+ context=settings, settings=get_settings(FEED_ALL_ATOM=None),
+ path=None, theme=settings['THEME'], output_path=None)
+ writer = MagicMock()
+ generator.generate_feeds(writer)
+ self.assertFalse(writer.write_feed.called)
+
+ def test_generate_feeds_override_url(self):
+ settings = get_settings()
+ settings['CACHE_PATH'] = self.temp_cache
+ settings['FEED_ALL_ATOM_URL'] = 'feeds/atom/all/'
+ generator = ArticlesGenerator(
+ context=settings, settings=settings,
+ path=None, theme=settings['THEME'], output_path=None)
+ writer = MagicMock()
+ generator.generate_feeds(writer)
+ writer.write_feed.assert_called_with([], settings,
+ 'feeds/all.atom.xml',
+ 'feeds/atom/all/')
+
+ def test_generate_context(self):
+ articles_expected = [
+ ['Article title', 'published', 'Default', 'article'],
+ ['Article with markdown and summary metadata multi', 'published',
+ 'Default', 'article'],
+ ['Article with markdown and nested summary metadata', 'published',
+ 'Default', 'article'],
+ ['Article with markdown and summary metadata single', 'published',
+ 'Default', 'article'],
+ ['Article with markdown containing footnotes', 'published',
+ 'Default', 'article'],
+ ['Article with template', 'published', 'Default', 'custom'],
+ ['Metadata tags as list!', 'published', 'Default', 'article'],
+ ['Rst with filename metadata', 'published', 'yeah', 'article'],
+ ['One -, two --, three --- dashes!', 'published', 'Default',
+ 'article'],
+ ['One -, two --, three --- dashes!', 'published', 'Default',
+ 'article'],
+ ['Test Markdown extensions', 'published', 'Default', 'article'],
+ ['Test markdown File', 'published', 'test', 'article'],
+ ['Test md File', 'published', 'test', 'article'],
+ ['Test mdown File', 'published', 'test', 'article'],
+ ['Test metadata duplicates', 'published', 'test', 'article'],
+ ['Test mkd File', 'published', 'test', 'article'],
+ ['This is a super article !', 'published', 'Yeah', 'article'],
+ ['This is a super article !', 'published', 'Yeah', 'article'],
+ ['Article with Nonconformant HTML meta tags', 'published',
+ 'Default', 'article'],
+ ['This is a super article !', 'published', 'yeah', 'article'],
+ ['This is a super article !', 'published', 'yeah', 'article'],
+ ['This is a super article !', 'published', 'yeah', 'article'],
+ ['This is a super article !', 'published', 'yeah', 'article'],
+ ['This is a super article !', 'published', 'yeah', 'article'],
+ ['This is a super article !', 'published', 'yeah', 'article'],
+ ['This is a super article !', 'published', 'yeah', 'article'],
+ ['This is a super article !', 'published', 'yeah', 'article'],
+ ['This is a super article !', 'published', 'Default', 'article'],
+ ['Article with an inline SVG', 'published', 'Default', 'article'],
+ ['This is an article with category !', 'published', 'yeah',
+ 'article'],
+ ['This is an article with multiple authors!', 'published',
+ 'Default', 'article'],
+ ['This is an article with multiple authors!', 'published',
+ 'Default', 'article'],
+ ['This is an article with multiple authors in list format!',
+ 'published', 'Default', 'article'],
+ ['This is an article with multiple authors in lastname, '
+ 'firstname format!', 'published', 'Default', 'article'],
+ ['This is an article without category !', 'published', 'Default',
+ 'article'],
+ ['This is an article without category !', 'published',
+ 'TestCategory', 'article'],
+ ['An Article With Code Block To Test Typogrify Ignore',
+ 'published', 'Default', 'article'],
+ ['マックOS X 10.8でパイソンとVirtualenvをインストールと設定',
+ 'published', '指導書', 'article'],
+ ]
+ self.assertEqual(sorted(articles_expected), sorted(self.articles))
+
+ def test_generate_categories(self):
+ # test for name
+ # categories are grouped by slug; if two categories have the same slug
+ # but different names they will be grouped together, the first one in
+ # terms of process order will define the name for that category
+ categories = [cat.name for cat, _ in self.generator.categories]
+ categories_alternatives = (
+ sorted(['Default', 'TestCategory', 'Yeah', 'test', '指導書']),
+ sorted(['Default', 'TestCategory', 'yeah', 'test', '指導書']),
+ )
+ self.assertIn(sorted(categories), categories_alternatives)
+ # test for slug
+ categories = [cat.slug for cat, _ in self.generator.categories]
+ categories_expected = ['default', 'testcategory', 'yeah', 'test',
+ 'zhi-dao-shu']
+ self.assertEqual(sorted(categories), sorted(categories_expected))
+
+ def test_do_not_use_folder_as_category(self):
+ settings = get_settings()
+ settings['DEFAULT_CATEGORY'] = 'Default'
+ settings['DEFAULT_DATE'] = (1970, 1, 1)
+ settings['USE_FOLDER_AS_CATEGORY'] = False
+ settings['CACHE_PATH'] = self.temp_cache
+ settings['READERS'] = {'asc': None}
+ context = get_context(settings)
+ generator = ArticlesGenerator(
+ context=context, settings=settings,
+ path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
+ generator.generate_context()
+ # test for name
+ # categories are grouped by slug; if two categories have the same slug
+ # but different names they will be grouped together, the first one in
+ # terms of process order will define the name for that category
+ categories = [cat.name for cat, _ in generator.categories]
+ categories_alternatives = (
+ sorted(['Default', 'Yeah', 'test', '指導書']),
+ sorted(['Default', 'yeah', 'test', '指導書']),
+ )
+ self.assertIn(sorted(categories), categories_alternatives)
+ # test for slug
+ categories = [cat.slug for cat, _ in generator.categories]
+ categories_expected = ['default', 'yeah', 'test', 'zhi-dao-shu']
+ self.assertEqual(sorted(categories), sorted(categories_expected))
+
+ def test_direct_templates_save_as_url_default(self):
+
+ settings = get_settings()
+ settings['CACHE_PATH'] = self.temp_cache
+ context = get_context(settings)
+ generator = ArticlesGenerator(
+ context=context, settings=settings,
+ path=None, theme=settings['THEME'], output_path=None)
+ write = MagicMock()
+ generator.generate_direct_templates(write)
+ write.assert_called_with("archives.html",
+ generator.get_template("archives"), context,
+ articles=generator.articles,
+ dates=generator.dates, blog=True,
+ template_name='archives',
+ page_name='archives', url="archives.html")
+
+ def test_direct_templates_save_as_url_modified(self):
+
+ settings = get_settings()
+ settings['DIRECT_TEMPLATES'] = ['archives']
+ settings['ARCHIVES_SAVE_AS'] = 'archives/index.html'
+ settings['ARCHIVES_URL'] = 'archives/'
+ settings['CACHE_PATH'] = self.temp_cache
+ generator = ArticlesGenerator(
+ context=settings, settings=settings,
+ path=None, theme=settings['THEME'], output_path=None)
+ write = MagicMock()
+ generator.generate_direct_templates(write)
+ write.assert_called_with("archives/index.html",
+ generator.get_template("archives"), settings,
+ articles=generator.articles,
+ dates=generator.dates, blog=True,
+ template_name='archives',
+ page_name='archives/index',
+ url="archives/")
+
+ def test_direct_templates_save_as_false(self):
+
+ settings = get_settings()
+ settings['DIRECT_TEMPLATES'] = ['archives']
+ settings['ARCHIVES_SAVE_AS'] = False
+ settings['CACHE_PATH'] = self.temp_cache
+ generator = ArticlesGenerator(
+ context=settings, settings=settings,
+ path=None, theme=settings['THEME'], output_path=None)
+ write = MagicMock()
+ generator.generate_direct_templates(write)
+ self.assertEqual(write.call_count, 0)
+
+ def test_per_article_template(self):
+ """
+ Custom template articles get the field but standard/unset are None
+ """
+ custom_template = ['Article with template', 'published', 'Default',
+ 'custom']
+ standard_template = ['This is a super article !', 'published', 'Yeah',
+ 'article']
+ self.assertIn(custom_template, self.articles)
+ self.assertIn(standard_template, self.articles)
+
+ def test_period_in_timeperiod_archive(self):
+ """
+ Test that the context of a generated period_archive is passed
+ 'period' : a tuple of year, month, day according to the time period
+ """
+ old_locale = locale.setlocale(locale.LC_ALL)
+ locale.setlocale(locale.LC_ALL, 'C')
+ settings = get_settings()
+
+ settings['YEAR_ARCHIVE_SAVE_AS'] = 'posts/{date:%Y}/index.html'
+ settings['YEAR_ARCHIVE_URL'] = 'posts/{date:%Y}/'
+ settings['CACHE_PATH'] = self.temp_cache
+ context = get_context(settings)
+ generator = ArticlesGenerator(
+ context=context, settings=settings,
+ path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
+ generator.generate_context()
+ write = MagicMock()
+ generator.generate_period_archives(write)
+ dates = [d for d in generator.dates if d.date.year == 1970]
+ articles = [d for d in generator.articles if d.date.year == 1970]
+ self.assertEqual(len(dates), 1)
+ # among other things it must have at least been called with this
+ context["period"] = (1970,)
+ write.assert_called_with("posts/1970/index.html",
+ generator.get_template("period_archives"),
+ context, blog=True, articles=articles,
+ dates=dates, template_name='period_archives',
+ url="posts/1970/",
+ all_articles=generator.articles)
+
+ settings['MONTH_ARCHIVE_SAVE_AS'] = \
+ 'posts/{date:%Y}/{date:%b}/index.html'
+ settings['MONTH_ARCHIVE_URL'] = \
+ 'posts/{date:%Y}/{date:%b}/'
+ context = get_context(settings)
+ generator = ArticlesGenerator(
+ context=context, settings=settings,
+ path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
+ generator.generate_context()
+ write = MagicMock()
+ generator.generate_period_archives(write)
+ dates = [d for d in generator.dates
+ if d.date.year == 1970 and d.date.month == 1]
+ articles = [d for d in generator.articles
+ if d.date.year == 1970 and d.date.month == 1]
+ self.assertEqual(len(dates), 1)
+ context["period"] = (1970, "January")
+ # among other things it must have at least been called with this
+ write.assert_called_with("posts/1970/Jan/index.html",
+ generator.get_template("period_archives"),
+ context, blog=True, articles=articles,
+ dates=dates, template_name='period_archives',
+ url="posts/1970/Jan/",
+ all_articles=generator.articles)
+
+ settings['DAY_ARCHIVE_SAVE_AS'] = \
+ 'posts/{date:%Y}/{date:%b}/{date:%d}/index.html'
+ settings['DAY_ARCHIVE_URL'] = \
+ 'posts/{date:%Y}/{date:%b}/{date:%d}/'
+ context = get_context(settings)
+ generator = ArticlesGenerator(
+ context=context, settings=settings,
+ path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
+ generator.generate_context()
+ write = MagicMock()
+ generator.generate_period_archives(write)
+ dates = [
+ d for d in generator.dates if
+ d.date.year == 1970 and
+ d.date.month == 1 and
+ d.date.day == 1
+ ]
+ articles = [
+ d for d in generator.articles if
+ d.date.year == 1970 and
+ d.date.month == 1 and
+ d.date.day == 1
+ ]
+ self.assertEqual(len(dates), 1)
+ context["period"] = (1970, "January", 1)
+ # among other things it must have at least been called with this
+ write.assert_called_with("posts/1970/Jan/01/index.html",
+ generator.get_template("period_archives"),
+ context, blog=True, articles=articles,
+ dates=dates, template_name='period_archives',
+ url="posts/1970/Jan/01/",
+ all_articles=generator.articles)
+ locale.setlocale(locale.LC_ALL, old_locale)
+
+ def test_nonexistent_template(self):
+ """Attempt to load a non-existent template"""
+ settings = get_settings()
+ context = get_context(settings)
+ generator = ArticlesGenerator(
+ context=context, settings=settings,
+ path=None, theme=settings['THEME'], output_path=None)
+ self.assertRaises(Exception, generator.get_template, "not_a_template")
+
+ def test_generate_authors(self):
+ """Check authors generation."""
+ authors = [author.name for author, _ in self.generator.authors]
+ authors_expected = sorted(
+ ['Alexis Métaireau', 'Author, First', 'Author, Second',
+ 'First Author', 'Second Author'])
+ self.assertEqual(sorted(authors), authors_expected)
+ # test for slug
+ authors = [author.slug for author, _ in self.generator.authors]
+ authors_expected = ['alexis-metaireau', 'author-first',
+ 'author-second', 'first-author', 'second-author']
+ self.assertEqual(sorted(authors), sorted(authors_expected))
+
+ def test_standard_metadata_in_default_metadata(self):
+ settings = get_settings()
+ settings['CACHE_CONTENT'] = False
+ settings['DEFAULT_CATEGORY'] = 'Default'
+ settings['DEFAULT_DATE'] = (1970, 1, 1)
+ settings['DEFAULT_METADATA'] = (('author', 'Blogger'),
+ # category will be ignored in favor of
+ # DEFAULT_CATEGORY
+ ('category', 'Random'),
+ ('tags', 'general, untagged'))
+ context = get_context(settings)
+ generator = ArticlesGenerator(
+ context=context, settings=settings,
+ path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
+ generator.generate_context()
+
+ authors = sorted([author.name for author, _ in generator.authors])
+ authors_expected = sorted(['Alexis Métaireau', 'Blogger',
+ 'Author, First', 'Author, Second',
+ 'First Author', 'Second Author'])
+ self.assertEqual(authors, authors_expected)
+
+ categories = sorted([category.name
+ for category, _ in generator.categories])
+ categories_expected = [
+ sorted(['Default', 'TestCategory', 'yeah', 'test', '指導書']),
+ sorted(['Default', 'TestCategory', 'Yeah', 'test', '指導書'])]
+ self.assertIn(categories, categories_expected)
+
+ tags = sorted([tag.name for tag in generator.tags])
+ tags_expected = sorted(['bar', 'foo', 'foobar', 'general', 'untagged',
+ 'パイソン', 'マック'])
+ self.assertEqual(tags, tags_expected)
+
+ def test_article_order_by(self):
+ settings = get_settings()
+ settings['DEFAULT_CATEGORY'] = 'Default'
+ settings['DEFAULT_DATE'] = (1970, 1, 1)
+ settings['ARTICLE_ORDER_BY'] = 'title'
+ context = get_context(settings)
+
+ generator = ArticlesGenerator(
+ context=context, settings=settings,
+ path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
+ generator.generate_context()
+
+ expected = [
+ 'An Article With Code Block To Test Typogrify Ignore',
+ 'Article title',
+ 'Article with Nonconformant HTML meta tags',
+ 'Article with an inline SVG',
+ 'Article with markdown and nested summary metadata',
+ 'Article with markdown and summary metadata multi',
+ 'Article with markdown and summary metadata single',
+ 'Article with markdown containing footnotes',
+ 'Article with template',
+ 'Metadata tags as list!',
+ 'One -, two --, three --- dashes!',
+ 'One -, two --, three --- dashes!',
+ 'Rst with filename metadata',
+ 'Test Markdown extensions',
+ 'Test markdown File',
+ 'Test md File',
+ 'Test mdown File',
+ 'Test metadata duplicates',
+ 'Test mkd File',
+ 'This is a super article !',
+ 'This is a super article !',
+ 'This is a super article !',
+ 'This is a super article !',
+ 'This is a super article !',
+ 'This is a super article !',
+ 'This is a super article !',
+ 'This is a super article !',
+ 'This is a super article !',
+ 'This is a super article !',
+ 'This is a super article !',
+ 'This is an article with category !',
+ ('This is an article with multiple authors in lastname, '
+ 'firstname format!'),
+ 'This is an article with multiple authors in list format!',
+ 'This is an article with multiple authors!',
+ 'This is an article with multiple authors!',
+ 'This is an article without category !',
+ 'This is an article without category !',
+ 'マックOS X 10.8でパイソンとVirtualenvをインストールと設定']
+
+ articles = [article.title for article in generator.articles]
+ self.assertEqual(articles, expected)
+
+ # reversed title
+ settings = get_settings()
+ settings['DEFAULT_CATEGORY'] = 'Default'
+ settings['DEFAULT_DATE'] = (1970, 1, 1)
+ settings['ARTICLE_ORDER_BY'] = 'reversed-title'
+ context = get_context(settings)
+
+ generator = ArticlesGenerator(
+ context=context, settings=settings,
+ path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
+ generator.generate_context()
+
+ articles = [article.title for article in generator.articles]
+ self.assertEqual(articles, list(reversed(expected)))
+
+
+class TestPageGenerator(unittest.TestCase):
+ # Note: Every time you want to test for a new field; Make sure the test
+ # pages in "TestPages" have all the fields Add it to distilled in
+ # distill_pages Then update the assertEqual in test_generate_context
+ # to match expected
+
+ def setUp(self):
+ self.temp_cache = mkdtemp(prefix='pelican_cache.')
+
+ def tearDown(self):
+ rmtree(self.temp_cache)
+
+ def distill_pages(self, pages):
+ return [[page.title, page.status, page.template] for page in pages]
+
+ def test_generate_context(self):
+ settings = get_settings()
+ settings['CACHE_PATH'] = self.temp_cache
+ settings['PAGE_PATHS'] = ['TestPages'] # relative to CUR_DIR
+ settings['DEFAULT_DATE'] = (1970, 1, 1)
+ context = get_context(settings)
+
+ generator = PagesGenerator(
+ context=context, settings=settings,
+ path=CUR_DIR, theme=settings['THEME'], output_path=None)
+ generator.generate_context()
+ pages = self.distill_pages(generator.pages)
+ hidden_pages = self.distill_pages(generator.hidden_pages)
+ draft_pages = self.distill_pages(generator.draft_pages)
+
+ pages_expected = [
+ ['This is a test page', 'published', 'page'],
+ ['This is a markdown test page', 'published', 'page'],
+ ['This is a test page with a preset template', 'published',
+ 'custom'],
+ ['Page with a bunch of links', 'published', 'page'],
+ ['Page with static links', 'published', 'page'],
+ ['A Page (Test) for sorting', 'published', 'page'],
+ ]
+ hidden_pages_expected = [
+ ['This is a test hidden page', 'hidden', 'page'],
+ ['This is a markdown test hidden page', 'hidden', 'page'],
+ ['This is a test hidden page with a custom template', 'hidden',
+ 'custom'],
+ ]
+ draft_pages_expected = [
+ ['This is a test draft page', 'draft', 'page'],
+ ['This is a markdown test draft page', 'draft', 'page'],
+ ['This is a test draft page with a custom template', 'draft',
+ 'custom'],
+ ]
+
+ self.assertEqual(sorted(pages_expected), sorted(pages))
+ self.assertEqual(
+ sorted(pages_expected),
+ sorted(self.distill_pages(generator.context['pages'])))
+ self.assertEqual(sorted(hidden_pages_expected), sorted(hidden_pages))
+ self.assertEqual(sorted(draft_pages_expected), sorted(draft_pages))
+ self.assertEqual(
+ sorted(hidden_pages_expected),
+ sorted(self.distill_pages(generator.context['hidden_pages'])))
+ self.assertEqual(
+ sorted(draft_pages_expected),
+ sorted(self.distill_pages(generator.context['draft_pages'])))
+
+ def test_generate_sorted(self):
+ settings = get_settings()
+ settings['PAGE_PATHS'] = ['TestPages'] # relative to CUR_DIR
+ settings['CACHE_PATH'] = self.temp_cache
+ settings['DEFAULT_DATE'] = (1970, 1, 1)
+ context = get_context(settings)
+
+ # default sort (filename)
+ pages_expected_sorted_by_filename = [
+ ['This is a test page', 'published', 'page'],
+ ['This is a markdown test page', 'published', 'page'],
+ ['A Page (Test) for sorting', 'published', 'page'],
+ ['Page with a bunch of links', 'published', 'page'],
+ ['Page with static links', 'published', 'page'],
+ ['This is a test page with a preset template', 'published',
+ 'custom'],
+ ]
+ generator = PagesGenerator(
+ context=context, settings=settings,
+ path=CUR_DIR, theme=settings['THEME'], output_path=None)
+ generator.generate_context()
+ pages = self.distill_pages(generator.pages)
+ self.assertEqual(pages_expected_sorted_by_filename, pages)
+
+ # sort by title
+ pages_expected_sorted_by_title = [
+ ['A Page (Test) for sorting', 'published', 'page'],
+ ['Page with a bunch of links', 'published', 'page'],
+ ['Page with static links', 'published', 'page'],
+ ['This is a markdown test page', 'published', 'page'],
+ ['This is a test page', 'published', 'page'],
+ ['This is a test page with a preset template', 'published',
+ 'custom'],
+ ]
+ settings['PAGE_ORDER_BY'] = 'title'
+ context = get_context(settings)
+ generator = PagesGenerator(
+ context=context.copy(), settings=settings,
+ path=CUR_DIR, theme=settings['THEME'], output_path=None)
+ generator.generate_context()
+ pages = self.distill_pages(generator.pages)
+ self.assertEqual(pages_expected_sorted_by_title, pages)
+
+ # sort by title reversed
+ pages_expected_sorted_by_title = [
+ ['This is a test page with a preset template', 'published',
+ 'custom'],
+ ['This is a test page', 'published', 'page'],
+ ['This is a markdown test page', 'published', 'page'],
+ ['Page with static links', 'published', 'page'],
+ ['Page with a bunch of links', 'published', 'page'],
+ ['A Page (Test) for sorting', 'published', 'page'],
+ ]
+ settings['PAGE_ORDER_BY'] = 'reversed-title'
+ context = get_context(settings)
+ generator = PagesGenerator(
+ context=context, settings=settings,
+ path=CUR_DIR, theme=settings['THEME'], output_path=None)
+ generator.generate_context()
+ pages = self.distill_pages(generator.pages)
+ self.assertEqual(pages_expected_sorted_by_title, pages)
+
+ def test_tag_and_category_links_on_generated_pages(self):
+ """
+ Test to ensure links of the form {tag}tagname and {category}catname
+ are generated correctly on pages
+ """
+ settings = get_settings()
+ settings['PAGE_PATHS'] = ['TestPages'] # relative to CUR_DIR
+ settings['CACHE_PATH'] = self.temp_cache
+ settings['DEFAULT_DATE'] = (1970, 1, 1)
+ context = get_context(settings)
+
+ generator = PagesGenerator(
+ context=context, settings=settings,
+ path=CUR_DIR, theme=settings['THEME'], output_path=None)
+ generator.generate_context()
+ pages_by_title = {p.title: p for p in generator.pages}
+
+ test_content = pages_by_title['Page with a bunch of links'].content
+ self.assertIn('', test_content)
+ self.assertIn(' ', test_content)
+
+ def test_static_and_attach_links_on_generated_pages(self):
+ """
+ Test to ensure links of the form {static}filename and {attach}filename
+ are included in context['static_links']
+ """
+ settings = get_settings()
+ settings['PAGE_PATHS'] = ['TestPages/page_with_static_links.md']
+ settings['CACHE_PATH'] = self.temp_cache
+ settings['DEFAULT_DATE'] = (1970, 1, 1)
+ context = get_context(settings)
+
+ generator = PagesGenerator(
+ context=context, settings=settings,
+ path=CUR_DIR, theme=settings['THEME'], output_path=None)
+ generator.generate_context()
+
+ self.assertIn('pelican/tests/TestPages/image0.jpg',
+ context['static_links'])
+ self.assertIn('pelican/tests/TestPages/image1.jpg',
+ context['static_links'])
+
+
+class TestTemplatePagesGenerator(unittest.TestCase):
+
+ TEMPLATE_CONTENT = "foo: {{ foo }}"
+
+ def setUp(self):
+ self.temp_content = mkdtemp(prefix='pelicantests.')
+ self.temp_output = mkdtemp(prefix='pelicantests.')
+ self.old_locale = locale.setlocale(locale.LC_ALL)
+ locale.setlocale(locale.LC_ALL, 'C')
+
+ def tearDown(self):
+ rmtree(self.temp_content)
+ rmtree(self.temp_output)
+ locale.setlocale(locale.LC_ALL, self.old_locale)
+
+ def test_generate_output(self):
+
+ settings = get_settings()
+ settings['STATIC_PATHS'] = ['static']
+ settings['TEMPLATE_PAGES'] = {
+ 'template/source.html': 'generated/file.html'
+ }
+
+ generator = TemplatePagesGenerator(
+ context={'foo': 'bar'}, settings=settings,
+ path=self.temp_content, theme='', output_path=self.temp_output)
+
+ # create a dummy template file
+ template_dir = os.path.join(self.temp_content, 'template')
+ template_path = os.path.join(template_dir, 'source.html')
+ os.makedirs(template_dir)
+ with open(template_path, 'w') as template_file:
+ template_file.write(self.TEMPLATE_CONTENT)
+
+ writer = Writer(self.temp_output, settings=settings)
+ generator.generate_output(writer)
+
+ output_path = os.path.join(self.temp_output, 'generated', 'file.html')
+
+ # output file has been generated
+ self.assertTrue(os.path.exists(output_path))
+
+ # output content is correct
+ with open(output_path) as output_file:
+ self.assertEqual(output_file.read(), 'foo: bar')
+
+
+class TestStaticGenerator(unittest.TestCase):
+
+ def setUp(self):
+ self.content_path = os.path.join(CUR_DIR, 'mixed_content')
+ self.temp_content = mkdtemp(prefix='testcontent.')
+ self.temp_output = mkdtemp(prefix='testoutput.')
+ self.settings = get_settings()
+ self.settings['PATH'] = self.temp_content
+ self.settings['STATIC_PATHS'] = ["static"]
+ self.settings['OUTPUT_PATH'] = self.temp_output
+ os.mkdir(os.path.join(self.temp_content, "static"))
+ self.startfile = os.path.join(self.temp_content,
+ "static", "staticfile")
+ self.endfile = os.path.join(self.temp_output, "static", "staticfile")
+ self.generator = StaticGenerator(
+ context=get_context(),
+ settings=self.settings,
+ path=self.temp_content,
+ theme="",
+ output_path=self.temp_output,
+ )
+
+ def tearDown(self):
+ rmtree(self.temp_content)
+ rmtree(self.temp_output)
+
+ def set_ancient_mtime(self, path, timestamp=1):
+ os.utime(path, (timestamp, timestamp))
+
+ def test_theme_static_paths_dirs(self):
+ """Test that StaticGenerator properly copies also files mentioned in
+ TEMPLATE_STATIC_PATHS, not just directories."""
+ settings = get_settings(PATH=self.content_path)
+ context = get_context(settings, staticfiles=[])
+
+ StaticGenerator(
+ context=context, settings=settings,
+ path=settings['PATH'], output_path=self.temp_output,
+ theme=settings['THEME']).generate_output(None)
+
+ # The content of dirs listed in THEME_STATIC_PATHS (defaulting to
+ # "static") is put into the output
+ self.assertTrue(os.path.isdir(os.path.join(self.temp_output,
+ "theme/css/")))
+ self.assertTrue(os.path.isdir(os.path.join(self.temp_output,
+ "theme/fonts/")))
+
+ def test_theme_static_paths_files(self):
+ """Test that StaticGenerator properly copies also files mentioned in
+ TEMPLATE_STATIC_PATHS, not just directories."""
+ settings = get_settings(
+ PATH=self.content_path,
+ THEME_STATIC_PATHS=['static/css/fonts.css', 'static/fonts/'],)
+ context = get_context(settings, staticfiles=[])
+
+ StaticGenerator(
+ context=context, settings=settings,
+ path=settings['PATH'], output_path=self.temp_output,
+ theme=settings['THEME']).generate_output(None)
+
+ # Only the content of dirs and files listed in THEME_STATIC_PATHS are
+ # put into the output, not everything from static/
+ self.assertFalse(os.path.isdir(os.path.join(self.temp_output,
+ "theme/css/")))
+ self.assertFalse(os.path.isdir(os.path.join(self.temp_output,
+ "theme/fonts/")))
+
+ self.assertTrue(os.path.isfile(os.path.join(
+ self.temp_output, "theme/Yanone_Kaffeesatz_400.eot")))
+ self.assertTrue(os.path.isfile(os.path.join(
+ self.temp_output, "theme/Yanone_Kaffeesatz_400.svg")))
+ self.assertTrue(os.path.isfile(os.path.join(
+ self.temp_output, "theme/Yanone_Kaffeesatz_400.ttf")))
+ self.assertTrue(os.path.isfile(os.path.join(
+ self.temp_output, "theme/Yanone_Kaffeesatz_400.woff")))
+ self.assertTrue(os.path.isfile(os.path.join(
+ self.temp_output, "theme/Yanone_Kaffeesatz_400.woff2")))
+ self.assertTrue(os.path.isfile(os.path.join(self.temp_output,
+ "theme/font.css")))
+ self.assertTrue(os.path.isfile(os.path.join(self.temp_output,
+ "theme/fonts.css")))
+
+ def test_static_excludes(self):
+ """Test that StaticGenerator respects STATIC_EXCLUDES.
+ """
+ settings = get_settings(
+ STATIC_EXCLUDES=['subdir'],
+ PATH=self.content_path,
+ STATIC_PATHS=[''],)
+ context = get_context(settings)
+
+ StaticGenerator(
+ context=context, settings=settings,
+ path=settings['PATH'], output_path=self.temp_output,
+ theme=settings['THEME']).generate_context()
+
+ staticnames = [os.path.basename(c.source_path)
+ for c in context['staticfiles']]
+
+ self.assertNotIn(
+ 'subdir_fake_image.jpg', staticnames,
+ "StaticGenerator processed a file in a STATIC_EXCLUDES directory")
+ self.assertIn(
+ 'fake_image.jpg', staticnames,
+ "StaticGenerator skipped a file that it should have included")
+
+ def test_static_exclude_sources(self):
+ """Test that StaticGenerator respects STATIC_EXCLUDE_SOURCES.
+ """
+
+ settings = get_settings(
+ STATIC_EXCLUDE_SOURCES=True,
+ PATH=self.content_path,
+ PAGE_PATHS=[''],
+ STATIC_PATHS=[''],
+ CACHE_CONTENT=False,)
+ context = get_context(settings)
+
+ for generator_class in (PagesGenerator, StaticGenerator):
+ generator_class(
+ context=context, settings=settings,
+ path=settings['PATH'], output_path=self.temp_output,
+ theme=settings['THEME']).generate_context()
+
+ staticnames = [os.path.basename(c.source_path)
+ for c in context['staticfiles']]
+
+ self.assertFalse(
+ any(name.endswith(".md") for name in staticnames),
+ "STATIC_EXCLUDE_SOURCES=True failed to exclude a markdown file")
+
+ settings.update(STATIC_EXCLUDE_SOURCES=False)
+ context = get_context(settings)
+
+ for generator_class in (PagesGenerator, StaticGenerator):
+ generator_class(
+ context=context, settings=settings,
+ path=settings['PATH'], output_path=self.temp_output,
+ theme=settings['THEME']).generate_context()
+
+ staticnames = [os.path.basename(c.source_path)
+ for c in context['staticfiles']]
+
+ self.assertTrue(
+ any(name.endswith(".md") for name in staticnames),
+ "STATIC_EXCLUDE_SOURCES=False failed to include a markdown file")
+
+ def test_static_links(self):
+ """Test that StaticGenerator uses files in static_links
+ """
+ settings = get_settings(
+ STATIC_EXCLUDES=['subdir'],
+ PATH=self.content_path,
+ STATIC_PATHS=[],)
+ context = get_context(settings)
+ context['static_links'] |= {'short_page.md', 'subdir_fake_image.jpg'}
+
+ StaticGenerator(
+ context=context, settings=settings,
+ path=settings['PATH'], output_path=self.temp_output,
+ theme=settings['THEME']).generate_context()
+
+ staticfiles_names = [
+ os.path.basename(c.source_path) for c in context['staticfiles']]
+
+ static_content_names = [
+ os.path.basename(c) for c in context['static_content']]
+
+ self.assertIn(
+ 'short_page.md', staticfiles_names,
+ "StaticGenerator skipped a file that it should have included")
+ self.assertIn(
+ 'short_page.md', static_content_names,
+ "StaticGenerator skipped a file that it should have included")
+ self.assertIn(
+ 'subdir_fake_image.jpg', staticfiles_names,
+ "StaticGenerator skipped a file that it should have included")
+ self.assertIn(
+ 'subdir_fake_image.jpg', static_content_names,
+ "StaticGenerator skipped a file that it should have included")
+
+ def test_copy_one_file(self):
+ with open(self.startfile, "w") as f:
+ f.write("staticcontent")
+ self.generator.generate_context()
+ self.generator.generate_output(None)
+ with open(self.endfile) as f:
+ self.assertEqual(f.read(), "staticcontent")
+
+ def test_file_update_required_when_dest_does_not_exist(self):
+ staticfile = MagicMock()
+ staticfile.source_path = self.startfile
+ staticfile.save_as = self.endfile
+ with open(staticfile.source_path, "w") as f:
+ f.write("a")
+ update_required = self.generator._file_update_required(staticfile)
+ self.assertTrue(update_required)
+
+ def test_dest_and_source_mtimes_are_equal(self):
+ staticfile = MagicMock()
+ staticfile.source_path = self.startfile
+ staticfile.save_as = self.endfile
+ self.settings['STATIC_CHECK_IF_MODIFIED'] = True
+ with open(staticfile.source_path, "w") as f:
+ f.write("a")
+ os.mkdir(os.path.join(self.temp_output, "static"))
+ copy(staticfile.source_path, staticfile.save_as)
+ isnewer = self.generator._source_is_newer(staticfile)
+ self.assertFalse(isnewer)
+
+ def test_source_is_newer(self):
+ staticfile = MagicMock()
+ staticfile.source_path = self.startfile
+ staticfile.save_as = self.endfile
+ with open(staticfile.source_path, "w") as f:
+ f.write("a")
+ os.mkdir(os.path.join(self.temp_output, "static"))
+ copy(staticfile.source_path, staticfile.save_as)
+ self.set_ancient_mtime(staticfile.save_as)
+ isnewer = self.generator._source_is_newer(staticfile)
+ self.assertTrue(isnewer)
+
+ def test_skip_file_when_source_is_not_newer(self):
+ self.settings['STATIC_CHECK_IF_MODIFIED'] = True
+ with open(self.startfile, "w") as f:
+ f.write("staticcontent")
+ os.mkdir(os.path.join(self.temp_output, "static"))
+ with open(self.endfile, "w") as f:
+ f.write("staticcontent")
+ expected = os.path.getmtime(self.endfile)
+ self.set_ancient_mtime(self.startfile)
+ self.generator.generate_context()
+ self.generator.generate_output(None)
+ self.assertEqual(os.path.getmtime(self.endfile), expected)
+
+ def test_dont_link_by_default(self):
+ with open(self.startfile, "w") as f:
+ f.write("staticcontent")
+ self.generator.generate_context()
+ self.generator.generate_output(None)
+ self.assertFalse(os.path.samefile(self.startfile, self.endfile))
+
+ def test_output_file_is_linked_to_source(self):
+ self.settings['STATIC_CREATE_LINKS'] = True
+ with open(self.startfile, "w") as f:
+ f.write("staticcontent")
+ self.generator.generate_context()
+ self.generator.generate_output(None)
+ self.assertTrue(os.path.samefile(self.startfile, self.endfile))
+
+ def test_output_file_exists_and_is_newer(self):
+ self.settings['STATIC_CREATE_LINKS'] = True
+ with open(self.startfile, "w") as f:
+ f.write("staticcontent")
+ os.mkdir(os.path.join(self.temp_output, "static"))
+ with open(self.endfile, "w") as f:
+ f.write("othercontent")
+ self.generator.generate_context()
+ self.generator.generate_output(None)
+ self.assertTrue(os.path.samefile(self.startfile, self.endfile))
+
+ @unittest.skipUnless(can_symlink(), 'No symlink privilege')
+ def test_can_symlink_when_hardlink_not_possible(self):
+ self.settings['STATIC_CREATE_LINKS'] = True
+ with open(self.startfile, "w") as f:
+ f.write("staticcontent")
+ os.mkdir(os.path.join(self.temp_output, "static"))
+ self.generator.fallback_to_symlinks = True
+ self.generator.generate_context()
+ self.generator.generate_output(None)
+ self.assertTrue(os.path.islink(self.endfile))
+
+ @unittest.skipUnless(can_symlink(), 'No symlink privilege')
+ def test_existing_symlink_is_considered_up_to_date(self):
+ self.settings['STATIC_CREATE_LINKS'] = True
+ with open(self.startfile, "w") as f:
+ f.write("staticcontent")
+ os.mkdir(os.path.join(self.temp_output, "static"))
+ os.symlink(self.startfile, self.endfile)
+ staticfile = MagicMock()
+ staticfile.source_path = self.startfile
+ staticfile.save_as = self.endfile
+ requires_update = self.generator._file_update_required(staticfile)
+ self.assertFalse(requires_update)
+
+ @unittest.skipUnless(can_symlink(), 'No symlink privilege')
+ def test_invalid_symlink_is_overwritten(self):
+ self.settings['STATIC_CREATE_LINKS'] = True
+ with open(self.startfile, "w") as f:
+ f.write("staticcontent")
+ os.mkdir(os.path.join(self.temp_output, "static"))
+ os.symlink("invalid", self.endfile)
+ staticfile = MagicMock()
+ staticfile.source_path = self.startfile
+ staticfile.save_as = self.endfile
+ requires_update = self.generator._file_update_required(staticfile)
+ self.assertTrue(requires_update)
+ self.generator.fallback_to_symlinks = True
+ self.generator.generate_context()
+ self.generator.generate_output(None)
+ self.assertTrue(os.path.islink(self.endfile))
+
+ # os.path.realpath is broken on Windows before python3.8 for symlinks.
+ # This is a (ugly) workaround.
+ # see: https://bugs.python.org/issue9949
+ if os.name == 'nt' and sys.version_info < (3, 8):
+ def get_real_path(path):
+ return os.readlink(path) if os.path.islink(path) else path
+ else:
+ get_real_path = os.path.realpath
+
+ self.assertEqual(get_real_path(self.endfile),
+ get_real_path(self.startfile))
+
+ def test_delete_existing_file_before_mkdir(self):
+ with open(self.startfile, "w") as f:
+ f.write("staticcontent")
+ with open(os.path.join(self.temp_output, "static"), "w") as f:
+ f.write("This file should be a directory")
+ self.generator.generate_context()
+ self.generator.generate_output(None)
+ self.assertTrue(
+ os.path.isdir(os.path.join(self.temp_output, "static")))
+ self.assertTrue(os.path.isfile(self.endfile))
+
+
+class TestJinja2Environment(unittest.TestCase):
+
+ def setUp(self):
+ self.temp_content = mkdtemp(prefix='pelicantests.')
+ self.temp_output = mkdtemp(prefix='pelicantests.')
+ self.old_locale = locale.setlocale(locale.LC_ALL)
+ locale.setlocale(locale.LC_ALL, 'C')
+
+ def tearDown(self):
+ rmtree(self.temp_content)
+ rmtree(self.temp_output)
+ locale.setlocale(locale.LC_ALL, self.old_locale)
+
+ def _test_jinja2_helper(self, additional_settings, content, expected):
+ settings = get_settings()
+ settings['STATIC_PATHS'] = ['static']
+ settings['TEMPLATE_PAGES'] = {
+ 'template/source.html': 'generated/file.html'
+ }
+ settings.update(additional_settings)
+
+ generator = TemplatePagesGenerator(
+ context={'foo': 'foo', 'bar': 'bar'}, settings=settings,
+ path=self.temp_content, theme='', output_path=self.temp_output)
+
+ # create a dummy template file
+ template_dir = os.path.join(self.temp_content, 'template')
+ template_path = os.path.join(template_dir, 'source.html')
+ os.makedirs(template_dir)
+ with open(template_path, 'w') as template_file:
+ template_file.write(content)
+
+ writer = Writer(self.temp_output, settings=settings)
+ generator.generate_output(writer)
+
+ output_path = os.path.join(self.temp_output, 'generated', 'file.html')
+
+ # output file has been generated
+ self.assertTrue(os.path.exists(output_path))
+
+ # output content is correct
+ with open(output_path) as output_file:
+ self.assertEqual(output_file.read(), expected)
+
+ def test_jinja2_filter(self):
+ """JINJA_FILTERS adds custom filters to Jinja2 environment"""
+ content = 'foo: {{ foo|custom_filter }}, bar: {{ bar|custom_filter }}'
+ settings = {'JINJA_FILTERS': {'custom_filter': lambda x: x.upper()}}
+ expected = 'foo: FOO, bar: BAR'
+
+ self._test_jinja2_helper(settings, content, expected)
+
+ def test_jinja2_test(self):
+ """JINJA_TESTS adds custom tests to Jinja2 environment"""
+ content = 'foo {{ foo is custom_test }}, bar {{ bar is custom_test }}'
+ settings = {'JINJA_TESTS': {'custom_test': lambda x: x == 'bar'}}
+ expected = 'foo False, bar True'
+
+ self._test_jinja2_helper(settings, content, expected)
+
+ def test_jinja2_global(self):
+ """JINJA_GLOBALS adds custom globals to Jinja2 environment"""
+ content = '{{ custom_global }}'
+ settings = {'JINJA_GLOBALS': {'custom_global': 'foobar'}}
+ expected = 'foobar'
+
+ self._test_jinja2_helper(settings, content, expected)
+
+ def test_jinja2_extension(self):
+ """JINJA_ENVIRONMENT adds extensions to Jinja2 environment"""
+ content = '{% set stuff = [] %}{% do stuff.append(1) %}{{ stuff }}'
+ settings = {'JINJA_ENVIRONMENT': {'extensions': ['jinja2.ext.do']}}
+ expected = '[1]'
+
+ self._test_jinja2_helper(settings, content, expected)
diff --git a/pelican/tests/test_importer.py b/pelican/tests/test_importer.py
new file mode 100644
index 00000000..76feb9ce
--- /dev/null
+++ b/pelican/tests/test_importer.py
@@ -0,0 +1,453 @@
+import locale
+import os
+import re
+from posixpath import join as posix_join
+
+from pelican.settings import DEFAULT_CONFIG
+from pelican.tests.support import (mute, skipIfNoExecutable, temporary_folder,
+ unittest)
+from pelican.tools.pelican_import import (blogger2fields, build_header,
+ build_markdown_header,
+ decode_wp_content,
+ download_attachments, fields2pelican,
+ get_attachments, wp2fields)
+from pelican.utils import path_to_file_url, slugify
+
+CUR_DIR = os.path.abspath(os.path.dirname(__file__))
+BLOGGER_XML_SAMPLE = os.path.join(CUR_DIR, 'content', 'bloggerexport.xml')
+WORDPRESS_XML_SAMPLE = os.path.join(CUR_DIR, 'content', 'wordpressexport.xml')
+WORDPRESS_ENCODED_CONTENT_SAMPLE = os.path.join(CUR_DIR,
+ 'content',
+ 'wordpress_content_encoded')
+WORDPRESS_DECODED_CONTENT_SAMPLE = os.path.join(CUR_DIR,
+ 'content',
+ 'wordpress_content_decoded')
+
+try:
+ from bs4 import BeautifulSoup
+except ImportError:
+ BeautifulSoup = False # NOQA
+
+try:
+ import bs4.builder._lxml as LXML
+except ImportError:
+ LXML = False
+
+
+@skipIfNoExecutable(['pandoc', '--version'])
+@unittest.skipUnless(BeautifulSoup, 'Needs BeautifulSoup module')
+class TestBloggerXmlImporter(unittest.TestCase):
+
+ def setUp(self):
+ self.old_locale = locale.setlocale(locale.LC_ALL)
+ locale.setlocale(locale.LC_ALL, 'C')
+ self.posts = blogger2fields(BLOGGER_XML_SAMPLE)
+
+ def tearDown(self):
+ locale.setlocale(locale.LC_ALL, self.old_locale)
+
+ def test_recognise_kind_and_title(self):
+ """Check that importer only outputs pages, articles and comments,
+ that these are correctly identified and that titles are correct.
+ """
+ test_posts = list(self.posts)
+ kinds = {x[8] for x in test_posts}
+ self.assertEqual({'page', 'article', 'comment'}, kinds)
+ page_titles = {x[0] for x in test_posts if x[8] == 'page'}
+ self.assertEqual({'Test page', 'Test page 2'}, page_titles)
+ article_titles = {x[0] for x in test_posts if x[8] == 'article'}
+ self.assertEqual({'Black as Egypt\'s Night', 'The Steel Windpipe'},
+ article_titles)
+ comment_titles = {x[0] for x in test_posts if x[8] == 'comment'}
+ self.assertEqual({'Mishka, always a pleasure to read your '
+ 'adventures!...'},
+ comment_titles)
+
+ def test_recognise_status_with_correct_filename(self):
+ """Check that importerer outputs only statuses 'published' and 'draft',
+ that these are correctly identified and that filenames are correct.
+ """
+ test_posts = list(self.posts)
+ statuses = {x[7] for x in test_posts}
+ self.assertEqual({'published', 'draft'}, statuses)
+
+ draft_filenames = {x[2] for x in test_posts if x[7] == 'draft'}
+ # draft filenames are id-based
+ self.assertEqual({'page-4386962582497458967',
+ 'post-1276418104709695660'}, draft_filenames)
+
+ published_filenames = {x[2] for x in test_posts if x[7] == 'published'}
+ # published filenames are url-based, except comments
+ self.assertEqual({'the-steel-windpipe',
+ 'test-page',
+ 'post-5590533389087749201'}, published_filenames)
+
+
+@skipIfNoExecutable(['pandoc', '--version'])
+@unittest.skipUnless(BeautifulSoup, 'Needs BeautifulSoup module')
+class TestWordpressXmlImporter(unittest.TestCase):
+
+ def setUp(self):
+ self.old_locale = locale.setlocale(locale.LC_ALL)
+ locale.setlocale(locale.LC_ALL, 'C')
+ self.posts = wp2fields(WORDPRESS_XML_SAMPLE)
+ self.custposts = wp2fields(WORDPRESS_XML_SAMPLE, True)
+
+ def tearDown(self):
+ locale.setlocale(locale.LC_ALL, self.old_locale)
+
+ def test_ignore_empty_posts(self):
+ self.assertTrue(self.posts)
+ for (title, content, fname, date, author,
+ categ, tags, status, kind, format) in self.posts:
+ self.assertTrue(title.strip())
+
+ def test_recognise_page_kind(self):
+ """ Check that we recognise pages in wordpress, as opposed to posts """
+ self.assertTrue(self.posts)
+ # Collect (title, filename, kind) of non-empty posts recognised as page
+ pages_data = []
+ for (title, content, fname, date, author,
+ categ, tags, status, kind, format) in self.posts:
+ if kind == 'page':
+ pages_data.append((title, fname))
+ self.assertEqual(2, len(pages_data))
+ self.assertEqual(('Page', 'contact'), pages_data[0])
+ self.assertEqual(('Empty Page', 'empty'), pages_data[1])
+
+ def test_dirpage_directive_for_page_kind(self):
+ silent_f2p = mute(True)(fields2pelican)
+ test_post = filter(lambda p: p[0].startswith("Empty Page"), self.posts)
+ with temporary_folder() as temp:
+ fname = list(silent_f2p(test_post, 'markdown',
+ temp, dirpage=True))[0]
+ self.assertTrue(fname.endswith('pages%sempty.md' % os.path.sep))
+
+ def test_dircat(self):
+ silent_f2p = mute(True)(fields2pelican)
+ test_posts = []
+ for post in self.posts:
+ # check post kind
+ if len(post[5]) > 0: # Has a category
+ test_posts.append(post)
+ with temporary_folder() as temp:
+ fnames = list(silent_f2p(test_posts, 'markdown',
+ temp, dircat=True))
+ subs = DEFAULT_CONFIG['SLUG_REGEX_SUBSTITUTIONS']
+ index = 0
+ for post in test_posts:
+ name = post[2]
+ category = slugify(post[5][0], regex_subs=subs, preserve_case=True)
+ name += '.md'
+ filename = os.path.join(category, name)
+ out_name = fnames[index]
+ self.assertTrue(out_name.endswith(filename))
+ index += 1
+
+ def test_unless_custom_post_all_items_should_be_pages_or_posts(self):
+ self.assertTrue(self.posts)
+ pages_data = []
+ for (title, content, fname, date, author, categ,
+ tags, status, kind, format) in self.posts:
+ if kind == 'page' or kind == 'article':
+ pass
+ else:
+ pages_data.append((title, fname))
+ self.assertEqual(0, len(pages_data))
+
+ def test_recognise_custom_post_type(self):
+ self.assertTrue(self.custposts)
+ cust_data = []
+ for (title, content, fname, date, author, categ,
+ tags, status, kind, format) in self.custposts:
+ if kind == 'article' or kind == 'page':
+ pass
+ else:
+ cust_data.append((title, kind))
+ self.assertEqual(3, len(cust_data))
+ self.assertEqual(
+ ('A custom post in category 4', 'custom1'),
+ cust_data[0])
+ self.assertEqual(
+ ('A custom post in category 5', 'custom1'),
+ cust_data[1])
+ self.assertEqual(
+ ('A 2nd custom post type also in category 5', 'custom2'),
+ cust_data[2])
+
+ def test_custom_posts_put_in_own_dir(self):
+ silent_f2p = mute(True)(fields2pelican)
+ test_posts = []
+ for post in self.custposts:
+ # check post kind
+ if post[8] == 'article' or post[8] == 'page':
+ pass
+ else:
+ test_posts.append(post)
+ with temporary_folder() as temp:
+ fnames = list(silent_f2p(test_posts, 'markdown',
+ temp, wp_custpost=True))
+ index = 0
+ for post in test_posts:
+ name = post[2]
+ kind = post[8]
+ name += '.md'
+ filename = os.path.join(kind, name)
+ out_name = fnames[index]
+ self.assertTrue(out_name.endswith(filename))
+ index += 1
+
+ def test_custom_posts_put_in_own_dir_and_catagory_sub_dir(self):
+ silent_f2p = mute(True)(fields2pelican)
+ test_posts = []
+ for post in self.custposts:
+ # check post kind
+ if post[8] == 'article' or post[8] == 'page':
+ pass
+ else:
+ test_posts.append(post)
+ with temporary_folder() as temp:
+ fnames = list(silent_f2p(test_posts, 'markdown', temp,
+ wp_custpost=True, dircat=True))
+ subs = DEFAULT_CONFIG['SLUG_REGEX_SUBSTITUTIONS']
+ index = 0
+ for post in test_posts:
+ name = post[2]
+ kind = post[8]
+ category = slugify(post[5][0], regex_subs=subs, preserve_case=True)
+ name += '.md'
+ filename = os.path.join(kind, category, name)
+ out_name = fnames[index]
+ self.assertTrue(out_name.endswith(filename))
+ index += 1
+
+ def test_wp_custpost_true_dirpage_false(self):
+ # pages should only be put in their own directory when dirpage = True
+ silent_f2p = mute(True)(fields2pelican)
+ test_posts = []
+ for post in self.custposts:
+ # check post kind
+ if post[8] == 'page':
+ test_posts.append(post)
+ with temporary_folder() as temp:
+ fnames = list(silent_f2p(test_posts, 'markdown', temp,
+ wp_custpost=True, dirpage=False))
+ index = 0
+ for post in test_posts:
+ name = post[2]
+ name += '.md'
+ filename = os.path.join('pages', name)
+ out_name = fnames[index]
+ self.assertFalse(out_name.endswith(filename))
+
+ def test_can_toggle_raw_html_code_parsing(self):
+ test_posts = list(self.posts)
+
+ def r(f):
+ with open(f, encoding='utf-8') as infile:
+ return infile.read()
+ silent_f2p = mute(True)(fields2pelican)
+
+ with temporary_folder() as temp:
+
+ rst_files = (r(f) for f
+ in silent_f2p(test_posts, 'markdown', temp))
+ self.assertTrue(any('
', '')
+ content = re.sub(r'\s*(?' + allblocks + '[^>]*>)', "\\1", content)
+ content = re.sub(r'(?' + allblocks + r'[^>]*>)\s*
', "\\1", content)
+ if br:
+ def _preserve_newline(match):
+ return match.group(0).replace("\n", " ")
+ content = re.sub(
+ r'/<(script|style).*?<\/\\1>/s',
+ _preserve_newline,
+ content)
+ # optionally make line breaks
+ content = re.sub(r'(?)\s*\n', " \n", content)
+ content = content.replace(" ", "\n")
+ content = re.sub(
+ r'(?' + allblocks + r'[^>]*>)\s* ', "\\1",
+ content)
+ content = re.sub(
+ r' (\s*?(?:p|li|div|dl|dd|dt|th|pre|td|ul|ol)[^>]*>)',
+ '\\1',
+ content)
+ content = re.sub(r'\n', "", content)
+
+ if pre_tags:
+ def _multi_replace(dic, string):
+ pattern = r'|'.join(map(re.escape, dic.keys()))
+ return re.sub(pattern, lambda m: dic[m.group()], string)
+ content = _multi_replace(pre_tags, content)
+
+ return content
+
+
+def xml_to_soup(xml):
+ """Opens an xml file"""
+ try:
+ from bs4 import BeautifulSoup
+ except ImportError:
+ error = ('Missing dependency "BeautifulSoup4" and "lxml" required to '
+ 'import XML files.')
+ sys.exit(error)
+ with open(xml, encoding='utf-8') as infile:
+ xmlfile = infile.read()
+ soup = BeautifulSoup(xmlfile, "xml")
+ return soup
+
+
+def get_filename(post_name, post_id):
+ if post_name is None or post_name.isspace():
+ return post_id
+ else:
+ return post_name
+
+
+def wp2fields(xml, wp_custpost=False):
+ """Opens a wordpress XML file, and yield Pelican fields"""
+
+ soup = xml_to_soup(xml)
+ items = soup.rss.channel.findAll('item')
+ for item in items:
+
+ if item.find('status').string in ["publish", "draft"]:
+
+ try:
+ # Use HTMLParser due to issues with BeautifulSoup 3
+ title = unescape(item.title.contents[0])
+ except IndexError:
+ title = 'No title [%s]' % item.find('post_name').string
+ logger.warning('Post "%s" is lacking a proper title', title)
+
+ post_name = item.find('post_name').string
+ post_id = item.find('post_id').string
+ filename = get_filename(post_name, post_id)
+
+ content = item.find('encoded').string
+ raw_date = item.find('post_date').string
+ if raw_date == '0000-00-00 00:00:00':
+ date = None
+ else:
+ date_object = SafeDatetime.strptime(
+ raw_date, '%Y-%m-%d %H:%M:%S')
+ date = date_object.strftime('%Y-%m-%d %H:%M')
+ author = item.find('creator').string
+
+ categories = [cat.string for cat
+ in item.findAll('category', {'domain': 'category'})]
+
+ tags = [tag.string for tag
+ in item.findAll('category', {'domain': 'post_tag'})]
+ # To publish a post the status should be 'published'
+ status = 'published' if item.find('status').string == "publish" \
+ else item.find('status').string
+
+ kind = 'article'
+ post_type = item.find('post_type').string
+ if post_type == 'page':
+ kind = 'page'
+ elif wp_custpost:
+ if post_type == 'post':
+ pass
+ # Old behaviour was to name everything not a page as an
+ # article.Theoretically all attachments have status == inherit
+ # so no attachments should be here. But this statement is to
+ # maintain existing behaviour in case that doesn't hold true.
+ elif post_type == 'attachment':
+ pass
+ else:
+ kind = post_type
+ yield (title, content, filename, date, author, categories,
+ tags, status, kind, 'wp-html')
+
+
+def blogger2fields(xml):
+ """Opens a blogger XML file, and yield Pelican fields"""
+
+ soup = xml_to_soup(xml)
+ entries = soup.feed.findAll('entry')
+ for entry in entries:
+ raw_kind = entry.find(
+ 'category', {'scheme': 'http://schemas.google.com/g/2005#kind'}
+ ).get('term')
+ if raw_kind == 'http://schemas.google.com/blogger/2008/kind#post':
+ kind = 'article'
+ elif raw_kind == 'http://schemas.google.com/blogger/2008/kind#comment':
+ kind = 'comment'
+ elif raw_kind == 'http://schemas.google.com/blogger/2008/kind#page':
+ kind = 'page'
+ else:
+ continue
+
+ try:
+ assert kind != 'comment'
+ filename = entry.find('link', {'rel': 'alternate'})['href']
+ filename = os.path.splitext(os.path.basename(filename))[0]
+ except (AssertionError, TypeError, KeyError):
+ filename = entry.find('id').string.split('.')[-1]
+
+ title = entry.find('title').string or ''
+
+ content = entry.find('content').string
+ raw_date = entry.find('published').string
+ if hasattr(SafeDatetime, 'fromisoformat'):
+ date_object = SafeDatetime.fromisoformat(raw_date)
+ else:
+ date_object = SafeDatetime.strptime(
+ raw_date[:23], '%Y-%m-%dT%H:%M:%S.%f')
+ date = date_object.strftime('%Y-%m-%d %H:%M')
+ author = entry.find('author').find('name').string
+
+ # blogger posts only have tags, no category
+ tags = [tag.get('term') for tag in entry.findAll(
+ 'category', {'scheme': 'http://www.blogger.com/atom/ns#'})]
+
+ # Drafts have yes
+ status = 'published'
+ try:
+ if entry.find('control').find('draft').string == 'yes':
+ status = 'draft'
+ except AttributeError:
+ pass
+
+ yield (title, content, filename, date, author, None, tags, status,
+ kind, 'html')
+
+
+def dc2fields(file):
+ """Opens a Dotclear export file, and yield pelican fields"""
+ try:
+ from bs4 import BeautifulSoup
+ except ImportError:
+ error = ('Missing dependency '
+ '"BeautifulSoup4" and "lxml" required '
+ 'to import Dotclear files.')
+ sys.exit(error)
+
+ in_cat = False
+ in_post = False
+ category_list = {}
+ posts = []
+
+ with open(file, encoding='utf-8') as f:
+
+ for line in f:
+ # remove final \n
+ line = line[:-1]
+
+ if line.startswith('[category'):
+ in_cat = True
+ elif line.startswith('[post'):
+ in_post = True
+ elif in_cat:
+ fields = line.split('","')
+ if not line:
+ in_cat = False
+ else:
+ # remove 1st and last ""
+ fields[0] = fields[0][1:]
+ # fields[-1] = fields[-1][:-1]
+ category_list[fields[0]] = fields[2]
+ elif in_post:
+ if not line:
+ in_post = False
+ break
+ else:
+ posts.append(line)
+
+ print("%i posts read." % len(posts))
+
+ settings = read_settings()
+ subs = settings['SLUG_REGEX_SUBSTITUTIONS']
+ for post in posts:
+ fields = post.split('","')
+
+ # post_id = fields[0][1:]
+ # blog_id = fields[1]
+ # user_id = fields[2]
+ cat_id = fields[3]
+ # post_dt = fields[4]
+ # post_tz = fields[5]
+ post_creadt = fields[6]
+ # post_upddt = fields[7]
+ # post_password = fields[8]
+ # post_type = fields[9]
+ post_format = fields[10]
+ # post_url = fields[11]
+ # post_lang = fields[12]
+ post_title = fields[13]
+ post_excerpt = fields[14]
+ post_excerpt_xhtml = fields[15]
+ post_content = fields[16]
+ post_content_xhtml = fields[17]
+ # post_notes = fields[18]
+ # post_words = fields[19]
+ # post_status = fields[20]
+ # post_selected = fields[21]
+ # post_position = fields[22]
+ # post_open_comment = fields[23]
+ # post_open_tb = fields[24]
+ # nb_comment = fields[25]
+ # nb_trackback = fields[26]
+ post_meta = fields[27]
+ # redirect_url = fields[28][:-1]
+
+ # remove seconds
+ post_creadt = ':'.join(post_creadt.split(':')[0:2])
+
+ author = ''
+ categories = []
+ tags = []
+
+ if cat_id:
+ categories = [category_list[id].strip() for id
+ in cat_id.split(',')]
+
+ # Get tags related to a post
+ tag = (post_meta.replace('{', '')
+ .replace('}', '')
+ .replace('a:1:s:3:\\"tag\\";a:', '')
+ .replace('a:0:', ''))
+ if len(tag) > 1:
+ if int(len(tag[:1])) == 1:
+ newtag = tag.split('"')[1]
+ tags.append(
+ BeautifulSoup(
+ newtag,
+ 'xml'
+ )
+ # bs4 always outputs UTF-8
+ .decode('utf-8')
+ )
+ else:
+ i = 1
+ j = 1
+ while(i <= int(tag[:1])):
+ newtag = tag.split('"')[j].replace('\\', '')
+ tags.append(
+ BeautifulSoup(
+ newtag,
+ 'xml'
+ )
+ # bs4 always outputs UTF-8
+ .decode('utf-8')
+ )
+ i = i + 1
+ if j < int(tag[:1]) * 2:
+ j = j + 2
+
+ """
+ dotclear2 does not use markdown by default unless
+ you use the markdown plugin
+ Ref: http://plugins.dotaddict.org/dc2/details/formatting-markdown
+ """
+ if post_format == "markdown":
+ content = post_excerpt + post_content
+ else:
+ content = post_excerpt_xhtml + post_content_xhtml
+ content = content.replace('\\n', '')
+ post_format = "html"
+
+ kind = 'article' # TODO: Recognise pages
+ status = 'published' # TODO: Find a way for draft posts
+
+ yield (post_title, content, slugify(post_title, regex_subs=subs),
+ post_creadt, author, categories, tags, status, kind,
+ post_format)
+
+
+def posterous2fields(api_token, email, password):
+ """Imports posterous posts"""
+ import base64
+ from datetime import timedelta
+ import json
+ import urllib.request as urllib_request
+
+ def get_posterous_posts(api_token, email, password, page=1):
+ base64string = base64.encodestring(
+ ("{}:{}".format(email, password)).encode('utf-8')).replace('\n', '')
+ url = ("http://posterous.com/api/v2/users/me/sites/primary/"
+ "posts?api_token=%s&page=%d") % (api_token, page)
+ request = urllib_request.Request(url)
+ request.add_header('Authorization', 'Basic %s' % base64string.decode())
+ handle = urllib_request.urlopen(request)
+ posts = json.loads(handle.read().decode('utf-8'))
+ return posts
+
+ page = 1
+ posts = get_posterous_posts(api_token, email, password, page)
+ settings = read_settings()
+ subs = settings['SLUG_REGEX_SUBSTITUTIONS']
+ while len(posts) > 0:
+ posts = get_posterous_posts(api_token, email, password, page)
+ page += 1
+
+ for post in posts:
+ slug = post.get('slug')
+ if not slug:
+ slug = slugify(post.get('title'), regex_subs=subs)
+ tags = [tag.get('name') for tag in post.get('tags')]
+ raw_date = post.get('display_date')
+ date_object = SafeDatetime.strptime(
+ raw_date[:-6], '%Y/%m/%d %H:%M:%S')
+ offset = int(raw_date[-5:])
+ delta = timedelta(hours=(offset / 100))
+ date_object -= delta
+ date = date_object.strftime('%Y-%m-%d %H:%M')
+ kind = 'article' # TODO: Recognise pages
+ status = 'published' # TODO: Find a way for draft posts
+
+ yield (post.get('title'), post.get('body_cleaned'),
+ slug, date, post.get('user').get('display_name'),
+ [], tags, status, kind, 'html')
+
+
+def tumblr2fields(api_key, blogname):
+ """ Imports Tumblr posts (API v2)"""
+ import json
+ import urllib.request as urllib_request
+
+ def get_tumblr_posts(api_key, blogname, offset=0):
+ url = ("https://api.tumblr.com/v2/blog/%s.tumblr.com/"
+ "posts?api_key=%s&offset=%d&filter=raw") % (
+ blogname, api_key, offset)
+ request = urllib_request.Request(url)
+ handle = urllib_request.urlopen(request)
+ posts = json.loads(handle.read().decode('utf-8'))
+ return posts.get('response').get('posts')
+
+ offset = 0
+ posts = get_tumblr_posts(api_key, blogname, offset)
+ settings = read_settings()
+ subs = settings['SLUG_REGEX_SUBSTITUTIONS']
+ while len(posts) > 0:
+ for post in posts:
+ title = \
+ post.get('title') or \
+ post.get('source_title') or \
+ post.get('type').capitalize()
+ slug = post.get('slug') or slugify(title, regex_subs=subs)
+ tags = post.get('tags')
+ timestamp = post.get('timestamp')
+ date = SafeDatetime.fromtimestamp(int(timestamp)).strftime(
+ "%Y-%m-%d %H:%M:%S")
+ slug = SafeDatetime.fromtimestamp(int(timestamp)).strftime(
+ "%Y-%m-%d-") + slug
+ format = post.get('format')
+ content = post.get('body')
+ type = post.get('type')
+ if type == 'photo':
+ if format == 'markdown':
+ fmtstr = ''
+ else:
+ fmtstr = ' '
+ content = ''
+ for photo in post.get('photos'):
+ content += '\n'.join(
+ fmtstr % (photo.get('caption'),
+ photo.get('original_size').get('url')))
+ content += '\n\n' + post.get('caption')
+ elif type == 'quote':
+ if format == 'markdown':
+ fmtstr = '\n\n— %s'
+ else:
+ fmtstr = '— %s
'
+ content = post.get('text') + fmtstr % post.get('source')
+ elif type == 'link':
+ if format == 'markdown':
+ fmtstr = '[via](%s)\n\n'
+ else:
+ fmtstr = 'via
\n'
+ content = fmtstr % post.get('url') + post.get('description')
+ elif type == 'audio':
+ if format == 'markdown':
+ fmtstr = '[via](%s)\n\n'
+ else:
+ fmtstr = 'via
\n'
+ content = fmtstr % post.get('source_url') + \
+ post.get('caption') + \
+ post.get('player')
+ elif type == 'video':
+ if format == 'markdown':
+ fmtstr = '[via](%s)\n\n'
+ else:
+ fmtstr = 'via
\n'
+ source = fmtstr % post.get('source_url')
+ caption = post.get('caption')
+ players = '\n'.join(player.get('embed_code')
+ for player in post.get('player'))
+ content = source + caption + players
+ elif type == 'answer':
+ title = post.get('question')
+ content = (''
+ '%s '
+ ': %s'
+ '
\n'
+ ' %s' % (post.get('asking_name'),
+ post.get('asking_url'),
+ post.get('question'),
+ post.get('answer')))
+
+ content = content.rstrip() + '\n'
+ kind = 'article'
+ status = 'published' # TODO: Find a way for draft posts
+
+ yield (title, content, slug, date, post.get('blog_name'), [type],
+ tags, status, kind, format)
+
+ offset += len(posts)
+ posts = get_tumblr_posts(api_key, blogname, offset)
+
+
+def feed2fields(file):
+ """Read a feed and yield pelican fields"""
+ import feedparser
+ d = feedparser.parse(file)
+ settings = read_settings()
+ subs = settings['SLUG_REGEX_SUBSTITUTIONS']
+ for entry in d.entries:
+ date = (time.strftime('%Y-%m-%d %H:%M', entry.updated_parsed)
+ if hasattr(entry, 'updated_parsed') else None)
+ author = entry.author if hasattr(entry, 'author') else None
+ tags = ([e['term'] for e in entry.tags]
+ if hasattr(entry, 'tags') else None)
+
+ slug = slugify(entry.title, regex_subs=subs)
+ kind = 'article'
+ yield (entry.title, entry.description, slug, date,
+ author, [], tags, None, kind, 'html')
+
+
+def build_header(title, date, author, categories, tags, slug,
+ status=None, attachments=None):
+ """Build a header from a list of fields"""
+
+ from docutils.utils import column_width
+
+ header = '{}\n{}\n'.format(title, '#' * column_width(title))
+ if date:
+ header += ':date: %s\n' % date
+ if author:
+ header += ':author: %s\n' % author
+ if categories:
+ header += ':category: %s\n' % ', '.join(categories)
+ if tags:
+ header += ':tags: %s\n' % ', '.join(tags)
+ if slug:
+ header += ':slug: %s\n' % slug
+ if status:
+ header += ':status: %s\n' % status
+ if attachments:
+ header += ':attachments: %s\n' % ', '.join(attachments)
+ header += '\n'
+ return header
+
+
+def build_asciidoc_header(title, date, author, categories, tags, slug,
+ status=None, attachments=None):
+ """Build a header from a list of fields"""
+
+ header = '= %s\n' % title
+ if author:
+ header += '%s\n' % author
+ if date:
+ header += '%s\n' % date
+ if categories:
+ header += ':category: %s\n' % ', '.join(categories)
+ if tags:
+ header += ':tags: %s\n' % ', '.join(tags)
+ if slug:
+ header += ':slug: %s\n' % slug
+ if status:
+ header += ':status: %s\n' % status
+ if attachments:
+ header += ':attachments: %s\n' % ', '.join(attachments)
+ header += '\n'
+ return header
+
+
+def build_markdown_header(title, date, author, categories, tags,
+ slug, status=None, attachments=None):
+ """Build a header from a list of fields"""
+ header = 'Title: %s\n' % title
+ if date:
+ header += 'Date: %s\n' % date
+ if author:
+ header += 'Author: %s\n' % author
+ if categories:
+ header += 'Category: %s\n' % ', '.join(categories)
+ if tags:
+ header += 'Tags: %s\n' % ', '.join(tags)
+ if slug:
+ header += 'Slug: %s\n' % slug
+ if status:
+ header += 'Status: %s\n' % status
+ if attachments:
+ header += 'Attachments: %s\n' % ', '.join(attachments)
+ header += '\n'
+ return header
+
+
+def get_ext(out_markup, in_markup='html'):
+ if out_markup == 'asciidoc':
+ ext = '.adoc'
+ elif in_markup == 'markdown' or out_markup == 'markdown':
+ ext = '.md'
+ else:
+ ext = '.rst'
+ return ext
+
+
+def get_out_filename(output_path, filename, ext, kind,
+ dirpage, dircat, categories, wp_custpost, slug_subs):
+ filename = os.path.basename(filename)
+
+ # Enforce filename restrictions for various filesystems at once; see
+ # https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
+ # we do not need to filter words because an extension will be appended
+ filename = re.sub(r'[<>:"/\\|?*^% ]', '-', filename) # invalid chars
+ filename = filename.lstrip('.') # should not start with a dot
+ if not filename:
+ filename = '_'
+ filename = filename[:249] # allow for 5 extra characters
+
+ out_filename = os.path.join(output_path, filename + ext)
+ # option to put page posts in pages/ subdirectory
+ if dirpage and kind == 'page':
+ pages_dir = os.path.join(output_path, 'pages')
+ if not os.path.isdir(pages_dir):
+ os.mkdir(pages_dir)
+ out_filename = os.path.join(pages_dir, filename + ext)
+ elif not dirpage and kind == 'page':
+ pass
+ # option to put wp custom post types in directories with post type
+ # names. Custom post types can also have categories so option to
+ # create subdirectories with category names
+ elif kind != 'article':
+ if wp_custpost:
+ typename = slugify(kind, regex_subs=slug_subs)
+ else:
+ typename = ''
+ kind = 'article'
+ if dircat and (len(categories) > 0):
+ catname = slugify(
+ categories[0], regex_subs=slug_subs, preserve_case=True)
+ else:
+ catname = ''
+ out_filename = os.path.join(output_path, typename,
+ catname, filename + ext)
+ if not os.path.isdir(os.path.join(output_path, typename, catname)):
+ os.makedirs(os.path.join(output_path, typename, catname))
+ # option to put files in directories with categories names
+ elif dircat and (len(categories) > 0):
+ catname = slugify(
+ categories[0], regex_subs=slug_subs, preserve_case=True)
+ out_filename = os.path.join(output_path, catname, filename + ext)
+ if not os.path.isdir(os.path.join(output_path, catname)):
+ os.mkdir(os.path.join(output_path, catname))
+
+ return out_filename
+
+
+def get_attachments(xml):
+ """returns a dictionary of posts that have attachments with a list
+ of the attachment_urls
+ """
+ soup = xml_to_soup(xml)
+ items = soup.rss.channel.findAll('item')
+ names = {}
+ attachments = []
+
+ for item in items:
+ kind = item.find('post_type').string
+ post_name = item.find('post_name').string
+ post_id = item.find('post_id').string
+
+ if kind == 'attachment':
+ attachments.append((item.find('post_parent').string,
+ item.find('attachment_url').string))
+ else:
+ filename = get_filename(post_name, post_id)
+ names[post_id] = filename
+ attachedposts = defaultdict(set)
+ for parent, url in attachments:
+ try:
+ parent_name = names[parent]
+ except KeyError:
+ # attachment's parent is not a valid post
+ parent_name = None
+
+ attachedposts[parent_name].add(url)
+ return attachedposts
+
+
+def download_attachments(output_path, urls):
+ """Downloads WordPress attachments and returns a list of paths to
+ attachments that can be associated with a post (relative path to output
+ directory). Files that fail to download, will not be added to posts"""
+ locations = {}
+ for url in urls:
+ path = urlparse(url).path
+ # teardown path and rebuild to negate any errors with
+ # os.path.join and leading /'s
+ path = path.split('/')
+ filename = path.pop(-1)
+ localpath = ''
+ for item in path:
+ if sys.platform != 'win32' or ':' not in item:
+ localpath = os.path.join(localpath, item)
+ full_path = os.path.join(output_path, localpath)
+
+ # Generate percent-encoded URL
+ scheme, netloc, path, query, fragment = urlsplit(url)
+ if scheme != 'file':
+ path = quote(path)
+ url = urlunsplit((scheme, netloc, path, query, fragment))
+
+ if not os.path.exists(full_path):
+ os.makedirs(full_path)
+ print('downloading {}'.format(filename))
+ try:
+ urlretrieve(url, os.path.join(full_path, filename))
+ locations[url] = os.path.join(localpath, filename)
+ except (URLError, OSError) as e:
+ # Python 2.7 throws an IOError rather Than URLError
+ logger.warning("No file could be downloaded from %s\n%s", url, e)
+ return locations
+
+
+def is_pandoc_needed(in_markup):
+ return in_markup in ('html', 'wp-html')
+
+
+def get_pandoc_version():
+ cmd = ['pandoc', '--version']
+ try:
+ output = subprocess.check_output(cmd, universal_newlines=True)
+ except (subprocess.CalledProcessError, OSError) as e:
+ logger.warning("Pandoc version unknown: %s", e)
+ return ()
+
+ return tuple(int(i) for i in output.split()[1].split('.'))
+
+
+def update_links_to_attached_files(content, attachments):
+ for old_url, new_path in attachments.items():
+ # url may occur both with http:// and https://
+ http_url = old_url.replace('https://', 'http://')
+ https_url = old_url.replace('http://', 'https://')
+ for url in [http_url, https_url]:
+ content = content.replace(url, '{static}' + new_path)
+ return content
+
+
+def fields2pelican(
+ fields, out_markup, output_path,
+ dircat=False, strip_raw=False, disable_slugs=False,
+ dirpage=False, filename_template=None, filter_author=None,
+ wp_custpost=False, wp_attach=False, attachments=None):
+
+ pandoc_version = get_pandoc_version()
+ posts_require_pandoc = []
+
+ settings = read_settings()
+ slug_subs = settings['SLUG_REGEX_SUBSTITUTIONS']
+
+ for (title, content, filename, date, author, categories, tags, status,
+ kind, in_markup) in fields:
+ if filter_author and filter_author != author:
+ continue
+ if is_pandoc_needed(in_markup) and not pandoc_version:
+ posts_require_pandoc.append(filename)
+
+ slug = not disable_slugs and filename or None
+
+ if wp_attach and attachments:
+ try:
+ urls = attachments[filename]
+ links = download_attachments(output_path, urls)
+ except KeyError:
+ links = None
+ else:
+ links = None
+
+ ext = get_ext(out_markup, in_markup)
+ if ext == '.adoc':
+ header = build_asciidoc_header(title, date, author, categories,
+ tags, slug, status, attachments)
+ elif ext == '.md':
+ header = build_markdown_header(
+ title, date, author, categories, tags, slug,
+ status, links.values() if links else None)
+ else:
+ out_markup = 'rst'
+ header = build_header(title, date, author, categories,
+ tags, slug, status, links.values()
+ if links else None)
+
+ out_filename = get_out_filename(
+ output_path, filename, ext, kind, dirpage, dircat,
+ categories, wp_custpost, slug_subs)
+ print(out_filename)
+
+ if in_markup in ('html', 'wp-html'):
+ html_filename = os.path.join(output_path, filename + '.html')
+
+ with open(html_filename, 'w', encoding='utf-8') as fp:
+ # Replace newlines with paragraphs wrapped with so
+ # HTML is valid before conversion
+ if in_markup == 'wp-html':
+ new_content = decode_wp_content(content)
+ else:
+ paragraphs = content.splitlines()
+ paragraphs = ['
{}
'.format(p) for p in paragraphs]
+ new_content = ''.join(paragraphs)
+
+ fp.write(new_content)
+
+ if pandoc_version < (2,):
+ parse_raw = '--parse-raw' if not strip_raw else ''
+ wrap_none = '--wrap=none' \
+ if pandoc_version >= (1, 16) else '--no-wrap'
+ cmd = ('pandoc --normalize {0} --from=html'
+ ' --to={1} {2} -o "{3}" "{4}"')
+ cmd = cmd.format(parse_raw, out_markup, wrap_none,
+ out_filename, html_filename)
+ else:
+ from_arg = '-f html+raw_html' if not strip_raw else '-f html'
+ cmd = ('pandoc {0} --to={1}-smart --wrap=none -o "{2}" "{3}"')
+ cmd = cmd.format(from_arg, out_markup,
+ out_filename, html_filename)
+
+ try:
+ rc = subprocess.call(cmd, shell=True)
+ if rc < 0:
+ error = 'Child was terminated by signal %d' % -rc
+ exit(error)
+
+ elif rc > 0:
+ error = 'Please, check your Pandoc installation.'
+ exit(error)
+ except OSError as e:
+ error = 'Pandoc execution failed: %s' % e
+ exit(error)
+
+ os.remove(html_filename)
+
+ with open(out_filename, encoding='utf-8') as fs:
+ content = fs.read()
+ if out_markup == 'markdown':
+ # In markdown, to insert a , end a line with two
+ # or more spaces & then a end-of-line
+ content = content.replace('\\\n ', ' \n')
+ content = content.replace('\\\n', ' \n')
+
+ if wp_attach and links:
+ content = update_links_to_attached_files(content, links)
+
+ with open(out_filename, 'w', encoding='utf-8') as fs:
+ fs.write(header + content)
+
+ if posts_require_pandoc:
+ logger.error("Pandoc must be installed to import the following posts:"
+ "\n {}".format("\n ".join(posts_require_pandoc)))
+
+ if wp_attach and attachments and None in attachments:
+ print("downloading attachments that don't have a parent post")
+ urls = attachments[None]
+ download_attachments(output_path, urls)
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Transform feed, Blogger, Dotclear, Posterous, Tumblr, or "
+ "WordPress files into reST (rst) or Markdown (md) files. "
+ "Be sure to have pandoc installed.",
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter)
+
+ parser.add_argument(
+ dest='input', help='The input file to read')
+ parser.add_argument(
+ '--blogger', action='store_true', dest='blogger',
+ help='Blogger XML export')
+ parser.add_argument(
+ '--dotclear', action='store_true', dest='dotclear',
+ help='Dotclear export')
+ parser.add_argument(
+ '--posterous', action='store_true', dest='posterous',
+ help='Posterous export')
+ parser.add_argument(
+ '--tumblr', action='store_true', dest='tumblr',
+ help='Tumblr export')
+ parser.add_argument(
+ '--wpfile', action='store_true', dest='wpfile',
+ help='Wordpress XML export')
+ parser.add_argument(
+ '--feed', action='store_true', dest='feed',
+ help='Feed to parse')
+ parser.add_argument(
+ '-o', '--output', dest='output', default='content',
+ help='Output path')
+ parser.add_argument(
+ '-m', '--markup', dest='markup', default='rst',
+ help='Output markup format (supports rst & markdown)')
+ parser.add_argument(
+ '--dir-cat', action='store_true', dest='dircat',
+ help='Put files in directories with categories name')
+ parser.add_argument(
+ '--dir-page', action='store_true', dest='dirpage',
+ help=('Put files recognised as pages in "pages/" sub-directory'
+ ' (blogger and wordpress import only)'))
+ parser.add_argument(
+ '--filter-author', dest='author',
+ help='Import only post from the specified author')
+ parser.add_argument(
+ '--strip-raw', action='store_true', dest='strip_raw',
+ help="Strip raw HTML code that can't be converted to "
+ "markup such as flash embeds or iframes (wordpress import only)")
+ parser.add_argument(
+ '--wp-custpost', action='store_true',
+ dest='wp_custpost',
+ help='Put wordpress custom post types in directories. If used with '
+ '--dir-cat option directories will be created as '
+ '/post_type/category/ (wordpress import only)')
+ parser.add_argument(
+ '--wp-attach', action='store_true', dest='wp_attach',
+ help='(wordpress import only) Download files uploaded to wordpress as '
+ 'attachments. Files will be added to posts as a list in the post '
+ 'header. All files will be downloaded, even if '
+ "they aren't associated with a post. Files will be downloaded "
+ 'with their original path inside the output directory. '
+ 'e.g. output/wp-uploads/date/postname/file.jpg '
+ '-- Requires an internet connection --')
+ parser.add_argument(
+ '--disable-slugs', action='store_true',
+ dest='disable_slugs',
+ help='Disable storing slugs from imported posts within output. '
+ 'With this disabled, your Pelican URLs may not be consistent '
+ 'with your original posts.')
+ parser.add_argument(
+ '-e', '--email', dest='email',
+ help="Email address (posterous import only)")
+ parser.add_argument(
+ '-p', '--password', dest='password',
+ help="Password (posterous import only)")
+ parser.add_argument(
+ '-b', '--blogname', dest='blogname',
+ help="Blog name (Tumblr import only)")
+
+ args = parser.parse_args()
+
+ input_type = None
+ if args.blogger:
+ input_type = 'blogger'
+ elif args.dotclear:
+ input_type = 'dotclear'
+ elif args.posterous:
+ input_type = 'posterous'
+ elif args.tumblr:
+ input_type = 'tumblr'
+ elif args.wpfile:
+ input_type = 'wordpress'
+ elif args.feed:
+ input_type = 'feed'
+ else:
+ error = ('You must provide either --blogger, --dotclear, '
+ '--posterous, --tumblr, --wpfile or --feed options')
+ exit(error)
+
+ if not os.path.exists(args.output):
+ try:
+ os.mkdir(args.output)
+ except OSError:
+ error = 'Unable to create the output folder: ' + args.output
+ exit(error)
+
+ if args.wp_attach and input_type != 'wordpress':
+ error = ('You must be importing a wordpress xml '
+ 'to use the --wp-attach option')
+ exit(error)
+
+ if input_type == 'blogger':
+ fields = blogger2fields(args.input)
+ elif input_type == 'dotclear':
+ fields = dc2fields(args.input)
+ elif input_type == 'posterous':
+ fields = posterous2fields(args.input, args.email, args.password)
+ elif input_type == 'tumblr':
+ fields = tumblr2fields(args.input, args.blogname)
+ elif input_type == 'wordpress':
+ fields = wp2fields(args.input, args.wp_custpost or False)
+ elif input_type == 'feed':
+ fields = feed2fields(args.input)
+
+ if args.wp_attach:
+ attachments = get_attachments(args.input)
+ else:
+ attachments = None
+
+ # init logging
+ init()
+ fields2pelican(fields, args.markup, args.output,
+ dircat=args.dircat or False,
+ dirpage=args.dirpage or False,
+ strip_raw=args.strip_raw or False,
+ disable_slugs=args.disable_slugs or False,
+ filter_author=args.author,
+ wp_custpost=args.wp_custpost or False,
+ wp_attach=args.wp_attach or False,
+ attachments=attachments or None)
diff --git a/pelican/tools/pelican_quickstart.py b/pelican/tools/pelican_quickstart.py
new file mode 100755
index 00000000..41ddea3c
--- /dev/null
+++ b/pelican/tools/pelican_quickstart.py
@@ -0,0 +1,354 @@
+#!/usr/bin/env python
+
+import argparse
+import locale
+import os
+
+from jinja2 import Environment, FileSystemLoader
+
+import pytz
+
+try:
+ import readline # NOQA
+except ImportError:
+ pass
+
+try:
+ import tzlocal
+ _DEFAULT_TIMEZONE = tzlocal.get_localzone().zone
+except ImportError:
+ _DEFAULT_TIMEZONE = 'Europe/Paris'
+
+from pelican import __version__
+
+locale.setlocale(locale.LC_ALL, '')
+try:
+ _DEFAULT_LANGUAGE = locale.getlocale()[0]
+except ValueError:
+ # Don't fail on macosx: "unknown locale: UTF-8"
+ _DEFAULT_LANGUAGE = None
+if _DEFAULT_LANGUAGE is None:
+ _DEFAULT_LANGUAGE = 'en'
+else:
+ _DEFAULT_LANGUAGE = _DEFAULT_LANGUAGE.split('_')[0]
+
+_TEMPLATES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+ "templates")
+_jinja_env = Environment(
+ loader=FileSystemLoader(_TEMPLATES_DIR),
+ trim_blocks=True,
+)
+
+
+_GITHUB_PAGES_BRANCHES = {
+ 'personal': 'main',
+ 'project': 'gh-pages'
+}
+
+CONF = {
+ 'pelican': 'pelican',
+ 'pelicanopts': '',
+ 'basedir': os.curdir,
+ 'ftp_host': 'localhost',
+ 'ftp_user': 'anonymous',
+ 'ftp_target_dir': '/',
+ 'ssh_host': 'localhost',
+ 'ssh_port': 22,
+ 'ssh_user': 'root',
+ 'ssh_target_dir': '/var/www',
+ 's3_bucket': 'my_s3_bucket',
+ 'cloudfiles_username': 'my_rackspace_username',
+ 'cloudfiles_api_key': 'my_rackspace_api_key',
+ 'cloudfiles_container': 'my_cloudfiles_container',
+ 'dropbox_dir': '~/Dropbox/Public/',
+ 'github_pages_branch': _GITHUB_PAGES_BRANCHES['project'],
+ 'default_pagination': 10,
+ 'siteurl': '',
+ 'lang': _DEFAULT_LANGUAGE,
+ 'timezone': _DEFAULT_TIMEZONE
+}
+
+# url for list of valid timezones
+_TZ_URL = 'https://en.wikipedia.org/wiki/List_of_tz_database_time_zones'
+
+
+# Create a 'marked' default path, to determine if someone has supplied
+# a path on the command-line.
+class _DEFAULT_PATH_TYPE(str):
+ is_default_path = True
+
+
+_DEFAULT_PATH = _DEFAULT_PATH_TYPE(os.curdir)
+
+
+def ask(question, answer=str, default=None, length=None):
+ if answer == str:
+ r = ''
+ while True:
+ if default:
+ r = input('> {} [{}] '.format(question, default))
+ else:
+ r = input('> {} '.format(question))
+
+ r = r.strip()
+
+ if len(r) <= 0:
+ if default:
+ r = default
+ break
+ else:
+ print('You must enter something')
+ else:
+ if length and len(r) != length:
+ print('Entry must be {} characters long'.format(length))
+ else:
+ break
+
+ return r
+
+ elif answer == bool:
+ r = None
+ while True:
+ if default is True:
+ r = input('> {} (Y/n) '.format(question))
+ elif default is False:
+ r = input('> {} (y/N) '.format(question))
+ else:
+ r = input('> {} (y/n) '.format(question))
+
+ r = r.strip().lower()
+
+ if r in ('y', 'yes'):
+ r = True
+ break
+ elif r in ('n', 'no'):
+ r = False
+ break
+ elif not r:
+ r = default
+ break
+ else:
+ print("You must answer 'yes' or 'no'")
+ return r
+ elif answer == int:
+ r = None
+ while True:
+ if default:
+ r = input('> {} [{}] '.format(question, default))
+ else:
+ r = input('> {} '.format(question))
+
+ r = r.strip()
+
+ if not r:
+ r = default
+ break
+
+ try:
+ r = int(r)
+ break
+ except ValueError:
+ print('You must enter an integer')
+ return r
+ else:
+ raise NotImplementedError(
+ 'Argument `answer` must be str, bool, or integer')
+
+
+def ask_timezone(question, default, tzurl):
+ """Prompt for time zone and validate input"""
+ lower_tz = [tz.lower() for tz in pytz.all_timezones]
+ while True:
+ r = ask(question, str, default)
+ r = r.strip().replace(' ', '_').lower()
+ if r in lower_tz:
+ r = pytz.all_timezones[lower_tz.index(r)]
+ break
+ else:
+ print('Please enter a valid time zone:\n'
+ ' (check [{}])'.format(tzurl))
+ return r
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="A kickstarter for Pelican",
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter)
+ parser.add_argument('-p', '--path', default=_DEFAULT_PATH,
+ help="The path to generate the blog into")
+ parser.add_argument('-t', '--title', metavar="title",
+ help='Set the title of the website')
+ parser.add_argument('-a', '--author', metavar="author",
+ help='Set the author name of the website')
+ parser.add_argument('-l', '--lang', metavar="lang",
+ help='Set the default web site language')
+
+ args = parser.parse_args()
+
+ print('''Welcome to pelican-quickstart v{v}.
+
+This script will help you create a new Pelican-based website.
+
+Please answer the following questions so this script can generate the files
+needed by Pelican.
+
+ '''.format(v=__version__))
+
+ project = os.path.join(
+ os.environ.get('VIRTUAL_ENV', os.curdir), '.project')
+ no_path_was_specified = hasattr(args.path, 'is_default_path')
+ if os.path.isfile(project) and no_path_was_specified:
+ CONF['basedir'] = open(project).read().rstrip("\n")
+ print('Using project associated with current virtual environment. '
+ 'Will save to:\n%s\n' % CONF['basedir'])
+ else:
+ CONF['basedir'] = os.path.abspath(os.path.expanduser(
+ ask('Where do you want to create your new web site?',
+ answer=str, default=args.path)))
+
+ CONF['sitename'] = ask('What will be the title of this web site?',
+ answer=str, default=args.title)
+ CONF['author'] = ask('Who will be the author of this web site?',
+ answer=str, default=args.author)
+ CONF['lang'] = ask('What will be the default language of this web site?',
+ str, args.lang or CONF['lang'], 2)
+
+ if ask('Do you want to specify a URL prefix? e.g., https://example.com ',
+ answer=bool, default=True):
+ CONF['siteurl'] = ask('What is your URL prefix? (see '
+ 'above example; no trailing slash)',
+ str, CONF['siteurl'])
+
+ CONF['with_pagination'] = ask('Do you want to enable article pagination?',
+ bool, bool(CONF['default_pagination']))
+
+ if CONF['with_pagination']:
+ CONF['default_pagination'] = ask('How many articles per page '
+ 'do you want?',
+ int, CONF['default_pagination'])
+ else:
+ CONF['default_pagination'] = False
+
+ CONF['timezone'] = ask_timezone('What is your time zone?',
+ CONF['timezone'], _TZ_URL)
+
+ automation = ask('Do you want to generate a tasks.py/Makefile '
+ 'to automate generation and publishing?', bool, True)
+
+ if automation:
+ if ask('Do you want to upload your website using FTP?',
+ answer=bool, default=False):
+ CONF['ftp'] = True,
+ CONF['ftp_host'] = ask('What is the hostname of your FTP server?',
+ str, CONF['ftp_host'])
+ CONF['ftp_user'] = ask('What is your username on that server?',
+ str, CONF['ftp_user'])
+ CONF['ftp_target_dir'] = ask('Where do you want to put your '
+ 'web site on that server?',
+ str, CONF['ftp_target_dir'])
+ if ask('Do you want to upload your website using SSH?',
+ answer=bool, default=False):
+ CONF['ssh'] = True,
+ CONF['ssh_host'] = ask('What is the hostname of your SSH server?',
+ str, CONF['ssh_host'])
+ CONF['ssh_port'] = ask('What is the port of your SSH server?',
+ int, CONF['ssh_port'])
+ CONF['ssh_user'] = ask('What is your username on that server?',
+ str, CONF['ssh_user'])
+ CONF['ssh_target_dir'] = ask('Where do you want to put your '
+ 'web site on that server?',
+ str, CONF['ssh_target_dir'])
+
+ if ask('Do you want to upload your website using Dropbox?',
+ answer=bool, default=False):
+ CONF['dropbox'] = True,
+ CONF['dropbox_dir'] = ask('Where is your Dropbox directory?',
+ str, CONF['dropbox_dir'])
+
+ if ask('Do you want to upload your website using S3?',
+ answer=bool, default=False):
+ CONF['s3'] = True,
+ CONF['s3_bucket'] = ask('What is the name of your S3 bucket?',
+ str, CONF['s3_bucket'])
+
+ if ask('Do you want to upload your website using '
+ 'Rackspace Cloud Files?', answer=bool, default=False):
+ CONF['cloudfiles'] = True,
+ CONF['cloudfiles_username'] = ask('What is your Rackspace '
+ 'Cloud username?', str,
+ CONF['cloudfiles_username'])
+ CONF['cloudfiles_api_key'] = ask('What is your Rackspace '
+ 'Cloud API key?', str,
+ CONF['cloudfiles_api_key'])
+ CONF['cloudfiles_container'] = ask('What is the name of your '
+ 'Cloud Files container?',
+ str,
+ CONF['cloudfiles_container'])
+
+ if ask('Do you want to upload your website using GitHub Pages?',
+ answer=bool, default=False):
+ CONF['github'] = True,
+ if ask('Is this your personal page (username.github.io)?',
+ answer=bool, default=False):
+ CONF['github_pages_branch'] = \
+ _GITHUB_PAGES_BRANCHES['personal']
+ else:
+ CONF['github_pages_branch'] = \
+ _GITHUB_PAGES_BRANCHES['project']
+
+ try:
+ os.makedirs(os.path.join(CONF['basedir'], 'content'))
+ except OSError as e:
+ print('Error: {}'.format(e))
+
+ try:
+ os.makedirs(os.path.join(CONF['basedir'], 'output'))
+ except OSError as e:
+ print('Error: {}'.format(e))
+
+ try:
+ with open(os.path.join(CONF['basedir'], 'pelicanconf.py'),
+ 'w', encoding='utf-8') as fd:
+ conf_python = dict()
+ for key, value in CONF.items():
+ conf_python[key] = repr(value)
+
+ _template = _jinja_env.get_template('pelicanconf.py.jinja2')
+ fd.write(_template.render(**conf_python))
+ fd.close()
+ except OSError as e:
+ print('Error: {}'.format(e))
+
+ try:
+ with open(os.path.join(CONF['basedir'], 'publishconf.py'),
+ 'w', encoding='utf-8') as fd:
+ _template = _jinja_env.get_template('publishconf.py.jinja2')
+ fd.write(_template.render(**CONF))
+ fd.close()
+ except OSError as e:
+ print('Error: {}'.format(e))
+
+ if automation:
+ try:
+ with open(os.path.join(CONF['basedir'], 'tasks.py'),
+ 'w', encoding='utf-8') as fd:
+ _template = _jinja_env.get_template('tasks.py.jinja2')
+ fd.write(_template.render(**CONF))
+ fd.close()
+ except OSError as e:
+ print('Error: {}'.format(e))
+ try:
+ with open(os.path.join(CONF['basedir'], 'Makefile'),
+ 'w', encoding='utf-8') as fd:
+ py_v = 'python3'
+ _template = _jinja_env.get_template('Makefile.jinja2')
+ fd.write(_template.render(py_v=py_v, **CONF))
+ fd.close()
+ except OSError as e:
+ print('Error: {}'.format(e))
+
+ print('Done. Your new project is available at %s' % CONF['basedir'])
+
+
+if __name__ == "__main__":
+ main()
diff --git a/pelican/tools/pelican_themes.py b/pelican/tools/pelican_themes.py
new file mode 100755
index 00000000..96d07c1f
--- /dev/null
+++ b/pelican/tools/pelican_themes.py
@@ -0,0 +1,259 @@
+#!/usr/bin/env python
+
+import argparse
+import os
+import shutil
+import sys
+
+
+def err(msg, die=None):
+ """Print an error message and exits if an exit code is given"""
+ sys.stderr.write(msg + '\n')
+ if die:
+ sys.exit(die if type(die) is int else 1)
+
+
+try:
+ import pelican
+except ImportError:
+ err('Cannot import pelican.\nYou must '
+ 'install Pelican in order to run this script.',
+ -1)
+
+
+global _THEMES_PATH
+_THEMES_PATH = os.path.join(
+ os.path.dirname(
+ os.path.abspath(pelican.__file__)
+ ),
+ 'themes'
+)
+
+__version__ = '0.2'
+_BUILTIN_THEMES = ['simple', 'notmyidea']
+
+
+def main():
+ """Main function"""
+
+ parser = argparse.ArgumentParser(
+ description="""Install themes for Pelican""")
+
+ excl = parser.add_mutually_exclusive_group()
+ excl.add_argument(
+ '-l', '--list', dest='action', action="store_const", const='list',
+ help="Show the themes already installed and exit")
+ excl.add_argument(
+ '-p', '--path', dest='action', action="store_const", const='path',
+ help="Show the themes path and exit")
+ excl.add_argument(
+ '-V', '--version', action='version',
+ version='pelican-themes v{}'.format(__version__),
+ help='Print the version of this script')
+
+ parser.add_argument(
+ '-i', '--install', dest='to_install', nargs='+', metavar="theme path",
+ help='The themes to install')
+ parser.add_argument(
+ '-r', '--remove', dest='to_remove', nargs='+', metavar="theme name",
+ help='The themes to remove')
+ parser.add_argument(
+ '-U', '--upgrade', dest='to_upgrade', nargs='+',
+ metavar="theme path", help='The themes to upgrade')
+ parser.add_argument(
+ '-s', '--symlink', dest='to_symlink', nargs='+', metavar="theme path",
+ help="Same as `--install', but create a symbolic link instead of "
+ "copying the theme. Useful for theme development")
+ parser.add_argument(
+ '-c', '--clean', dest='clean', action="store_true",
+ help="Remove the broken symbolic links of the theme path")
+
+ parser.add_argument(
+ '-v', '--verbose', dest='verbose',
+ action="store_true",
+ help="Verbose output")
+
+ args = parser.parse_args()
+
+ to_install = args.to_install or args.to_upgrade
+ to_sym = args.to_symlink or args.clean
+
+ if args.action:
+ if args.action == 'list':
+ list_themes(args.verbose)
+ elif args.action == 'path':
+ print(_THEMES_PATH)
+ elif to_install or args.to_remove or to_sym:
+ if args.to_remove:
+ if args.verbose:
+ print('Removing themes...')
+
+ for i in args.to_remove:
+ remove(i, v=args.verbose)
+
+ if args.to_install:
+ if args.verbose:
+ print('Installing themes...')
+
+ for i in args.to_install:
+ install(i, v=args.verbose)
+
+ if args.to_upgrade:
+ if args.verbose:
+ print('Upgrading themes...')
+
+ for i in args.to_upgrade:
+ install(i, v=args.verbose, u=True)
+
+ if args.to_symlink:
+ if args.verbose:
+ print('Linking themes...')
+
+ for i in args.to_symlink:
+ symlink(i, v=args.verbose)
+
+ if args.clean:
+ if args.verbose:
+ print('Cleaning the themes directory...')
+
+ clean(v=args.verbose)
+ else:
+ print('No argument given... exiting.')
+
+
+def themes():
+ """Returns the list of the themes"""
+ for i in os.listdir(_THEMES_PATH):
+ e = os.path.join(_THEMES_PATH, i)
+
+ if os.path.isdir(e):
+ if os.path.islink(e):
+ yield (e, os.readlink(e))
+ else:
+ yield (e, None)
+
+
+def list_themes(v=False):
+ """Display the list of the themes"""
+ for t, l in themes():
+ if not v:
+ t = os.path.basename(t)
+ if l:
+ if v:
+ print(t + (" (symbolic link to `" + l + "')"))
+ else:
+ print(t + '@')
+ else:
+ print(t)
+
+
+def remove(theme_name, v=False):
+ """Removes a theme"""
+
+ theme_name = theme_name.replace('/', '')
+ target = os.path.join(_THEMES_PATH, theme_name)
+
+ if theme_name in _BUILTIN_THEMES:
+ err(theme_name + ' is a builtin theme.\n'
+ 'You cannot remove a builtin theme with this script, '
+ 'remove it by hand if you want.')
+ elif os.path.islink(target):
+ if v:
+ print('Removing link `' + target + "'")
+ os.remove(target)
+ elif os.path.isdir(target):
+ if v:
+ print('Removing directory `' + target + "'")
+ shutil.rmtree(target)
+ elif os.path.exists(target):
+ err(target + ' : not a valid theme')
+ else:
+ err(target + ' : no such file or directory')
+
+
+def install(path, v=False, u=False):
+ """Installs a theme"""
+ if not os.path.exists(path):
+ err(path + ' : no such file or directory')
+ elif not os.path.isdir(path):
+ err(path + ' : not a directory')
+ else:
+ theme_name = os.path.basename(os.path.normpath(path))
+ theme_path = os.path.join(_THEMES_PATH, theme_name)
+ exists = os.path.exists(theme_path)
+ if exists and not u:
+ err(path + ' : already exists')
+ elif exists and u:
+ remove(theme_name, v)
+ install(path, v)
+ else:
+ if v:
+ print("Copying '{p}' to '{t}' ...".format(p=path,
+ t=theme_path))
+ try:
+ shutil.copytree(path, theme_path)
+
+ try:
+ if os.name == 'posix':
+ for root, dirs, files in os.walk(theme_path):
+ for d in dirs:
+ dname = os.path.join(root, d)
+ os.chmod(dname, 493) # 0o755
+ for f in files:
+ fname = os.path.join(root, f)
+ os.chmod(fname, 420) # 0o644
+ except OSError as e:
+ err("Cannot change permissions of files "
+ "or directory in `{r}':\n{e}".format(r=theme_path,
+ e=str(e)),
+ die=False)
+ except Exception as e:
+ err("Cannot copy `{p}' to `{t}':\n{e}".format(
+ p=path, t=theme_path, e=str(e)))
+
+
+def symlink(path, v=False):
+ """Symbolically link a theme"""
+ if not os.path.exists(path):
+ err(path + ' : no such file or directory')
+ elif not os.path.isdir(path):
+ err(path + ' : not a directory')
+ else:
+ theme_name = os.path.basename(os.path.normpath(path))
+ theme_path = os.path.join(_THEMES_PATH, theme_name)
+ if os.path.exists(theme_path):
+ err(path + ' : already exists')
+ else:
+ if v:
+ print("Linking `{p}' to `{t}' ...".format(
+ p=path, t=theme_path))
+ try:
+ os.symlink(path, theme_path)
+ except Exception as e:
+ err("Cannot link `{p}' to `{t}':\n{e}".format(
+ p=path, t=theme_path, e=str(e)))
+
+
+def is_broken_link(path):
+ """Returns True if the path given as is a broken symlink"""
+ path = os.readlink(path)
+ return not os.path.exists(path)
+
+
+def clean(v=False):
+ """Removes the broken symbolic links"""
+ c = 0
+ for path in os.listdir(_THEMES_PATH):
+ path = os.path.join(_THEMES_PATH, path)
+ if os.path.islink(path):
+ if is_broken_link(path):
+ if v:
+ print('Removing {}'.format(path))
+ try:
+ os.remove(path)
+ except OSError:
+ print('Error: cannot remove {}'.format(path))
+ else:
+ c += 1
+
+ print("\nRemoved {} broken links".format(c))
diff --git a/pelican/tools/templates/Makefile.jinja2 b/pelican/tools/templates/Makefile.jinja2
new file mode 100644
index 00000000..a60f1e16
--- /dev/null
+++ b/pelican/tools/templates/Makefile.jinja2
@@ -0,0 +1,161 @@
+PY?={{py_v}}
+PELICAN?={{pelican}}
+PELICANOPTS={{pelicanopts}}
+
+BASEDIR=$(CURDIR)
+INPUTDIR=$(BASEDIR)/content
+OUTPUTDIR=$(BASEDIR)/output
+CONFFILE=$(BASEDIR)/pelicanconf.py
+PUBLISHCONF=$(BASEDIR)/publishconf.py
+
+{% if ftp %}
+FTP_HOST={{ftp_host}}
+FTP_USER={{ftp_user}}
+FTP_TARGET_DIR={{ftp_target_dir}}
+
+{% endif %}
+{% if ssh %}
+SSH_HOST={{ssh_host}}
+SSH_PORT={{ssh_port}}
+SSH_USER={{ssh_user}}
+SSH_TARGET_DIR={{ssh_target_dir}}
+
+{% endif %}
+{% if s3 %}
+S3_BUCKET={{s3_bucket}}
+
+{% endif %}
+{% if cloudfiles %}
+CLOUDFILES_USERNAME={{cloudfiles_username}}
+CLOUDFILES_API_KEY={{cloudfiles_api_key}}
+CLOUDFILES_CONTAINER={{cloudfiles_container}}
+
+{% endif %}
+{% if dropbox %}
+DROPBOX_DIR={{dropbox_dir}}
+
+{% endif %}
+{% if github %}
+GITHUB_PAGES_BRANCH={{github_pages_branch}}
+
+{% endif %}
+
+DEBUG ?= 0
+ifeq ($(DEBUG), 1)
+ PELICANOPTS += -D
+endif
+
+RELATIVE ?= 0
+ifeq ($(RELATIVE), 1)
+ PELICANOPTS += --relative-urls
+endif
+
+SERVER ?= "0.0.0.0"
+
+PORT ?= 0
+ifneq ($(PORT), 0)
+ PELICANOPTS += -p $(PORT)
+endif
+
+
+help:
+ @echo 'Makefile for a pelican Web site '
+ @echo ' '
+ @echo 'Usage: '
+ @echo ' make html (re)generate the web site '
+ @echo ' make clean remove the generated files '
+ @echo ' make regenerate regenerate files upon modification '
+ @echo ' make publish generate using production settings '
+ @echo ' make serve [PORT=8000] serve site at http://localhost:8000'
+ @echo ' make serve-global [SERVER=0.0.0.0] serve (as root) to $(SERVER):80 '
+ @echo ' make devserver [PORT=8000] serve and regenerate together '
+ @echo ' make ssh_upload upload the web site via SSH '
+ @echo ' make rsync_upload upload the web site via rsync+ssh '
+{% if dropbox %}
+ @echo ' make dropbox_upload upload the web site via Dropbox '
+{% endif %}
+{% if ftp %}
+ @echo ' make ftp_upload upload the web site via FTP '
+{% endif %}
+{% if s3 %}
+ @echo ' make s3_upload upload the web site via S3 '
+{% endif %}
+{% if cloudfiles %}
+ @echo ' make cf_upload upload the web site via Cloud Files'
+{% endif %}
+{% if github %}
+ @echo ' make github upload the web site via gh-pages '
+{% endif %}
+ @echo ' '
+ @echo 'Set the DEBUG variable to 1 to enable debugging, e.g. make DEBUG=1 html '
+ @echo 'Set the RELATIVE variable to 1 to enable relative urls '
+ @echo ' '
+
+html:
+ "$(PELICAN)" "$(INPUTDIR)" -o "$(OUTPUTDIR)" -s "$(CONFFILE)" $(PELICANOPTS)
+
+clean:
+ [ ! -d "$(OUTPUTDIR)" ] || rm -rf "$(OUTPUTDIR)"
+
+regenerate:
+ "$(PELICAN)" -r "$(INPUTDIR)" -o "$(OUTPUTDIR)" -s "$(CONFFILE)" $(PELICANOPTS)
+
+serve:
+ "$(PELICAN)" -l "$(INPUTDIR)" -o "$(OUTPUTDIR)" -s "$(CONFFILE)" $(PELICANOPTS)
+
+serve-global:
+ "$(PELICAN)" -l "$(INPUTDIR)" -o "$(OUTPUTDIR)" -s "$(CONFFILE)" $(PELICANOPTS) -b $(SERVER)
+
+devserver:
+ "$(PELICAN)" -lr "$(INPUTDIR)" -o "$(OUTPUTDIR)" -s "$(CONFFILE)" $(PELICANOPTS)
+
+devserver-global:
+ $(PELICAN) -lr $(INPUTDIR) -o $(OUTPUTDIR) -s $(CONFFILE) $(PELICANOPTS) -b 0.0.0.0
+
+publish:
+ "$(PELICAN)" "$(INPUTDIR)" -o "$(OUTPUTDIR)" -s "$(PUBLISHCONF)" $(PELICANOPTS)
+
+{% set upload = [] %}
+{% if ssh %}
+{% set upload = upload + ["ssh_upload"] %}
+ssh_upload: publish
+ scp -P $(SSH_PORT) -r "$(OUTPUTDIR)"/* "$(SSH_USER)@$(SSH_HOST):$(SSH_TARGET_DIR)"
+
+{% set upload = upload + ["rsync_upload"] %}
+rsync_upload: publish
+ rsync -e "ssh -p $(SSH_PORT)" -P -rvzc --include tags --cvs-exclude --delete "$(OUTPUTDIR)"/ "$(SSH_USER)@$(SSH_HOST):$(SSH_TARGET_DIR)"
+
+{% endif %}
+{% if dropbox %}
+{% set upload = upload + ["dropbox_upload"] %}
+dropbox_upload: publish
+ cp -r "$(OUTPUTDIR)"/* "$(DROPBOX_DIR)"/
+
+{% endif %}
+{% if ftp %}
+{% set upload = upload + ["ftp_upload"] %}
+ftp_upload: publish
+ lftp ftp://$(FTP_USER)@$(FTP_HOST) -e "mirror -R $(OUTPUTDIR) $(FTP_TARGET_DIR) ; quit"
+
+{% endif %}
+{% if s3 %}
+{% set upload = upload + ["s3_upload"] %}
+s3_upload: publish
+ aws s3 sync "$(OUTPUTDIR)"/ s3://$(S3_BUCKET) --acl public-read --delete
+
+{% endif %}
+{% if cloudfiles %}
+{% set upload = upload + ["cf_upload"] %}
+cf_upload: publish
+ cd "$(OUTPUTDIR)" && swift -v -A https://auth.api.rackspacecloud.com/v1.0 -U $(CLOUDFILES_USERNAME) -K $(CLOUDFILES_API_KEY) upload -c $(CLOUDFILES_CONTAINER) .
+
+{% endif %}
+{% if github %}
+{% set upload = upload + ["github"] %}
+github: publish
+ ghp-import -m "Generate Pelican site" -b $(GITHUB_PAGES_BRANCH) "$(OUTPUTDIR)"
+ git push origin $(GITHUB_PAGES_BRANCH)
+
+{% endif %}
+
+.PHONY: html help clean regenerate serve serve-global devserver publish {{ upload|join(" ") }}
diff --git a/pelican/tools/templates/pelicanconf.py.jinja2 b/pelican/tools/templates/pelicanconf.py.jinja2
new file mode 100644
index 00000000..4ba0208c
--- /dev/null
+++ b/pelican/tools/templates/pelicanconf.py.jinja2
@@ -0,0 +1,34 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*- #
+
+AUTHOR = {{author}}
+SITENAME = {{sitename}}
+SITEURL = ''
+
+PATH = 'content'
+
+TIMEZONE = {{timezone}}
+
+DEFAULT_LANG = {{lang}}
+
+# Feed generation is usually not desired when developing
+FEED_ALL_ATOM = None
+CATEGORY_FEED_ATOM = None
+TRANSLATION_FEED_ATOM = None
+AUTHOR_FEED_ATOM = None
+AUTHOR_FEED_RSS = None
+
+# Blogroll
+LINKS = (('Pelican', 'https://getpelican.com/'),
+ ('Python.org', 'https://www.python.org/'),
+ ('Jinja2', 'https://palletsprojects.com/p/jinja/'),
+ ('You can modify those links in your config file', '#'),)
+
+# Social widget
+SOCIAL = (('You can add links in your config file', '#'),
+ ('Another social link', '#'),)
+
+DEFAULT_PAGINATION = {{default_pagination}}
+
+# Uncomment following line if you want document-relative URLs when developing
+#RELATIVE_URLS = True
diff --git a/pelican/tools/templates/publishconf.py.jinja2 b/pelican/tools/templates/publishconf.py.jinja2
new file mode 100755
index 00000000..bb18966b
--- /dev/null
+++ b/pelican/tools/templates/publishconf.py.jinja2
@@ -0,0 +1,24 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*- #
+
+# This file is only used if you use `make publish` or
+# explicitly specify it as your config file.
+
+import os
+import sys
+sys.path.append(os.curdir)
+from pelicanconf import *
+
+# If your site is available via HTTPS, make sure SITEURL begins with https://
+SITEURL = '{{siteurl}}'
+RELATIVE_URLS = False
+
+FEED_ALL_ATOM = 'feeds/all.atom.xml'
+CATEGORY_FEED_ATOM = 'feeds/{slug}.atom.xml'
+
+DELETE_OUTPUT_DIRECTORY = True
+
+# Following items are often useful when publishing
+
+#DISQUS_SITENAME = ""
+#GOOGLE_ANALYTICS = ""
diff --git a/pelican/tools/templates/tasks.py.jinja2 b/pelican/tools/templates/tasks.py.jinja2
new file mode 100644
index 00000000..b65f01d2
--- /dev/null
+++ b/pelican/tools/templates/tasks.py.jinja2
@@ -0,0 +1,156 @@
+# -*- coding: utf-8 -*-
+
+import os
+import shlex
+import shutil
+import sys
+import datetime
+
+from invoke import task
+from invoke.main import program
+from invoke.util import cd
+from pelican import main as pelican_main
+from pelican.server import ComplexHTTPRequestHandler, RootedHTTPServer
+from pelican.settings import DEFAULT_CONFIG, get_settings_from_file
+
+SETTINGS_FILE_BASE = 'pelicanconf.py'
+SETTINGS = {}
+SETTINGS.update(DEFAULT_CONFIG)
+LOCAL_SETTINGS = get_settings_from_file(SETTINGS_FILE_BASE)
+SETTINGS.update(LOCAL_SETTINGS)
+
+CONFIG = {
+ 'settings_base': SETTINGS_FILE_BASE,
+ 'settings_publish': 'publishconf.py',
+ # Output path. Can be absolute or relative to tasks.py. Default: 'output'
+ 'deploy_path': SETTINGS['OUTPUT_PATH'],
+{% if ssh %}
+ # Remote server configuration
+ 'ssh_user': '{{ssh_user}}',
+ 'ssh_host': '{{ssh_host}}',
+ 'ssh_port': '{{ssh_port}}',
+ 'ssh_path': '{{ssh_target_dir}}',
+{% endif %}
+{% if cloudfiles %}
+ # Rackspace Cloud Files configuration settings
+ 'cloudfiles_username': '{{cloudfiles_username}}',
+ 'cloudfiles_api_key': '{{cloudfiles_api_key}}',
+ 'cloudfiles_container': '{{cloudfiles_container}}',
+{% endif %}
+{% if github %}
+ # Github Pages configuration
+ 'github_pages_branch': '{{github_pages_branch}}',
+ 'commit_message': "'Publish site on {}'".format(datetime.date.today().isoformat()),
+{% endif %}
+ # Host and port for `serve`
+ 'host': 'localhost',
+ 'port': 8000,
+}
+
+@task
+def clean(c):
+ """Remove generated files"""
+ if os.path.isdir(CONFIG['deploy_path']):
+ shutil.rmtree(CONFIG['deploy_path'])
+ os.makedirs(CONFIG['deploy_path'])
+
+@task
+def build(c):
+ """Build local version of site"""
+ pelican_run('-s {settings_base}'.format(**CONFIG))
+
+@task
+def rebuild(c):
+ """`build` with the delete switch"""
+ pelican_run('-d -s {settings_base}'.format(**CONFIG))
+
+@task
+def regenerate(c):
+ """Automatically regenerate site upon file modification"""
+ pelican_run('-r -s {settings_base}'.format(**CONFIG))
+
+@task
+def serve(c):
+ """Serve site at http://$HOST:$PORT/ (default is localhost:8000)"""
+
+ class AddressReuseTCPServer(RootedHTTPServer):
+ allow_reuse_address = True
+
+ server = AddressReuseTCPServer(
+ CONFIG['deploy_path'],
+ (CONFIG['host'], CONFIG['port']),
+ ComplexHTTPRequestHandler)
+
+ sys.stderr.write('Serving at {host}:{port} ...\n'.format(**CONFIG))
+ server.serve_forever()
+
+@task
+def reserve(c):
+ """`build`, then `serve`"""
+ build(c)
+ serve(c)
+
+@task
+def preview(c):
+ """Build production version of site"""
+ pelican_run('-s {settings_publish}'.format(**CONFIG))
+
+@task
+def livereload(c):
+ """Automatically reload browser tab upon file modification."""
+ from livereload import Server
+ build(c)
+ server = Server()
+ # Watch the base settings file
+ server.watch(CONFIG['settings_base'], lambda: build(c))
+ # Watch content source files
+ content_file_extensions = ['.md', '.rst']
+ for extension in content_file_extensions:
+ content_blob = '{0}/**/*{1}'.format(SETTINGS['PATH'], extension)
+ server.watch(content_blob, lambda: build(c))
+ # Watch the theme's templates and static assets
+ theme_path = SETTINGS['THEME']
+ server.watch('{}/templates/*.html'.format(theme_path), lambda: build(c))
+ static_file_extensions = ['.css', '.js']
+ for extension in static_file_extensions:
+ static_file = '{0}/static/**/*{1}'.format(theme_path, extension)
+ server.watch(static_file, lambda: build(c))
+ # Serve output path on configured host and port
+ server.serve(host=CONFIG['host'], port=CONFIG['port'], root=CONFIG['deploy_path'])
+
+{% if cloudfiles %}
+@task
+def cf_upload(c):
+ """Publish to Rackspace Cloud Files"""
+ rebuild(c)
+ with cd(CONFIG['deploy_path']):
+ c.run('swift -v -A https://auth.api.rackspacecloud.com/v1.0 '
+ '-U {cloudfiles_username} '
+ '-K {cloudfiles_api_key} '
+ 'upload -c {cloudfiles_container} .'.format(**CONFIG))
+{% endif %}
+
+@task
+def publish(c):
+ """Publish to production via rsync"""
+ pelican_run('-s {settings_publish}'.format(**CONFIG))
+ c.run(
+ 'rsync --delete --exclude ".DS_Store" -pthrvz -c '
+ '-e "ssh -p {ssh_port}" '
+ '{} {ssh_user}@{ssh_host}:{ssh_path}'.format(
+ CONFIG['deploy_path'].rstrip('/') + '/',
+ **CONFIG))
+
+{% if github %}
+@task
+def gh_pages(c):
+ """Publish to GitHub Pages"""
+ preview(c)
+ c.run('ghp-import -b {github_pages_branch} '
+ '-m {commit_message} '
+ '{deploy_path} -p'.format(**CONFIG))
+{% endif %}
+
+def pelican_run(cmd):
+ cmd += ' ' + program.core.remainder # allows to pass-through args to pelican
+ pelican_main(shlex.split(cmd))
diff --git a/pelican/urlwrappers.py b/pelican/urlwrappers.py
new file mode 100644
index 00000000..efe09fbc
--- /dev/null
+++ b/pelican/urlwrappers.py
@@ -0,0 +1,138 @@
+import functools
+import logging
+import os
+
+from pelican.utils import slugify
+
+logger = logging.getLogger(__name__)
+
+
+@functools.total_ordering
+class URLWrapper:
+ def __init__(self, name, settings):
+ self.settings = settings
+ self._name = name
+ self._slug = None
+ self._slug_from_name = True
+
+ @property
+ def name(self):
+ return self._name
+
+ @name.setter
+ def name(self, name):
+ self._name = name
+ # if slug wasn't explicitly set, it needs to be regenerated from name
+ # so, changing name should reset slug for slugification
+ if self._slug_from_name:
+ self._slug = None
+
+ @property
+ def slug(self):
+ if self._slug is None:
+ class_key = '{}_REGEX_SUBSTITUTIONS'.format(
+ self.__class__.__name__.upper())
+ regex_subs = self.settings.get(
+ class_key,
+ self.settings.get('SLUG_REGEX_SUBSTITUTIONS', []))
+ preserve_case = self.settings.get('SLUGIFY_PRESERVE_CASE', False)
+ self._slug = slugify(
+ self.name,
+ regex_subs=regex_subs,
+ preserve_case=preserve_case,
+ use_unicode=self.settings.get('SLUGIFY_USE_UNICODE', False)
+ )
+ return self._slug
+
+ @slug.setter
+ def slug(self, slug):
+ # if slug is expliticly set, changing name won't alter slug
+ self._slug_from_name = False
+ self._slug = slug
+
+ def as_dict(self):
+ d = self.__dict__
+ d['name'] = self.name
+ d['slug'] = self.slug
+ return d
+
+ def __hash__(self):
+ return hash(self.slug)
+
+ def _normalize_key(self, key):
+ class_key = '{}_REGEX_SUBSTITUTIONS'.format(
+ self.__class__.__name__.upper())
+ regex_subs = self.settings.get(
+ class_key,
+ self.settings.get('SLUG_REGEX_SUBSTITUTIONS', []))
+ use_unicode = self.settings.get('SLUGIFY_USE_UNICODE', False)
+ preserve_case = self.settings.get('SLUGIFY_PRESERVE_CASE', False)
+ return slugify(
+ key,
+ regex_subs=regex_subs,
+ preserve_case=preserve_case,
+ use_unicode=use_unicode)
+
+ def __eq__(self, other):
+ if isinstance(other, self.__class__):
+ return self.slug == other.slug
+ if isinstance(other, str):
+ return self.slug == self._normalize_key(other)
+ return False
+
+ def __ne__(self, other):
+ if isinstance(other, self.__class__):
+ return self.slug != other.slug
+ if isinstance(other, str):
+ return self.slug != self._normalize_key(other)
+ return True
+
+ def __lt__(self, other):
+ if isinstance(other, self.__class__):
+ return self.slug < other.slug
+ if isinstance(other, str):
+ return self.slug < self._normalize_key(other)
+ return False
+
+ def __str__(self):
+ return self.name
+
+ def __repr__(self):
+ return '<{} {}>'.format(type(self).__name__, repr(self._name))
+
+ def _from_settings(self, key, get_page_name=False):
+ """Returns URL information as defined in settings.
+
+ When get_page_name=True returns URL without anything after {slug} e.g.
+ if in settings: CATEGORY_URL="cat/{slug}.html" this returns
+ "cat/{slug}" Useful for pagination.
+
+ """
+ setting = "{}_{}".format(self.__class__.__name__.upper(), key)
+ value = self.settings[setting]
+ if not isinstance(value, str):
+ logger.warning('%s is set to %s', setting, value)
+ return value
+ else:
+ if get_page_name:
+ return os.path.splitext(value)[0].format(**self.as_dict())
+ else:
+ return value.format(**self.as_dict())
+
+ page_name = property(functools.partial(_from_settings, key='URL',
+ get_page_name=True))
+ url = property(functools.partial(_from_settings, key='URL'))
+ save_as = property(functools.partial(_from_settings, key='SAVE_AS'))
+
+
+class Category(URLWrapper):
+ pass
+
+
+class Tag(URLWrapper):
+ def __init__(self, name, *args, **kwargs):
+ super().__init__(name.strip(), *args, **kwargs)
+
+
+class Author(URLWrapper):
+ pass
diff --git a/pelican/utils.py b/pelican/utils.py
new file mode 100644
index 00000000..e82117d3
--- /dev/null
+++ b/pelican/utils.py
@@ -0,0 +1,968 @@
+import datetime
+import fnmatch
+import locale
+import logging
+import os
+import re
+import shutil
+import sys
+import traceback
+import urllib
+from collections.abc import Hashable
+from contextlib import contextmanager
+from functools import partial
+from html import entities
+from html.parser import HTMLParser
+from itertools import groupby
+from operator import attrgetter
+
+import dateutil.parser
+
+from jinja2 import Markup
+
+import pytz
+
+
+logger = logging.getLogger(__name__)
+
+
+def sanitised_join(base_directory, *parts):
+ joined = posixize_path(
+ os.path.abspath(os.path.join(base_directory, *parts)))
+ base = posixize_path(os.path.abspath(base_directory))
+ if not joined.startswith(base):
+ raise RuntimeError(
+ "Attempted to break out of output directory to {}".format(
+ joined
+ )
+ )
+
+ return joined
+
+
+def strftime(date, date_format):
+ '''
+ Enhanced replacement for built-in strftime with zero stripping
+
+ This works by 'grabbing' possible format strings (those starting with %),
+ formatting them with the date, stripping any leading zeros if - prefix is
+ used and replacing formatted output back.
+ '''
+ def strip_zeros(x):
+ return x.lstrip('0') or '0'
+ c89_directives = 'aAbBcdfHIjmMpSUwWxXyYzZ%'
+
+ # grab candidate format options
+ format_options = '%[-]?.'
+ candidates = re.findall(format_options, date_format)
+
+ # replace candidates with placeholders for later % formatting
+ template = re.sub(format_options, '%s', date_format)
+
+ formatted_candidates = []
+ for candidate in candidates:
+ # test for valid C89 directives only
+ if candidate[-1] in c89_directives:
+ # check for '-' prefix
+ if len(candidate) == 3:
+ # '-' prefix
+ candidate = '%{}'.format(candidate[-1])
+ conversion = strip_zeros
+ else:
+ conversion = None
+
+ # format date
+ if isinstance(date, SafeDatetime):
+ formatted = date.strftime(candidate, safe=False)
+ else:
+ formatted = date.strftime(candidate)
+
+ # strip zeros if '-' prefix is used
+ if conversion:
+ formatted = conversion(formatted)
+ else:
+ formatted = candidate
+ formatted_candidates.append(formatted)
+
+ # put formatted candidates back and return
+ return template % tuple(formatted_candidates)
+
+
+class SafeDatetime(datetime.datetime):
+ '''Subclass of datetime that works with utf-8 format strings on PY2'''
+
+ def strftime(self, fmt, safe=True):
+ '''Uses our custom strftime if supposed to be *safe*'''
+ if safe:
+ return strftime(self, fmt)
+ else:
+ return super().strftime(fmt)
+
+
+class DateFormatter:
+ '''A date formatter object used as a jinja filter
+
+ Uses the `strftime` implementation and makes sure jinja uses the locale
+ defined in LOCALE setting
+ '''
+
+ def __init__(self):
+ self.locale = locale.setlocale(locale.LC_TIME)
+
+ def __call__(self, date, date_format):
+ old_lc_time = locale.setlocale(locale.LC_TIME)
+ old_lc_ctype = locale.setlocale(locale.LC_CTYPE)
+
+ locale.setlocale(locale.LC_TIME, self.locale)
+ # on OSX, encoding from LC_CTYPE determines the unicode output in PY3
+ # make sure it's same as LC_TIME
+ locale.setlocale(locale.LC_CTYPE, self.locale)
+
+ formatted = strftime(date, date_format)
+
+ locale.setlocale(locale.LC_TIME, old_lc_time)
+ locale.setlocale(locale.LC_CTYPE, old_lc_ctype)
+ return formatted
+
+
+class memoized:
+ """Function decorator to cache return values.
+
+ If called later with the same arguments, the cached value is returned
+ (not reevaluated).
+
+ """
+ def __init__(self, func):
+ self.func = func
+ self.cache = {}
+
+ def __call__(self, *args):
+ if not isinstance(args, Hashable):
+ # uncacheable. a list, for instance.
+ # better to not cache than blow up.
+ return self.func(*args)
+ if args in self.cache:
+ return self.cache[args]
+ else:
+ value = self.func(*args)
+ self.cache[args] = value
+ return value
+
+ def __repr__(self):
+ return self.func.__doc__
+
+ def __get__(self, obj, objtype):
+ '''Support instance methods.'''
+ return partial(self.__call__, obj)
+
+
+def deprecated_attribute(old, new, since=None, remove=None, doc=None):
+ """Attribute deprecation decorator for gentle upgrades
+
+ For example:
+
+ class MyClass (object):
+ @deprecated_attribute(
+ old='abc', new='xyz', since=(3, 2, 0), remove=(4, 1, 3))
+ def abc(): return None
+
+ def __init__(self):
+ xyz = 5
+
+ Note that the decorator needs a dummy method to attach to, but the
+ content of the dummy method is ignored.
+ """
+ def _warn():
+ version = '.'.join(str(x) for x in since)
+ message = ['{} has been deprecated since {}'.format(old, version)]
+ if remove:
+ version = '.'.join(str(x) for x in remove)
+ message.append(
+ ' and will be removed by version {}'.format(version))
+ message.append('. Use {} instead.'.format(new))
+ logger.warning(''.join(message))
+ logger.debug(''.join(str(x) for x
+ in traceback.format_stack()))
+
+ def fget(self):
+ _warn()
+ return getattr(self, new)
+
+ def fset(self, value):
+ _warn()
+ setattr(self, new, value)
+
+ def decorator(dummy):
+ return property(fget=fget, fset=fset, doc=doc)
+
+ return decorator
+
+
+def get_date(string):
+ """Return a datetime object from a string.
+
+ If no format matches the given date, raise a ValueError.
+ """
+ string = re.sub(' +', ' ', string)
+ default = SafeDatetime.now().replace(hour=0, minute=0,
+ second=0, microsecond=0)
+ try:
+ return dateutil.parser.parse(string, default=default)
+ except (TypeError, ValueError):
+ raise ValueError('{!r} is not a valid date'.format(string))
+
+
+@contextmanager
+def pelican_open(filename, mode='r', strip_crs=(sys.platform == 'win32')):
+ """Open a file and return its content"""
+
+ # utf-8-sig will clear any BOM if present
+ with open(filename, mode, encoding='utf-8-sig') as infile:
+ content = infile.read()
+ yield content
+
+
+def slugify(value, regex_subs=(), preserve_case=False, use_unicode=False):
+ """
+ Normalizes string, converts to lowercase, removes non-alpha characters,
+ and converts spaces to hyphens.
+
+ Took from Django sources.
+ """
+
+ import unicodedata
+ import unidecode
+
+ def normalize_unicode(text):
+ # normalize text by compatibility composition
+ # see: https://en.wikipedia.org/wiki/Unicode_equivalence
+ return unicodedata.normalize('NFKC', text)
+
+ # strip tags from value
+ value = Markup(value).striptags()
+
+ # normalization
+ value = normalize_unicode(value)
+
+ if not use_unicode:
+ # ASCII-fy
+ value = unidecode.unidecode(value)
+
+ # perform regex substitutions
+ for src, dst in regex_subs:
+ value = re.sub(
+ normalize_unicode(src),
+ normalize_unicode(dst),
+ value,
+ flags=re.IGNORECASE)
+
+ if not preserve_case:
+ value = value.lower()
+
+ return value.strip()
+
+
+def copy(source, destination, ignores=None):
+ """Recursively copy source into destination.
+
+ If source is a file, destination has to be a file as well.
+ The function is able to copy either files or directories.
+
+ :param source: the source file or directory
+ :param destination: the destination file or directory
+ :param ignores: either None, or a list of glob patterns;
+ files matching those patterns will _not_ be copied.
+ """
+
+ def walk_error(err):
+ logger.warning("While copying %s: %s: %s",
+ source_, err.filename, err.strerror)
+
+ source_ = os.path.abspath(os.path.expanduser(source))
+ destination_ = os.path.abspath(os.path.expanduser(destination))
+
+ if ignores is None:
+ ignores = []
+
+ if any(fnmatch.fnmatch(os.path.basename(source), ignore)
+ for ignore in ignores):
+ logger.info('Not copying %s due to ignores', source_)
+ return
+
+ if os.path.isfile(source_):
+ dst_dir = os.path.dirname(destination_)
+ if not os.path.exists(dst_dir):
+ logger.info('Creating directory %s', dst_dir)
+ os.makedirs(dst_dir)
+ logger.info('Copying %s to %s', source_, destination_)
+ copy_file_metadata(source_, destination_)
+
+ elif os.path.isdir(source_):
+ if not os.path.exists(destination_):
+ logger.info('Creating directory %s', destination_)
+ os.makedirs(destination_)
+ if not os.path.isdir(destination_):
+ logger.warning('Cannot copy %s (a directory) to %s (a file)',
+ source_, destination_)
+ return
+
+ for src_dir, subdirs, others in os.walk(source_, followlinks=True):
+ dst_dir = os.path.join(destination_,
+ os.path.relpath(src_dir, source_))
+
+ subdirs[:] = (s for s in subdirs if not any(fnmatch.fnmatch(s, i)
+ for i in ignores))
+ others[:] = (o for o in others if not any(fnmatch.fnmatch(o, i)
+ for i in ignores))
+
+ if not os.path.isdir(dst_dir):
+ logger.info('Creating directory %s', dst_dir)
+ # Parent directories are known to exist, so 'mkdir' suffices.
+ os.mkdir(dst_dir)
+
+ for o in others:
+ src_path = os.path.join(src_dir, o)
+ dst_path = os.path.join(dst_dir, o)
+ if os.path.isfile(src_path):
+ logger.info('Copying %s to %s', src_path, dst_path)
+ copy_file_metadata(src_path, dst_path)
+ else:
+ logger.warning('Skipped copy %s (not a file or '
+ 'directory) to %s',
+ src_path, dst_path)
+
+
+def copy_file_metadata(source, destination):
+ '''Copy a file and its metadata (perm bits, access times, ...)'''
+
+ # This function is a workaround for Android python copystat
+ # bug ([issue28141]) https://bugs.python.org/issue28141
+ try:
+ shutil.copy2(source, destination)
+ except OSError as e:
+ logger.warning("A problem occurred copying file %s to %s; %s",
+ source, destination, e)
+
+
+def clean_output_dir(path, retention):
+ """Remove all files from output directory except those in retention list"""
+
+ if not os.path.exists(path):
+ logger.debug("Directory already removed: %s", path)
+ return
+
+ if not os.path.isdir(path):
+ try:
+ os.remove(path)
+ except Exception as e:
+ logger.error("Unable to delete file %s; %s", path, e)
+ return
+
+ # remove existing content from output folder unless in retention list
+ for filename in os.listdir(path):
+ file = os.path.join(path, filename)
+ if any(filename == retain for retain in retention):
+ logger.debug("Skipping deletion; %s is on retention list: %s",
+ filename, file)
+ elif os.path.isdir(file):
+ try:
+ shutil.rmtree(file)
+ logger.debug("Deleted directory %s", file)
+ except Exception as e:
+ logger.error("Unable to delete directory %s; %s",
+ file, e)
+ elif os.path.isfile(file) or os.path.islink(file):
+ try:
+ os.remove(file)
+ logger.debug("Deleted file/link %s", file)
+ except Exception as e:
+ logger.error("Unable to delete file %s; %s", file, e)
+ else:
+ logger.error("Unable to delete %s, file type unknown", file)
+
+
+def get_relative_path(path):
+ """Return the relative path from the given path to the root path."""
+ components = split_all(path)
+ if len(components) <= 1:
+ return os.curdir
+ else:
+ parents = [os.pardir] * (len(components) - 1)
+ return os.path.join(*parents)
+
+
+def path_to_url(path):
+ """Return the URL corresponding to a given path."""
+ if path is not None:
+ path = posixize_path(path)
+ return path
+
+
+def posixize_path(rel_path):
+ """Use '/' as path separator, so that source references,
+ like '{static}/foo/bar.jpg' or 'extras/favicon.ico',
+ will work on Windows as well as on Mac and Linux."""
+ return rel_path.replace(os.sep, '/')
+
+
+class _HTMLWordTruncator(HTMLParser):
+
+ _word_regex = re.compile(r"\w[\w'-]*", re.U)
+ _word_prefix_regex = re.compile(r'\w', re.U)
+ _singlets = ('br', 'col', 'link', 'base', 'img', 'param', 'area',
+ 'hr', 'input')
+
+ class TruncationCompleted(Exception):
+
+ def __init__(self, truncate_at):
+ super().__init__(truncate_at)
+ self.truncate_at = truncate_at
+
+ def __init__(self, max_words):
+ super().__init__(convert_charrefs=False)
+
+ self.max_words = max_words
+ self.words_found = 0
+ self.open_tags = []
+ self.last_word_end = None
+ self.truncate_at = None
+
+ def feed(self, *args, **kwargs):
+ try:
+ super().feed(*args, **kwargs)
+ except self.TruncationCompleted as exc:
+ self.truncate_at = exc.truncate_at
+ else:
+ self.truncate_at = None
+
+ def getoffset(self):
+ line_start = 0
+ lineno, line_offset = self.getpos()
+ for i in range(lineno - 1):
+ line_start = self.rawdata.index('\n', line_start) + 1
+ return line_start + line_offset
+
+ def add_word(self, word_end):
+ self.words_found += 1
+ self.last_word_end = None
+ if self.words_found == self.max_words:
+ raise self.TruncationCompleted(word_end)
+
+ def add_last_word(self):
+ if self.last_word_end is not None:
+ self.add_word(self.last_word_end)
+
+ def handle_starttag(self, tag, attrs):
+ self.add_last_word()
+ if tag not in self._singlets:
+ self.open_tags.insert(0, tag)
+
+ def handle_endtag(self, tag):
+ self.add_last_word()
+ try:
+ i = self.open_tags.index(tag)
+ except ValueError:
+ pass
+ else:
+ # SGML: An end tag closes, back to the matching start tag,
+ # all unclosed intervening start tags with omitted end tags
+ del self.open_tags[:i + 1]
+
+ def handle_data(self, data):
+ word_end = 0
+ offset = self.getoffset()
+
+ while self.words_found < self.max_words:
+ match = self._word_regex.search(data, word_end)
+ if not match:
+ break
+
+ if match.start(0) > 0:
+ self.add_last_word()
+
+ word_end = match.end(0)
+ self.last_word_end = offset + word_end
+
+ if word_end < len(data):
+ self.add_last_word()
+
+ def _handle_ref(self, name, char):
+ """
+ Called by handle_entityref() or handle_charref() when a ref like
+ `—`, `—`, or `—` is found.
+
+ The arguments for this method are:
+
+ - `name`: the HTML entity name (such as `mdash` or `#8212` or `#x2014`)
+ - `char`: the Unicode representation of the ref (such as `—`)
+
+ This method checks whether the entity is considered to be part of a
+ word or not and, if not, signals the end of a word.
+ """
+ # Compute the index of the character right after the ref.
+ #
+ # In a string like 'prefix—suffix', the end is the sum of:
+ #
+ # - `self.getoffset()` (the length of `prefix`)
+ # - `1` (the length of `&`)
+ # - `len(name)` (the length of `mdash`)
+ # - `1` (the length of `;`)
+ #
+ # Note that, in case of malformed HTML, the ';' character may
+ # not be present.
+
+ offset = self.getoffset()
+ ref_end = offset + len(name) + 1
+
+ try:
+ if self.rawdata[ref_end] == ';':
+ ref_end += 1
+ except IndexError:
+ # We are at the end of the string and there's no ';'
+ pass
+
+ if self.last_word_end is None:
+ if self._word_prefix_regex.match(char):
+ self.last_word_end = ref_end
+ else:
+ if self._word_regex.match(char):
+ self.last_word_end = ref_end
+ else:
+ self.add_last_word()
+
+ def handle_entityref(self, name):
+ """
+ Called when an entity ref like '—' is found
+
+ `name` is the entity ref without ampersand and semicolon (e.g. `mdash`)
+ """
+ try:
+ codepoint = entities.name2codepoint[name]
+ char = chr(codepoint)
+ except KeyError:
+ char = ''
+ self._handle_ref(name, char)
+
+ def handle_charref(self, name):
+ """
+ Called when a char ref like '—' or '—' is found
+
+ `name` is the char ref without ampersand and semicolon (e.g. `#8212` or
+ `#x2014`)
+ """
+ try:
+ if name.startswith('x'):
+ codepoint = int(name[1:], 16)
+ else:
+ codepoint = int(name)
+ char = chr(codepoint)
+ except (ValueError, OverflowError):
+ char = ''
+ self._handle_ref('#' + name, char)
+
+
+def truncate_html_words(s, num, end_text='…'):
+ """Truncates HTML to a certain number of words.
+
+ (not counting tags and comments). Closes opened tags if they were correctly
+ closed in the given html. Takes an optional argument of what should be used
+ to notify that the string has been truncated, defaulting to ellipsis (…).
+
+ Newlines in the HTML are preserved. (From the django framework).
+ """
+ length = int(num)
+ if length <= 0:
+ return ''
+ truncator = _HTMLWordTruncator(length)
+ truncator.feed(s)
+ if truncator.truncate_at is None:
+ return s
+ out = s[:truncator.truncate_at]
+ if end_text:
+ out += ' ' + end_text
+ # Close any tags still open
+ for tag in truncator.open_tags:
+ out += '%s>' % tag
+ # Return string
+ return out
+
+
+def process_translations(content_list, translation_id=None):
+ """ Finds translations and returns them.
+
+ For each content_list item, populates the 'translations' attribute, and
+ returns a tuple with two lists (index, translations). Index list includes
+ items in default language or items which have no variant in default
+ language. Items with the `translation` metadata set to something else than
+ `False` or `false` will be used as translations, unless all the items in
+ the same group have that metadata.
+
+ Translations and original items are determined relative to one another
+ amongst items in the same group. Items are in the same group if they
+ have the same value(s) for the metadata attribute(s) specified by the
+ 'translation_id', which must be a string or a collection of strings.
+ If 'translation_id' is falsy, the identification of translations is skipped
+ and all items are returned as originals.
+ """
+
+ if not translation_id:
+ return content_list, []
+
+ if isinstance(translation_id, str):
+ translation_id = {translation_id}
+
+ index = []
+
+ try:
+ content_list.sort(key=attrgetter(*translation_id))
+ except TypeError:
+ raise TypeError('Cannot unpack {}, \'translation_id\' must be falsy, a'
+ ' string or a collection of strings'
+ .format(translation_id))
+ except AttributeError:
+ raise AttributeError('Cannot use {} as \'translation_id\', there '
+ 'appear to be items without these metadata '
+ 'attributes'.format(translation_id))
+
+ for id_vals, items in groupby(content_list, attrgetter(*translation_id)):
+ # prepare warning string
+ id_vals = (id_vals,) if len(translation_id) == 1 else id_vals
+ with_str = 'with' + ', '.join([' {} "{{}}"'] * len(translation_id))\
+ .format(*translation_id).format(*id_vals)
+
+ items = list(items)
+ original_items = get_original_items(items, with_str)
+ index.extend(original_items)
+ for a in items:
+ a.translations = [x for x in items if x != a]
+
+ translations = [x for x in content_list if x not in index]
+
+ return index, translations
+
+
+def get_original_items(items, with_str):
+ def _warn_source_paths(msg, items, *extra):
+ args = [len(items)]
+ args.extend(extra)
+ args.extend(x.source_path for x in items)
+ logger.warning('{}: {}'.format(msg, '\n%s' * len(items)), *args)
+
+ # warn if several items have the same lang
+ for lang, lang_items in groupby(items, attrgetter('lang')):
+ lang_items = list(lang_items)
+ if len(lang_items) > 1:
+ _warn_source_paths('There are %s items "%s" with lang %s',
+ lang_items, with_str, lang)
+
+ # items with `translation` metadata will be used as translations...
+ candidate_items = [
+ i for i in items
+ if i.metadata.get('translation', 'false').lower() == 'false']
+
+ # ...unless all items with that slug are translations
+ if not candidate_items:
+ _warn_source_paths('All items ("%s") "%s" are translations',
+ items, with_str)
+ candidate_items = items
+
+ # find items with default language
+ original_items = [i for i in candidate_items if i.in_default_lang]
+
+ # if there is no article with default language, go back one step
+ if not original_items:
+ original_items = candidate_items
+
+ # warn if there are several original items
+ if len(original_items) > 1:
+ _warn_source_paths('There are %s original (not translated) items %s',
+ original_items, with_str)
+ return original_items
+
+
+def order_content(content_list, order_by='slug'):
+ """ Sorts content.
+
+ order_by can be a string of an attribute or sorting function. If order_by
+ is defined, content will be ordered by that attribute or sorting function.
+ By default, content is ordered by slug.
+
+ Different content types can have default order_by attributes defined
+ in settings, e.g. PAGES_ORDER_BY='sort-order', in which case `sort-order`
+ should be a defined metadata attribute in each page.
+ """
+
+ if order_by:
+ if callable(order_by):
+ try:
+ content_list.sort(key=order_by)
+ except Exception:
+ logger.error('Error sorting with function %s', order_by)
+ elif isinstance(order_by, str):
+ if order_by.startswith('reversed-'):
+ order_reversed = True
+ order_by = order_by.replace('reversed-', '', 1)
+ else:
+ order_reversed = False
+
+ if order_by == 'basename':
+ content_list.sort(
+ key=lambda x: os.path.basename(x.source_path or ''),
+ reverse=order_reversed)
+ else:
+ try:
+ content_list.sort(key=attrgetter(order_by),
+ reverse=order_reversed)
+ except AttributeError:
+ for content in content_list:
+ try:
+ getattr(content, order_by)
+ except AttributeError:
+ logger.warning(
+ 'There is no "%s" attribute in "%s". '
+ 'Defaulting to slug order.',
+ order_by,
+ content.get_relative_source_path(),
+ extra={
+ 'limit_msg': ('More files are missing '
+ 'the needed attribute.')
+ })
+ else:
+ logger.warning(
+ 'Invalid *_ORDER_BY setting (%s). '
+ 'Valid options are strings and functions.', order_by)
+
+ return content_list
+
+
+class FileSystemWatcher:
+ def __init__(self, settings_file, reader_class, settings=None):
+ self.watchers = {
+ 'settings': FileSystemWatcher.file_watcher(settings_file)
+ }
+
+ self.settings = None
+ self.reader_class = reader_class
+ self._extensions = None
+ self._content_path = None
+ self._theme_path = None
+ self._ignore_files = None
+
+ if settings is not None:
+ self.update_watchers(settings)
+
+ def update_watchers(self, settings):
+ new_extensions = set(self.reader_class(settings).extensions)
+ new_content_path = settings.get('PATH', '')
+ new_theme_path = settings.get('THEME', '')
+ new_ignore_files = set(settings.get('IGNORE_FILES', []))
+
+ extensions_changed = new_extensions != self._extensions
+ content_changed = new_content_path != self._content_path
+ theme_changed = new_theme_path != self._theme_path
+ ignore_changed = new_ignore_files != self._ignore_files
+
+ # Refresh content watcher if related settings changed
+ if extensions_changed or content_changed or ignore_changed:
+ self.add_watcher('content',
+ new_content_path,
+ new_extensions,
+ new_ignore_files)
+
+ # Refresh theme watcher if related settings changed
+ if theme_changed or ignore_changed:
+ self.add_watcher('theme',
+ new_theme_path,
+ [''],
+ new_ignore_files)
+
+ # Watch STATIC_PATHS
+ old_static_watchers = set(key
+ for key in self.watchers
+ if key.startswith('[static]'))
+
+ for path in settings.get('STATIC_PATHS', []):
+ key = '[static]{}'.format(path)
+ if ignore_changed or (key not in self.watchers):
+ self.add_watcher(
+ key,
+ os.path.join(new_content_path, path),
+ [''],
+ new_ignore_files)
+ if key in old_static_watchers:
+ old_static_watchers.remove(key)
+
+ # cleanup removed static watchers
+ for key in old_static_watchers:
+ del self.watchers[key]
+
+ # update values
+ self.settings = settings
+ self._extensions = new_extensions
+ self._content_path = new_content_path
+ self._theme_path = new_theme_path
+ self._ignore_files = new_ignore_files
+
+ def check(self):
+ '''return a key:watcher_status dict for all watchers'''
+ result = {key: next(watcher) for key, watcher in self.watchers.items()}
+
+ # Various warnings
+ if result.get('content') is None:
+ reader_descs = sorted(
+ {
+ '%s (%s)' % (type(r).__name__, ', '.join(r.file_extensions))
+ for r in self.reader_class(self.settings).readers.values()
+ if r.enabled
+ }
+ )
+ logger.warning(
+ 'No valid files found in content for the active readers:\n'
+ + '\n'.join(reader_descs))
+
+ if result.get('theme') is None:
+ logger.warning('Empty theme folder. Using `basic` theme.')
+
+ return result
+
+ def add_watcher(self, key, path, extensions=[''], ignores=[]):
+ watcher = self.get_watcher(path, extensions, ignores)
+ if watcher is not None:
+ self.watchers[key] = watcher
+
+ def get_watcher(self, path, extensions=[''], ignores=[]):
+ '''return a watcher depending on path type (file or folder)'''
+ if not os.path.exists(path):
+ logger.warning("Watched path does not exist: %s", path)
+ return None
+
+ if os.path.isdir(path):
+ return self.folder_watcher(path, extensions, ignores)
+ else:
+ return self.file_watcher(path)
+
+ @staticmethod
+ def folder_watcher(path, extensions, ignores=[]):
+ '''Generator for monitoring a folder for modifications.
+
+ Returns a boolean indicating if files are changed since last check.
+ Returns None if there are no matching files in the folder'''
+
+ def file_times(path):
+ '''Return `mtime` for each file in path'''
+
+ for root, dirs, files in os.walk(path, followlinks=True):
+ dirs[:] = [x for x in dirs if not x.startswith(os.curdir)]
+
+ for f in files:
+ valid_extension = f.endswith(tuple(extensions))
+ file_ignored = any(
+ fnmatch.fnmatch(f, ignore) for ignore in ignores
+ )
+ if valid_extension and not file_ignored:
+ try:
+ yield os.stat(os.path.join(root, f)).st_mtime
+ except OSError as e:
+ logger.warning('Caught Exception: %s', e)
+
+ LAST_MTIME = 0
+ while True:
+ try:
+ mtime = max(file_times(path))
+ if mtime > LAST_MTIME:
+ LAST_MTIME = mtime
+ yield True
+ except ValueError:
+ yield None
+ else:
+ yield False
+
+ @staticmethod
+ def file_watcher(path):
+ '''Generator for monitoring a file for modifications'''
+ LAST_MTIME = 0
+ while True:
+ if path:
+ try:
+ mtime = os.stat(path).st_mtime
+ except OSError as e:
+ logger.warning('Caught Exception: %s', e)
+ continue
+
+ if mtime > LAST_MTIME:
+ LAST_MTIME = mtime
+ yield True
+ else:
+ yield False
+ else:
+ yield None
+
+
+def set_date_tzinfo(d, tz_name=None):
+ """Set the timezone for dates that don't have tzinfo"""
+ if tz_name and not d.tzinfo:
+ tz = pytz.timezone(tz_name)
+ d = tz.localize(d)
+ return SafeDatetime(d.year, d.month, d.day, d.hour, d.minute, d.second,
+ d.microsecond, d.tzinfo)
+ return d
+
+
+def mkdir_p(path):
+ os.makedirs(path, exist_ok=True)
+
+
+def split_all(path):
+ """Split a path into a list of components
+
+ While os.path.split() splits a single component off the back of
+ `path`, this function splits all components:
+
+ >>> split_all(os.path.join('a', 'b', 'c'))
+ ['a', 'b', 'c']
+ """
+ components = []
+ path = path.lstrip('/')
+ while path:
+ head, tail = os.path.split(path)
+ if tail:
+ components.insert(0, tail)
+ elif head == path:
+ components.insert(0, head)
+ break
+ path = head
+ return components
+
+
+def is_selected_for_writing(settings, path):
+ '''Check whether path is selected for writing
+ according to the WRITE_SELECTED list
+
+ If WRITE_SELECTED is an empty list (default),
+ any path is selected for writing.
+ '''
+ if settings['WRITE_SELECTED']:
+ return path in settings['WRITE_SELECTED']
+ else:
+ return True
+
+
+def path_to_file_url(path):
+ '''Convert file-system path to file:// URL'''
+ return urllib.parse.urljoin("file://", urllib.request.pathname2url(path))
+
+
+def maybe_pluralize(count, singular, plural):
+ '''
+ Returns a formatted string containing count and plural if count is not 1
+ Returns count and singular if count is 1
+
+ maybe_pluralize(0, 'Article', 'Articles') -> '0 Articles'
+ maybe_pluralize(1, 'Article', 'Articles') -> '1 Article'
+ maybe_pluralize(2, 'Article', 'Articles') -> '2 Articles'
+
+ '''
+ selection = plural
+ if count == 1:
+ selection = singular
+ return '{} {}'.format(count, selection)
diff --git a/pelican/writers.py b/pelican/writers.py
new file mode 100644
index 00000000..9b27a748
--- /dev/null
+++ b/pelican/writers.py
@@ -0,0 +1,270 @@
+import logging
+import os
+from posixpath import join as posix_join
+from urllib.parse import urljoin
+
+from feedgenerator import Atom1Feed, Rss201rev2Feed, get_tag_uri
+
+from jinja2 import Markup
+
+from pelican.paginator import Paginator
+from pelican.plugins import signals
+from pelican.utils import (get_relative_path, is_selected_for_writing,
+ path_to_url, sanitised_join, set_date_tzinfo)
+
+logger = logging.getLogger(__name__)
+
+
+class Writer:
+
+ def __init__(self, output_path, settings=None):
+ self.output_path = output_path
+ self.reminder = dict()
+ self.settings = settings or {}
+ self._written_files = set()
+ self._overridden_files = set()
+
+ # See Content._link_replacer for details
+ if self.settings['RELATIVE_URLS']:
+ self.urljoiner = posix_join
+ else:
+ self.urljoiner = lambda base, url: urljoin(
+ base if base.endswith('/') else base + '/', url)
+
+ def _create_new_feed(self, feed_type, feed_title, context):
+ feed_class = Rss201rev2Feed if feed_type == 'rss' else Atom1Feed
+ if feed_title:
+ feed_title = context['SITENAME'] + ' - ' + feed_title
+ else:
+ feed_title = context['SITENAME']
+ feed = feed_class(
+ title=Markup(feed_title).striptags(),
+ link=(self.site_url + '/'),
+ feed_url=self.feed_url,
+ description=context.get('SITESUBTITLE', ''),
+ subtitle=context.get('SITESUBTITLE', None))
+ return feed
+
+ def _add_item_to_the_feed(self, feed, item):
+ title = Markup(item.title).striptags()
+ link = self.urljoiner(self.site_url, item.url)
+
+ if isinstance(feed, Rss201rev2Feed):
+ # RSS feeds use a single tag called 'description' for both the full
+ # content and the summary
+ content = None
+ if self.settings.get('RSS_FEED_SUMMARY_ONLY'):
+ description = item.summary
+ else:
+ description = item.get_content(self.site_url)
+
+ else:
+ # Atom feeds have two different tags for full content (called
+ # 'content' by feedgenerator) and summary (called 'description' by
+ # feedgenerator).
+ #
+ # It does not make sense to have the summary be the
+ # exact same thing as the full content. If we detect that
+ # they are we just remove the summary.
+ content = item.get_content(self.site_url)
+ description = item.summary
+ if description == content:
+ description = None
+
+ categories = list()
+ if hasattr(item, 'category'):
+ categories.append(item.category)
+ if hasattr(item, 'tags'):
+ categories.extend(item.tags)
+
+ feed.add_item(
+ title=title,
+ link=link,
+ unique_id=get_tag_uri(link, item.date),
+ description=description,
+ content=content,
+ categories=categories if categories else None,
+ author_name=getattr(item, 'author', ''),
+ pubdate=set_date_tzinfo(
+ item.date, self.settings.get('TIMEZONE', None)),
+ updateddate=set_date_tzinfo(
+ item.modified, self.settings.get('TIMEZONE', None)
+ ) if hasattr(item, 'modified') else None)
+
+ def _open_w(self, filename, encoding, override=False):
+ """Open a file to write some content to it.
+
+ Exit if we have already written to that file, unless one (and no more
+ than one) of the writes has the override parameter set to True.
+ """
+ if filename in self._overridden_files:
+ if override:
+ raise RuntimeError('File %s is set to be overridden twice'
+ % filename)
+ else:
+ logger.info('Skipping %s', filename)
+ filename = os.devnull
+ elif filename in self._written_files:
+ if override:
+ logger.info('Overwriting %s', filename)
+ else:
+ raise RuntimeError('File %s is to be overwritten' % filename)
+ if override:
+ self._overridden_files.add(filename)
+ self._written_files.add(filename)
+ return open(filename, 'w', encoding=encoding)
+
+ def write_feed(self, elements, context, path=None, url=None,
+ feed_type='atom', override_output=False, feed_title=None):
+ """Generate a feed with the list of articles provided
+
+ Return the feed. If no path or output_path is specified, just
+ return the feed object.
+
+ :param elements: the articles to put on the feed.
+ :param context: the context to get the feed metadata.
+ :param path: the path to output.
+ :param url: the publicly visible feed URL; if None, path is used
+ instead
+ :param feed_type: the feed type to use (atom or rss)
+ :param override_output: boolean telling if we can override previous
+ output with the same name (and if next files written with the same
+ name should be skipped to keep that one)
+ :param feed_title: the title of the feed.o
+ """
+ if not is_selected_for_writing(self.settings, path):
+ return
+
+ self.site_url = context.get(
+ 'SITEURL', path_to_url(get_relative_path(path)))
+
+ self.feed_domain = context.get('FEED_DOMAIN')
+ self.feed_url = self.urljoiner(self.feed_domain, url if url else path)
+
+ feed = self._create_new_feed(feed_type, feed_title, context)
+
+ max_items = len(elements)
+ if self.settings['FEED_MAX_ITEMS']:
+ max_items = min(self.settings['FEED_MAX_ITEMS'], max_items)
+ for i in range(max_items):
+ self._add_item_to_the_feed(feed, elements[i])
+
+ signals.feed_generated.send(context, feed=feed)
+ if path:
+ complete_path = sanitised_join(self.output_path, path)
+
+ try:
+ os.makedirs(os.path.dirname(complete_path))
+ except Exception:
+ pass
+
+ with self._open_w(complete_path, 'utf-8', override_output) as fp:
+ feed.write(fp, 'utf-8')
+ logger.info('Writing %s', complete_path)
+
+ signals.feed_written.send(
+ complete_path, context=context, feed=feed)
+ return feed
+
+ def write_file(self, name, template, context, relative_urls=False,
+ paginated=None, template_name=None, override_output=False,
+ url=None, **kwargs):
+ """Render the template and write the file.
+
+ :param name: name of the file to output
+ :param template: template to use to generate the content
+ :param context: dict to pass to the templates.
+ :param relative_urls: use relative urls or absolutes ones
+ :param paginated: dict of article list to paginate - must have the
+ same length (same list in different orders)
+ :param template_name: the template name, for pagination
+ :param override_output: boolean telling if we can override previous
+ output with the same name (and if next files written with the same
+ name should be skipped to keep that one)
+ :param url: url of the file (needed by the paginator)
+ :param **kwargs: additional variables to pass to the templates
+ """
+
+ if name is False or \
+ name == "" or \
+ not is_selected_for_writing(self.settings,
+ os.path.join(self.output_path, name)):
+ return
+ elif not name:
+ # other stuff, just return for now
+ return
+
+ def _write_file(template, localcontext, output_path, name, override):
+ """Render the template write the file."""
+ # set localsiteurl for context so that Contents can adjust links
+ if localcontext['localsiteurl']:
+ context['localsiteurl'] = localcontext['localsiteurl']
+ output = template.render(localcontext)
+ path = sanitised_join(output_path, name)
+
+ try:
+ os.makedirs(os.path.dirname(path))
+ except Exception:
+ pass
+
+ with self._open_w(path, 'utf-8', override=override) as f:
+ f.write(output)
+ logger.info('Writing %s', path)
+
+ # Send a signal to say we're writing a file with some specific
+ # local context.
+ signals.content_written.send(path, context=localcontext)
+
+ def _get_localcontext(context, name, kwargs, relative_urls):
+ localcontext = context.copy()
+ localcontext['localsiteurl'] = localcontext.get(
+ 'localsiteurl', None)
+ if relative_urls:
+ relative_url = path_to_url(get_relative_path(name))
+ localcontext['SITEURL'] = relative_url
+ localcontext['localsiteurl'] = relative_url
+ localcontext['output_file'] = name
+ localcontext.update(kwargs)
+ return localcontext
+
+ if paginated is None:
+ paginated = {key: val for key, val in kwargs.items()
+ if key in {'articles', 'dates'}}
+
+ # pagination
+ if paginated and template_name in self.settings['PAGINATED_TEMPLATES']:
+ # pagination needed
+ per_page = self.settings['PAGINATED_TEMPLATES'][template_name] \
+ or self.settings['DEFAULT_PAGINATION']
+
+ # init paginators
+ paginators = {key: Paginator(name, url, val, self.settings,
+ per_page)
+ for key, val in paginated.items()}
+
+ # generated pages, and write
+ for page_num in range(list(paginators.values())[0].num_pages):
+ paginated_kwargs = kwargs.copy()
+ for key in paginators.keys():
+ paginator = paginators[key]
+ previous_page = paginator.page(page_num) \
+ if page_num > 0 else None
+ page = paginator.page(page_num + 1)
+ next_page = paginator.page(page_num + 2) \
+ if page_num + 1 < paginator.num_pages else None
+ paginated_kwargs.update(
+ {'%s_paginator' % key: paginator,
+ '%s_page' % key: page,
+ '%s_previous_page' % key: previous_page,
+ '%s_next_page' % key: next_page})
+
+ localcontext = _get_localcontext(
+ context, page.save_as, paginated_kwargs, relative_urls)
+ _write_file(template, localcontext, self.output_path,
+ page.save_as, override_output)
+ else:
+ # no pagination
+ localcontext = _get_localcontext(
+ context, name, kwargs, relative_urls)
+ _write_file(template, localcontext, self.output_path, name,
+ override_output)
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..6aafee89
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,87 @@
+[tool.poetry]
+name = "pelican"
+version = "4.2.0"
+description = "Static site generator supporting Markdown and reStructuredText"
+authors = ["Justin Mayer "]
+license = "AGPLv3"
+readme = "README.rst"
+keywords = ["static site generator", "static sites", "ssg"]
+
+homepage = "https://getpelican.com"
+repository = "https://github.com/getpelican/pelican"
+documentation = "https://docs.getpelican.com"
+
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Environment :: Console",
+ "Framework :: Pelican",
+ "License :: OSI Approved :: GNU Affero General Public License v3",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.6",
+ "Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: Implementation :: CPython",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+]
+
+[tool.poetry.urls]
+"Funding" = "https://donate.getpelican.com/"
+"Tracker" = "https://github.com/getpelican/pelican/issues"
+
+[tool.poetry.dependencies]
+python = "^3.6"
+blinker = "^1.4"
+docutils = "^0.16"
+feedgenerator = "^1.9"
+jinja2 = "~2.11"
+pygments = "~2.6.1"
+python-dateutil = "^2.8"
+pytz = "^2020.1"
+unidecode = "^1.1"
+markdown = {version = "~3.2.2", optional = true}
+
+[tool.poetry.dev-dependencies]
+BeautifulSoup4 = "^4.9"
+lxml = "^4.3"
+markdown = "~3.2.2"
+typogrify = "^2.0"
+sphinx = "^3.0"
+sphinx_rtd_theme = "^0.5"
+livereload = "^2.6"
+pytest = "^6.0"
+pytest-cov = "^2.8"
+pytest-pythonpath = "^0.7.3"
+pytest-sugar = "^0.9.4"
+pytest-xdist = "^2.0"
+tox = "^3.13"
+flake8 = "^3.8"
+flake8-import-order = "^0.18.1"
+invoke = "^1.3"
+isort = "^5.2"
+black = {version = "^19.10b0", allow-prereleases = true}
+
+[tool.poetry.extras]
+markdown = ["markdown"]
+
+[tool.poetry.scripts]
+pelican = "pelican.__main__:main"
+pelican-import = "pelican.tools.pelican_import:main"
+pelican-plugins = "pelican.plugins._utils:list_plugins"
+pelican-quickstart = "pelican.tools.pelican_quickstart:main"
+pelican-themes = "pelican.tools.pelican_themes:main"
+
+[tool.autopub]
+project-name = "Pelican"
+git-username = "botpub"
+git-email = "botpub@autopub.rocks"
+changelog-file = "docs/changelog.rst"
+changelog-header = "###############"
+version-header = "="
+version-strings = ["setup.py"]
+build-system = "setuptools"
+
+[build-system]
+requires = ["poetry>=1.0"]
+build-backend = "poetry.masonry.api"
diff --git a/requirements/developer.pip b/requirements/developer.pip
new file mode 100644
index 00000000..5c2f5a69
--- /dev/null
+++ b/requirements/developer.pip
@@ -0,0 +1,3 @@
+-r test.pip
+-r docs.pip
+-r style.pip
diff --git a/requirements/docs.pip b/requirements/docs.pip
new file mode 100644
index 00000000..bd25c939
--- /dev/null
+++ b/requirements/docs.pip
@@ -0,0 +1,3 @@
+sphinx
+sphinx_rtd_theme
+livereload
diff --git a/requirements/owner.pip b/requirements/owner.pip
new file mode 100644
index 00000000..45232fde
--- /dev/null
+++ b/requirements/owner.pip
@@ -0,0 +1,5 @@
+-r developer.pip
+
+# Release issuance
+tox
+wheel
diff --git a/requirements/style.pip b/requirements/style.pip
new file mode 100644
index 00000000..90225d01
--- /dev/null
+++ b/requirements/style.pip
@@ -0,0 +1,2 @@
+flake8
+flake8-import-order
diff --git a/requirements/test.pip b/requirements/test.pip
new file mode 100644
index 00000000..647a8694
--- /dev/null
+++ b/requirements/test.pip
@@ -0,0 +1,11 @@
+# Tests
+Pygments==2.6.1
+pytest
+pytest-cov
+pytest-xdist
+
+# Optional Packages
+Markdown >= 3.1
+BeautifulSoup4
+lxml
+typogrify
diff --git a/samples/content/2012-11-30_filename-metadata.rst b/samples/content/2012-11-30_filename-metadata.rst
new file mode 100644
index 00000000..b048103d
--- /dev/null
+++ b/samples/content/2012-11-30_filename-metadata.rst
@@ -0,0 +1,4 @@
+FILENAME_METADATA example
+#########################
+
+Some cool stuff!
diff --git a/samples/content/another_super_article-fr.rst b/samples/content/another_super_article-fr.rst
new file mode 100644
index 00000000..dcd252a7
--- /dev/null
+++ b/samples/content/another_super_article-fr.rst
@@ -0,0 +1,8 @@
+Trop bien !
+###########
+
+:date: 2010-10-20 10:14
+:lang: fr
+:slug: oh-yeah
+
+Et voila du contenu en français
diff --git a/samples/content/another_super_article.rst b/samples/content/another_super_article.rst
new file mode 100644
index 00000000..a170c4a1
--- /dev/null
+++ b/samples/content/another_super_article.rst
@@ -0,0 +1,21 @@
+Oh yeah !
+#########
+
+:tags: oh, bar, yeah
+:date: 2010-10-20 10:14
+:category: bar
+:author: Alexis Métaireau
+:lang: en
+:slug: oh-yeah
+:license: WTFPL
+
+Why not ?
+=========
+
+After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst !
+YEAH !
+
+.. image:: |static|/pictures/Sushi.jpg
+ :height: 450 px
+ :width: 600 px
+ :alt: alternate text
diff --git a/samples/content/article2-fr.rst b/samples/content/article2-fr.rst
new file mode 100644
index 00000000..31970f7e
--- /dev/null
+++ b/samples/content/article2-fr.rst
@@ -0,0 +1,9 @@
+Deuxième article
+################
+
+:tags: foo, bar, baz
+:date: 2012-02-29
+:lang: fr
+:slug: second-article
+
+Ceci est un article, en français.
diff --git a/samples/content/article2.rst b/samples/content/article2.rst
new file mode 100644
index 00000000..66f768ea
--- /dev/null
+++ b/samples/content/article2.rst
@@ -0,0 +1,9 @@
+Second article
+##############
+
+:tags: foo, bar, baz
+:date: 2012-02-29
+:lang: en
+:slug: second-article
+
+This is some article, in english
diff --git a/samples/content/article_tag_baz.rst b/samples/content/article_tag_baz.rst
new file mode 100644
index 00000000..9a33a8d3
--- /dev/null
+++ b/samples/content/article_tag_baz.rst
@@ -0,0 +1,8 @@
+The baz tag
+###########
+
+:date: 2010-03-14
+:url: tag/baz.html
+:save_as: tag/baz.html
+
+This article overrides the listening of the articles under the *baz* tag.
diff --git a/samples/content/cat1/article1.rst b/samples/content/cat1/article1.rst
new file mode 100644
index 00000000..1148a8f9
--- /dev/null
+++ b/samples/content/cat1/article1.rst
@@ -0,0 +1,7 @@
+Article 1
+#########
+
+:date: 2011-02-17
+:yeah: oh yeah !
+
+Article 1
diff --git a/samples/content/cat1/article2.rst b/samples/content/cat1/article2.rst
new file mode 100644
index 00000000..a4f87866
--- /dev/null
+++ b/samples/content/cat1/article2.rst
@@ -0,0 +1,6 @@
+Article 2
+#########
+
+:date: 2011-02-17
+
+Article 2
diff --git a/samples/content/cat1/article3.rst b/samples/content/cat1/article3.rst
new file mode 100644
index 00000000..53471177
--- /dev/null
+++ b/samples/content/cat1/article3.rst
@@ -0,0 +1,6 @@
+Article 3
+#########
+
+:date: 2011-02-17
+
+Article 3
diff --git a/samples/content/cat1/markdown-article.md b/samples/content/cat1/markdown-article.md
new file mode 100644
index 00000000..5307b47a
--- /dev/null
+++ b/samples/content/cat1/markdown-article.md
@@ -0,0 +1,7 @@
+Title: A markdown powered article
+Date: 2011-04-20
+
+You're mutually oblivious.
+
+[a root-relative link to unbelievable](|filename|/unbelievable.rst)
+[a file-relative link to unbelievable](|filename|../unbelievable.rst)
diff --git a/samples/content/draft_article without_date.rst b/samples/content/draft_article without_date.rst
new file mode 100644
index 00000000..5ac62034
--- /dev/null
+++ b/samples/content/draft_article without_date.rst
@@ -0,0 +1,7 @@
+A draft article without date
+############################
+
+:status: draft
+
+This is a draft article, it should live under the /drafts/ folder and not be
+listed anywhere else.
diff --git a/samples/content/draft_article.rst b/samples/content/draft_article.rst
new file mode 100644
index 00000000..6b383ca6
--- /dev/null
+++ b/samples/content/draft_article.rst
@@ -0,0 +1,8 @@
+A draft article
+###############
+
+:date: 2011-05-08 15:58
+:status: draft
+
+This is a draft article, it should live under the /drafts/ folder and not be
+listed anywhere else.
diff --git a/samples/content/extra/robots.txt b/samples/content/extra/robots.txt
new file mode 100644
index 00000000..19a6e299
--- /dev/null
+++ b/samples/content/extra/robots.txt
@@ -0,0 +1,2 @@
+User-agent: *
+Disallow: /pictures
diff --git a/samples/content/pages/hidden_page.rst b/samples/content/pages/hidden_page.rst
new file mode 100644
index 00000000..ab8704ed
--- /dev/null
+++ b/samples/content/pages/hidden_page.rst
@@ -0,0 +1,9 @@
+This is a test hidden page
+##########################
+
+:category: test
+:status: hidden
+
+This is great for things like error(404) pages
+Anyone can see this page but it's not linked to anywhere!
+
diff --git a/samples/content/pages/jinja2_template.html b/samples/content/pages/jinja2_template.html
new file mode 100644
index 00000000..1b0dc4e4
--- /dev/null
+++ b/samples/content/pages/jinja2_template.html
@@ -0,0 +1,6 @@
+{% extends "base.html" %}
+{% block content %}
+
+Some text
+
+{% endblock %}
diff --git a/samples/content/pages/override_tag_oh.rst b/samples/content/pages/override_tag_oh.rst
new file mode 100644
index 00000000..c3160349
--- /dev/null
+++ b/samples/content/pages/override_tag_oh.rst
@@ -0,0 +1,8 @@
+Oh Oh Oh
+########
+
+:date: 2010-03-14
+:url: tag/oh.html
+:save_as: tag/oh.html
+
+This page overrides the listening of the articles under the *oh* tag.
diff --git a/samples/content/pages/override_url_saveas.rst b/samples/content/pages/override_url_saveas.rst
new file mode 100644
index 00000000..8a515f60
--- /dev/null
+++ b/samples/content/pages/override_url_saveas.rst
@@ -0,0 +1,9 @@
+Override url/save_as
+####################
+
+:date: 2012-12-07
+:url: override/
+:save_as: override/index.html
+
+Test page which overrides save_as and url so that this page will be generated
+at a custom location.
diff --git a/samples/content/pages/test_page.rst b/samples/content/pages/test_page.rst
new file mode 100644
index 00000000..3747faed
--- /dev/null
+++ b/samples/content/pages/test_page.rst
@@ -0,0 +1,16 @@
+This is a test page
+###################
+
+:category: test
+
+Just an image.
+
+.. image:: {static}/pictures/Fat_Cat.jpg
+ :height: 450 px
+ :width: 600 px
+ :alt: alternate text
+
+.. image:: |filename|/images/Fat_Cat.jpg
+ :height: 450 px
+ :width: 600 px
+ :alt: wrong path since 'images' folder does not exist
diff --git a/samples/content/pictures/Fat_Cat.jpg b/samples/content/pictures/Fat_Cat.jpg
new file mode 100644
index 00000000..d8a96d35
Binary files /dev/null and b/samples/content/pictures/Fat_Cat.jpg differ
diff --git a/samples/content/pictures/Sushi.jpg b/samples/content/pictures/Sushi.jpg
new file mode 100644
index 00000000..e49e5f0a
Binary files /dev/null and b/samples/content/pictures/Sushi.jpg differ
diff --git a/samples/content/pictures/Sushi_Macro.jpg b/samples/content/pictures/Sushi_Macro.jpg
new file mode 100644
index 00000000..21f935a1
Binary files /dev/null and b/samples/content/pictures/Sushi_Macro.jpg differ
diff --git a/samples/content/super_article.rst b/samples/content/super_article.rst
new file mode 100644
index 00000000..b2caa070
--- /dev/null
+++ b/samples/content/super_article.rst
@@ -0,0 +1,37 @@
+This is a super article !
+#########################
+
+:tags: foo, bar, foobar
+:date: 2010-12-02 10:14
+:modified: 2013-11-17 23:29
+:category: yeah
+:author: Alexis Métaireau
+:summary:
+ Multi-line metadata should be supported
+ as well as **inline markup**.
+
+Some content here !
+
+This is a simple title
+======================
+
+And here comes the cool stuff_.
+
+.. image:: |static|/pictures/Sushi.jpg
+ :height: 450 px
+ :width: 600 px
+ :alt: alternate text
+
+.. image:: |static|/pictures/Sushi_Macro.jpg
+ :height: 450 px
+ :width: 600 px
+ :alt: alternate text
+
+::
+
+ >>> from ipdb import set_trace
+ >>> set_trace()
+
+→ And now try with some utf8 hell: ééé
+
+.. _stuff: http://books.couchdb.org/relax/design-documents/views
diff --git a/samples/content/unbelievable.rst b/samples/content/unbelievable.rst
new file mode 100644
index 00000000..209e3557
--- /dev/null
+++ b/samples/content/unbelievable.rst
@@ -0,0 +1,98 @@
+Unbelievable !
+##############
+
+:date: 2010-10-15 20:30
+
+Or completely awesome. Depends the needs.
+
+`a root-relative link to markdown-article <|filename|/cat1/markdown-article.md>`_
+`a file-relative link to markdown-article <|filename|cat1/markdown-article.md>`_
+
+Testing sourcecode directive
+----------------------------
+
+.. sourcecode:: python
+ :linenos:
+
+ formatter = self.options and VARIANTS[self.options.keys()[0]]
+
+
+Testing another case
+--------------------
+
+This will now have a line number in 'custom' since it's the default in
+pelican.conf, it will have nothing in default.
+
+.. sourcecode:: python
+
+ formatter = self.options and VARIANTS[self.options.keys()[0]]
+
+
+Lovely.
+
+Testing more sourcecode directives
+----------------------------------
+
+.. sourcecode:: python
+ :anchorlinenos:
+ :classprefix: testing
+ :hl_lines: 10,11,12
+ :lineanchors: foo
+ :linenos: inline
+ :linenospecial: 2
+ :linenostart: 8
+ :linenostep: 2
+ :lineseparator:
+ :linespans: foo
+ :nobackground:
+
+ 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()
+
+ if ('linenos' in self.options and
+ self.options['linenos'] not in ('table', 'inline')):
+ 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')]
+
+
+Lovely.
+
+Testing even more sourcecode directives
+---------------------------------------
+
+.. sourcecode:: python
+ :linenos: table
+ :nowrap:
+
+
+ formatter = self.options and VARIANTS[self.options.keys()[0]]
+
+
+Lovely.
+
+Testing overriding config defaults
+----------------------------------
+
+Even if the default is line numbers, we can override it here
+
+.. sourcecode:: python
+ :linenos: none
+
+
+ formatter = self.options and VARIANTS[self.options.keys()[0]]
+
+
+Lovely.
diff --git a/samples/content/unwanted_file b/samples/content/unwanted_file
new file mode 100644
index 00000000..591255ae
--- /dev/null
+++ b/samples/content/unwanted_file
@@ -0,0 +1 @@
+not to be parsed
diff --git a/samples/kinda/exciting/new/files/zap! b/samples/kinda/exciting/new/files/zap!
new file mode 100644
index 00000000..e69de29b
diff --git a/samples/kinda/exciting/old b/samples/kinda/exciting/old
new file mode 100644
index 00000000..e69de29b
diff --git a/samples/pelican.conf.py b/samples/pelican.conf.py
new file mode 100755
index 00000000..1fa7c472
--- /dev/null
+++ b/samples/pelican.conf.py
@@ -0,0 +1,56 @@
+AUTHOR = 'Alexis Métaireau'
+SITENAME = "Alexis' log"
+SITESUBTITLE = 'A personal blog.'
+SITEURL = 'http://blog.notmyidea.org'
+TIMEZONE = "Europe/Paris"
+
+# can be useful in development, but set to False when you're ready to publish
+RELATIVE_URLS = True
+
+GITHUB_URL = 'http://github.com/ametaireau/'
+DISQUS_SITENAME = "blog-notmyidea"
+REVERSE_CATEGORY_ORDER = True
+LOCALE = "C"
+DEFAULT_PAGINATION = 4
+DEFAULT_DATE = (2012, 3, 2, 14, 1, 1)
+
+FEED_ALL_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 = [
+ 'images',
+ 'extra/robots.txt',
+ ]
+
+# custom page generated with a jinja2 template
+TEMPLATE_PAGES = {'pages/jinja2_template.html': 'jinja2_template.html'}
+
+# there is no other HTML content
+READERS = {'html': None}
+
+# code blocks with line numbers
+PYGMENTS_RST_OPTIONS = {'linenos': 'table'}
+
+# foobar will not be used, because it's not in caps. All configuration keys
+# have to be in caps
+foobar = "barbaz"
diff --git a/samples/pelican.conf_FR.py b/samples/pelican.conf_FR.py
new file mode 100644
index 00000000..dc657404
--- /dev/null
+++ b/samples/pelican.conf_FR.py
@@ -0,0 +1,57 @@
+AUTHOR = 'Alexis Métaireau'
+SITENAME = "Alexis' log"
+SITEURL = 'http://blog.notmyidea.org'
+TIMEZONE = "Europe/Paris"
+
+# can be useful in development, but set to False when you're ready to publish
+RELATIVE_URLS = True
+
+GITHUB_URL = 'http://github.com/ametaireau/'
+DISQUS_SITENAME = "blog-notmyidea"
+PDF_GENERATOR = False
+REVERSE_CATEGORY_ORDER = True
+LOCALE = "fr_FR.UTF-8"
+DEFAULT_PAGINATION = 4
+DEFAULT_DATE = (2012, 3, 2, 14, 1, 1)
+DEFAULT_DATE_FORMAT = '%d %B %Y'
+
+ARTICLE_URL = 'posts/{date:%Y}/{date:%B}/{date:%d}/{slug}/'
+ARTICLE_SAVE_AS = ARTICLE_URL + 'index.html'
+
+FEED_ALL_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',
+ ]
+
+# custom page generated with a jinja2 template
+TEMPLATE_PAGES = {'pages/jinja2_template.html': 'jinja2_template.html'}
+
+# code blocks with line numbers
+PYGMENTS_RST_OPTIONS = {'linenos': 'table'}
+
+# foobar will not be used, because it's not in caps. All configuration keys
+# have to be in caps
+foobar = "barbaz"
diff --git a/samples/theme_standard/a_stylesheet b/samples/theme_standard/a_stylesheet
new file mode 100644
index 00000000..e69de29b
diff --git a/samples/theme_standard/a_template b/samples/theme_standard/a_template
new file mode 100644
index 00000000..e69de29b
diff --git a/samples/very/exciting/new/files/bap! b/samples/very/exciting/new/files/bap!
new file mode 100644
index 00000000..e69de29b
diff --git a/samples/very/exciting/new/files/boom! b/samples/very/exciting/new/files/boom!
new file mode 100644
index 00000000..e69de29b
diff --git a/samples/very/exciting/new/files/wow! b/samples/very/exciting/new/files/wow!
new file mode 100644
index 00000000..e69de29b
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 00000000..2a9acf13
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,2 @@
+[bdist_wheel]
+universal = 1
diff --git a/setup.py b/setup.py
new file mode 100755
index 00000000..af96c726
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python
+
+from os import walk
+from os.path import join, relpath
+
+from setuptools import setup, find_packages
+
+
+version = "4.2.0"
+
+requires = ['feedgenerator >= 1.9', 'jinja2 >= 2.11', 'pygments',
+ 'docutils>=0.15', 'pytz >= 0a', 'blinker', 'unidecode',
+ 'python-dateutil']
+
+entry_points = {
+ 'console_scripts': [
+ 'pelican = pelican.__main__:main',
+ 'pelican-import = pelican.tools.pelican_import:main',
+ 'pelican-quickstart = pelican.tools.pelican_quickstart:main',
+ 'pelican-themes = pelican.tools.pelican_themes:main',
+ 'pelican-plugins = pelican.plugins._utils:list_plugins'
+ ]
+}
+
+README = open('README.rst', encoding='utf-8').read()
+CHANGELOG = open('docs/changelog.rst', encoding='utf-8').read()
+
+description = '\n'.join([README, CHANGELOG])
+
+setup(
+ name='pelican',
+ version=version,
+ url='https://getpelican.com/',
+ author='Justin Mayer',
+ author_email='authors@getpelican.com',
+ description="Static site generator supporting reStructuredText and "
+ "Markdown source content.",
+ project_urls={
+ 'Documentation': 'https://docs.getpelican.com/',
+ 'Funding': 'https://donate.getpelican.com/',
+ 'Source': 'https://github.com/getpelican/pelican',
+ 'Tracker': 'https://github.com/getpelican/pelican/issues',
+ },
+ keywords='static web site generator SSG reStructuredText Markdown',
+ license='AGPLv3',
+ long_description=description,
+ packages=find_packages(),
+ include_package_data=True, # includes all in MANIFEST.in if in package
+ # NOTE : This will collect any files that happen to be in the themes
+ # directory, even though they may not be checked into version control.
+ package_data={ # pelican/themes is not a package, so include manually
+ 'pelican': [relpath(join(root, name), 'pelican')
+ for root, _, names in walk(join('pelican', 'themes'))
+ for name in names],
+ },
+ install_requires=requires,
+ extras_require={
+ 'Markdown': ['markdown~=3.1.1']
+ },
+ entry_points=entry_points,
+ classifiers=[
+ 'Development Status :: 5 - Production/Stable',
+ 'Environment :: Console',
+ 'Framework :: Pelican',
+ 'License :: OSI Approved :: GNU Affero General Public License v3',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.5',
+ 'Programming Language :: Python :: 3.6',
+ 'Programming Language :: Python :: 3.7',
+ 'Programming Language :: Python :: Implementation :: CPython',
+ 'Topic :: Internet :: WWW/HTTP',
+ 'Topic :: Software Development :: Libraries :: Python Modules',
+ ],
+ test_suite='pelican.tests',
+)
diff --git a/static/css/in.css b/static/css/in.css
deleted file mode 100644
index 041b7b4a..00000000
--- a/static/css/in.css
+++ /dev/null
@@ -1,108 +0,0 @@
-@tailwind base; /* Preflight is injected here */
-@tailwind components;
-@tailwind utilities;
-
-@layer base {
- @font-face {
- /*https://style64.org/c64-truetype*/
- font-family: "C64 Pro Mono";
- font-weight: 400;
- src: url(/theme/font/C64_Pro_Mono-STYLE.woff2) format("woff");
- }
-
- html {
- /* text-lg == prose-lg */
- /* text-xl == prose-xl */
- @apply text-lg md:text-lg lg:text-xl font-texts text-rp-dawn-text dark:text-rp-moon-iris;
- }
- text {
- @apply text-rp-dawn-text dark:text-rp-moon-iris;
- }
- a {
- @apply underline decoration-2 underline-offset-4 decoration-rp-dawn-gold dark:decoration-rp-moon-pine;
- }
- a:hover {
- @apply underline decoration-2 underline-offset-4 decoration-rp-dawn-foam dark:decoration-rp-moon-rose;
- }
- h1,
- h2,
- h3,
- h4,
- h5,
- h6 {
- @apply font-headings text-rp-dawn-text dark:text-rp-moon-love;
- }
- h1 {
- @apply text-5xl md:text-7xl mb-9;
- }
- h2 {
- @apply text-4xl md:text-5xl mb-9;
- }
- h3 {
- @apply text-3xl md:text-4xl;
- }
- /*for unknown reasons, h4 must be undefined to not make it bigger than h3*/
- /*h5-h6 are ignored by tailwind typography*/
- img {
- @apply drop-shadow-lg;
- }
- dd {
- @apply ml-12 md:ml-24;
- }
- ul {
- @apply pl-4 list-disc;
- }
-
- /* pelican tag_cloud */
- ul.tagcloud {
- @apply list-none p-0;
- }
- ul.tagcloud li {
- @apply inline-block p-1;
- }
-}
-
-@layer components {
- /*This does not seem to affect prismjs which is good*/
- code {
- @apply bg-rp-dawn-highlight-low dark:bg-rp-moon-highlight-low;
- }
-}
-
-/*Hide heading anchor links unless hovering over them*/
-.headerlink {
- @apply no-underline text-rp-dawn-love ml-1 md:ml-2 hover:no-underline;
-}
-
-.note {
- @apply bg-rp-dawn-highlight-med dark:bg-rp-moon-highlight-med m-8 p-4;
-}
-.warn {
- @apply bg-rp-dawn-text text-rp-dawn-base dark:bg-rp-moon-text dark:text-rp-moon-base m-8 p-4;
-}
-
-/*Attempt to only float thumbnails to the right*/
-.image-process-article-image,
-.image-process-thumb,
-.image-process-responsive {
- /*@apply lg:float-right p-2 lg:p-4;*/
- @apply lg:float-right px-1 lg:px-3;
-}
-
-#skiptocontent a {
- position: absolute;
- top: -40px;
- left: 0px;
- /*color: white;*/
- /*border-right: 1px solid white;*/
- /*border-bottom: 1px solid white;*/
- z-index: 100;
- @apply bg-rp-dawn-text text-rp-dawn-surface dark:bg-rp-moon-text dark:text-rp-moon-surface;
-}
-
-#skiptocontent a:focus {
- position: absolute;
- left: 0px;
- top: 0px;
- outline-color: transparent;
-}
diff --git a/static/css/out.css b/static/css/out.css
deleted file mode 100644
index e1870e6c..00000000
--- a/static/css/out.css
+++ /dev/null
@@ -1,2318 +0,0 @@
-*, ::before, ::after {
- --tw-border-spacing-x: 0;
- --tw-border-spacing-y: 0;
- --tw-translate-x: 0;
- --tw-translate-y: 0;
- --tw-rotate: 0;
- --tw-skew-x: 0;
- --tw-skew-y: 0;
- --tw-scale-x: 1;
- --tw-scale-y: 1;
- --tw-pan-x: ;
- --tw-pan-y: ;
- --tw-pinch-zoom: ;
- --tw-scroll-snap-strictness: proximity;
- --tw-gradient-from-position: ;
- --tw-gradient-via-position: ;
- --tw-gradient-to-position: ;
- --tw-ordinal: ;
- --tw-slashed-zero: ;
- --tw-numeric-figure: ;
- --tw-numeric-spacing: ;
- --tw-numeric-fraction: ;
- --tw-ring-inset: ;
- --tw-ring-offset-width: 0px;
- --tw-ring-offset-color: #fff;
- --tw-ring-color: rgb(59 130 246 / 0.5);
- --tw-ring-offset-shadow: 0 0 #0000;
- --tw-ring-shadow: 0 0 #0000;
- --tw-shadow: 0 0 #0000;
- --tw-shadow-colored: 0 0 #0000;
- --tw-blur: ;
- --tw-brightness: ;
- --tw-contrast: ;
- --tw-grayscale: ;
- --tw-hue-rotate: ;
- --tw-invert: ;
- --tw-saturate: ;
- --tw-sepia: ;
- --tw-drop-shadow: ;
- --tw-backdrop-blur: ;
- --tw-backdrop-brightness: ;
- --tw-backdrop-contrast: ;
- --tw-backdrop-grayscale: ;
- --tw-backdrop-hue-rotate: ;
- --tw-backdrop-invert: ;
- --tw-backdrop-opacity: ;
- --tw-backdrop-saturate: ;
- --tw-backdrop-sepia: ;
- --tw-contain-size: ;
- --tw-contain-layout: ;
- --tw-contain-paint: ;
- --tw-contain-style: ;
-}
-
-::backdrop {
- --tw-border-spacing-x: 0;
- --tw-border-spacing-y: 0;
- --tw-translate-x: 0;
- --tw-translate-y: 0;
- --tw-rotate: 0;
- --tw-skew-x: 0;
- --tw-skew-y: 0;
- --tw-scale-x: 1;
- --tw-scale-y: 1;
- --tw-pan-x: ;
- --tw-pan-y: ;
- --tw-pinch-zoom: ;
- --tw-scroll-snap-strictness: proximity;
- --tw-gradient-from-position: ;
- --tw-gradient-via-position: ;
- --tw-gradient-to-position: ;
- --tw-ordinal: ;
- --tw-slashed-zero: ;
- --tw-numeric-figure: ;
- --tw-numeric-spacing: ;
- --tw-numeric-fraction: ;
- --tw-ring-inset: ;
- --tw-ring-offset-width: 0px;
- --tw-ring-offset-color: #fff;
- --tw-ring-color: rgb(59 130 246 / 0.5);
- --tw-ring-offset-shadow: 0 0 #0000;
- --tw-ring-shadow: 0 0 #0000;
- --tw-shadow: 0 0 #0000;
- --tw-shadow-colored: 0 0 #0000;
- --tw-blur: ;
- --tw-brightness: ;
- --tw-contrast: ;
- --tw-grayscale: ;
- --tw-hue-rotate: ;
- --tw-invert: ;
- --tw-saturate: ;
- --tw-sepia: ;
- --tw-drop-shadow: ;
- --tw-backdrop-blur: ;
- --tw-backdrop-brightness: ;
- --tw-backdrop-contrast: ;
- --tw-backdrop-grayscale: ;
- --tw-backdrop-hue-rotate: ;
- --tw-backdrop-invert: ;
- --tw-backdrop-opacity: ;
- --tw-backdrop-saturate: ;
- --tw-backdrop-sepia: ;
- --tw-contain-size: ;
- --tw-contain-layout: ;
- --tw-contain-paint: ;
- --tw-contain-style: ;
-}
-
-/*
-! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com
-*/
-
-/*
-1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
-2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
-*/
-
-*,
-::before,
-::after {
- box-sizing: border-box;
- /* 1 */
- border-width: 0;
- /* 2 */
- border-style: solid;
- /* 2 */
- border-color: #e5e7eb;
- /* 2 */
-}
-
-::before,
-::after {
- --tw-content: '';
-}
-
-/*
-1. Use a consistent sensible line-height in all browsers.
-2. Prevent adjustments of font size after orientation changes in iOS.
-3. Use a more readable tab size.
-4. Use the user's configured `sans` font-family by default.
-5. Use the user's configured `sans` font-feature-settings by default.
-6. Use the user's configured `sans` font-variation-settings by default.
-7. Disable tap highlights on iOS
-*/
-
-html,
-:host {
- line-height: 1.5;
- /* 1 */
- -webkit-text-size-adjust: 100%;
- /* 2 */
- -moz-tab-size: 4;
- /* 3 */
- -o-tab-size: 4;
- tab-size: 4;
- /* 3 */
- font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
- /* 4 */
- font-feature-settings: normal;
- /* 5 */
- font-variation-settings: normal;
- /* 6 */
- -webkit-tap-highlight-color: transparent;
- /* 7 */
-}
-
-/*
-1. Remove the margin in all browsers.
-2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
-*/
-
-body {
- margin: 0;
- /* 1 */
- line-height: inherit;
- /* 2 */
-}
-
-/*
-1. Add the correct height in Firefox.
-2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
-3. Ensure horizontal rules are visible by default.
-*/
-
-hr {
- height: 0;
- /* 1 */
- color: inherit;
- /* 2 */
- border-top-width: 1px;
- /* 3 */
-}
-
-/*
-Add the correct text decoration in Chrome, Edge, and Safari.
-*/
-
-abbr:where([title]) {
- -webkit-text-decoration: underline dotted;
- text-decoration: underline dotted;
-}
-
-/*
-Remove the default font size and weight for headings.
-*/
-
-h1,
-h2,
-h3,
-h4,
-h5,
-h6 {
- font-size: inherit;
- font-weight: inherit;
-}
-
-/*
-Reset links to optimize for opt-in styling instead of opt-out.
-*/
-
-a {
- color: inherit;
- text-decoration: inherit;
-}
-
-/*
-Add the correct font weight in Edge and Safari.
-*/
-
-b,
-strong {
- font-weight: bolder;
-}
-
-/*
-1. Use the user's configured `mono` font-family by default.
-2. Use the user's configured `mono` font-feature-settings by default.
-3. Use the user's configured `mono` font-variation-settings by default.
-4. Correct the odd `em` font sizing in all browsers.
-*/
-
-code,
-kbd,
-samp,
-pre {
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
- /* 1 */
- font-feature-settings: normal;
- /* 2 */
- font-variation-settings: normal;
- /* 3 */
- font-size: 1em;
- /* 4 */
-}
-
-/*
-Add the correct font size in all browsers.
-*/
-
-small {
- font-size: 80%;
-}
-
-/*
-Prevent `sub` and `sup` elements from affecting the line height in all browsers.
-*/
-
-sub,
-sup {
- font-size: 75%;
- line-height: 0;
- position: relative;
- vertical-align: baseline;
-}
-
-sub {
- bottom: -0.25em;
-}
-
-sup {
- top: -0.5em;
-}
-
-/*
-1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
-2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
-3. Remove gaps between table borders by default.
-*/
-
-table {
- text-indent: 0;
- /* 1 */
- border-color: inherit;
- /* 2 */
- border-collapse: collapse;
- /* 3 */
-}
-
-/*
-1. Change the font styles in all browsers.
-2. Remove the margin in Firefox and Safari.
-3. Remove default padding in all browsers.
-*/
-
-button,
-input,
-optgroup,
-select,
-textarea {
- font-family: inherit;
- /* 1 */
- font-feature-settings: inherit;
- /* 1 */
- font-variation-settings: inherit;
- /* 1 */
- font-size: 100%;
- /* 1 */
- font-weight: inherit;
- /* 1 */
- line-height: inherit;
- /* 1 */
- letter-spacing: inherit;
- /* 1 */
- color: inherit;
- /* 1 */
- margin: 0;
- /* 2 */
- padding: 0;
- /* 3 */
-}
-
-/*
-Remove the inheritance of text transform in Edge and Firefox.
-*/
-
-button,
-select {
- text-transform: none;
-}
-
-/*
-1. Correct the inability to style clickable types in iOS and Safari.
-2. Remove default button styles.
-*/
-
-button,
-input:where([type='button']),
-input:where([type='reset']),
-input:where([type='submit']) {
- -webkit-appearance: button;
- /* 1 */
- background-color: transparent;
- /* 2 */
- background-image: none;
- /* 2 */
-}
-
-/*
-Use the modern Firefox focus style for all focusable elements.
-*/
-
-:-moz-focusring {
- outline: auto;
-}
-
-/*
-Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
-*/
-
-:-moz-ui-invalid {
- box-shadow: none;
-}
-
-/*
-Add the correct vertical alignment in Chrome and Firefox.
-*/
-
-progress {
- vertical-align: baseline;
-}
-
-/*
-Correct the cursor style of increment and decrement buttons in Safari.
-*/
-
-::-webkit-inner-spin-button,
-::-webkit-outer-spin-button {
- height: auto;
-}
-
-/*
-1. Correct the odd appearance in Chrome and Safari.
-2. Correct the outline style in Safari.
-*/
-
-[type='search'] {
- -webkit-appearance: textfield;
- /* 1 */
- outline-offset: -2px;
- /* 2 */
-}
-
-/*
-Remove the inner padding in Chrome and Safari on macOS.
-*/
-
-::-webkit-search-decoration {
- -webkit-appearance: none;
-}
-
-/*
-1. Correct the inability to style clickable types in iOS and Safari.
-2. Change font properties to `inherit` in Safari.
-*/
-
-::-webkit-file-upload-button {
- -webkit-appearance: button;
- /* 1 */
- font: inherit;
- /* 2 */
-}
-
-/*
-Add the correct display in Chrome and Safari.
-*/
-
-summary {
- display: list-item;
-}
-
-/*
-Removes the default spacing and border for appropriate elements.
-*/
-
-blockquote,
-dl,
-dd,
-h1,
-h2,
-h3,
-h4,
-h5,
-h6,
-hr,
-figure,
-p,
-pre {
- margin: 0;
-}
-
-fieldset {
- margin: 0;
- padding: 0;
-}
-
-legend {
- padding: 0;
-}
-
-ol,
-ul,
-menu {
- list-style: none;
- margin: 0;
- padding: 0;
-}
-
-/*
-Reset default styling for dialogs.
-*/
-
-dialog {
- padding: 0;
-}
-
-/*
-Prevent resizing textareas horizontally by default.
-*/
-
-textarea {
- resize: vertical;
-}
-
-/*
-1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
-2. Set the default placeholder color to the user's configured gray 400 color.
-*/
-
-input::-moz-placeholder, textarea::-moz-placeholder {
- opacity: 1;
- /* 1 */
- color: #9ca3af;
- /* 2 */
-}
-
-input::placeholder,
-textarea::placeholder {
- opacity: 1;
- /* 1 */
- color: #9ca3af;
- /* 2 */
-}
-
-/*
-Set the default cursor for buttons.
-*/
-
-button,
-[role="button"] {
- cursor: pointer;
-}
-
-/*
-Make sure disabled buttons don't get the pointer cursor.
-*/
-
-:disabled {
- cursor: default;
-}
-
-/*
-1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
-2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
- This can trigger a poorly considered lint error in some tools but is included by design.
-*/
-
-img,
-svg,
-video,
-canvas,
-audio,
-iframe,
-embed,
-object {
- display: block;
- /* 1 */
- vertical-align: middle;
- /* 2 */
-}
-
-/*
-Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
-*/
-
-img,
-video {
- max-width: 100%;
- height: auto;
-}
-
-/* Make elements with the HTML hidden attribute stay hidden by default */
-
-[hidden]:where(:not([hidden="until-found"])) {
- display: none;
-}
-
-@font-face {
- /*https://style64.org/c64-truetype*/
-
- font-family: "C64 Pro Mono";
-
- font-weight: 400;
-
- src: url(/theme/font/C64_Pro_Mono-STYLE.woff2) format("woff");
-}
-
-html {
- /* text-lg == prose-lg */
- /* text-xl == prose-xl */
- font-family: Erode, serif;
- font-size: 1.125rem;
- line-height: 1.75rem;
- --tw-text-opacity: 1;
- color: rgb(87 82 121 / var(--tw-text-opacity, 1));
-}
-
-@media (min-width: 768px) {
- html {
- font-size: 1.125rem;
- line-height: 1.75rem;
- }
-}
-
-@media (min-width: 1024px) {
- html {
- font-size: 1.25rem;
- line-height: 1.75rem;
- }
-}
-
-@media (prefers-color-scheme: dark) {
- html {
- --tw-text-opacity: 1;
- color: rgb(196 167 231 / var(--tw-text-opacity, 1));
- }
-}
-
-text {
- --tw-text-opacity: 1;
- color: rgb(87 82 121 / var(--tw-text-opacity, 1));
-}
-
-@media (prefers-color-scheme: dark) {
- text {
- --tw-text-opacity: 1;
- color: rgb(196 167 231 / var(--tw-text-opacity, 1));
- }
-}
-
-a {
- text-decoration-line: underline;
- text-decoration-color: #ea9d34;
- text-decoration-thickness: 2px;
- text-underline-offset: 4px;
-}
-
-@media (prefers-color-scheme: dark) {
- a {
- text-decoration-color: #3e8fb0;
- }
-}
-
-a:hover {
- text-decoration-line: underline;
- text-decoration-color: #56949f;
- text-decoration-thickness: 2px;
- text-underline-offset: 4px;
-}
-
-@media (prefers-color-scheme: dark) {
- a:hover {
- text-decoration-color: #ea9a97;
- }
-}
-
-h1,
- h2,
- h3,
- h4,
- h5,
- h6 {
- font-family: Fira Sans, sans-serif;
- --tw-text-opacity: 1;
- color: rgb(87 82 121 / var(--tw-text-opacity, 1));
-}
-
-@media (prefers-color-scheme: dark) {
- h1,
- h2,
- h3,
- h4,
- h5,
- h6 {
- --tw-text-opacity: 1;
- color: rgb(235 111 146 / var(--tw-text-opacity, 1));
- }
-}
-
-h1 {
- margin-bottom: 2.25rem;
- font-size: 3rem;
- line-height: 1;
-}
-
-@media (min-width: 768px) {
- h1 {
- font-size: 4.5rem;
- line-height: 1;
- }
-}
-
-h2 {
- margin-bottom: 2.25rem;
- font-size: 2.25rem;
- line-height: 2.5rem;
-}
-
-@media (min-width: 768px) {
- h2 {
- font-size: 3rem;
- line-height: 1;
- }
-}
-
-h3 {
- font-size: 1.875rem;
- line-height: 2.25rem;
-}
-
-@media (min-width: 768px) {
- h3 {
- font-size: 2.25rem;
- line-height: 2.5rem;
- }
-}
-
-/*for unknown reasons, h4 must be undefined to not make it bigger than h3*/
-
-/*h5-h6 are ignored by tailwind typography*/
-
-img {
- --tw-drop-shadow: drop-shadow(0 10px 8px rgb(0 0 0 / 0.04)) drop-shadow(0 4px 3px rgb(0 0 0 / 0.1));
- filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
-}
-
-dd {
- margin-left: 3rem;
-}
-
-@media (min-width: 768px) {
- dd {
- margin-left: 6rem;
- }
-}
-
-ul {
- list-style-type: disc;
- padding-left: 1rem;
-}
-
-/* pelican tag_cloud */
-
-ul.tagcloud {
- list-style-type: none;
- padding: 0px;
-}
-
-ul.tagcloud li {
- display: inline-block;
- padding: 0.25rem;
-}
-
-/* Preflight is injected here */
-
-.prose {
- color: #575279;
- max-width: 80ch;
-}
-
-.prose :where(p):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 1.25em;
- margin-bottom: 1.25em;
-}
-
-.prose :where([class~="lead"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- color: var(--tw-prose-lead);
- font-size: 1.25em;
- line-height: 1.6;
- margin-top: 1.2em;
- margin-bottom: 1.2em;
-}
-
-.prose :where(a):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- color: #575279;
- text-decoration: underline;
- font-weight: 500;
- text-decoration-color: #ea9d34;
- text-decoration-thickness: 2px;
-}
-
-.prose :where(strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- color: #575279;
- font-weight: 800;
-}
-
-.prose :where(a strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- color: inherit;
-}
-
-.prose :where(blockquote strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- color: inherit;
-}
-
-.prose :where(thead th strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- color: inherit;
-}
-
-.prose :where(ol):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- list-style-type: decimal;
- margin-top: 1.25em;
- margin-bottom: 1.25em;
- padding-inline-start: 1.625em;
-}
-
-.prose :where(ol[type="A"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- list-style-type: upper-alpha;
-}
-
-.prose :where(ol[type="a"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- list-style-type: lower-alpha;
-}
-
-.prose :where(ol[type="A" s]):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- list-style-type: upper-alpha;
-}
-
-.prose :where(ol[type="a" s]):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- list-style-type: lower-alpha;
-}
-
-.prose :where(ol[type="I"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- list-style-type: upper-roman;
-}
-
-.prose :where(ol[type="i"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- list-style-type: lower-roman;
-}
-
-.prose :where(ol[type="I" s]):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- list-style-type: upper-roman;
-}
-
-.prose :where(ol[type="i" s]):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- list-style-type: lower-roman;
-}
-
-.prose :where(ol[type="1"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- list-style-type: decimal;
-}
-
-.prose :where(ul):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- list-style-type: disc;
- margin-top: 1.25em;
- margin-bottom: 1.25em;
- padding-inline-start: 1.625em;
-}
-
-.prose :where(ol > li):not(:where([class~="not-prose"],[class~="not-prose"] *))::marker {
- font-weight: 400;
- color: var(--tw-prose-counters);
-}
-
-.prose :where(ul > li):not(:where([class~="not-prose"],[class~="not-prose"] *))::marker {
- color: var(--tw-prose-bullets);
-}
-
-.prose :where(dt):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- color: var(--tw-prose-headings);
- font-weight: 600;
- margin-top: 1.25em;
-}
-
-.prose :where(hr):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- border-color: var(--tw-prose-hr);
- border-top-width: 1px;
- margin-top: 3em;
- margin-bottom: 3em;
-}
-
-.prose :where(blockquote):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- font-weight: 500;
- font-style: italic;
- color: #575279;
- border-inline-start-width: 0.25rem;
- border-inline-start-color: var(--tw-prose-quote-borders);
- quotes: "\201C""\201D""\2018""\2019";
- margin-top: 1.6em;
- margin-bottom: 1.6em;
- padding-inline-start: 1em;
- border-color: #9893a5;
- background-color: #f2e9e1;
-}
-
-.prose :where(blockquote p:first-of-type):not(:where([class~="not-prose"],[class~="not-prose"] *))::before {
- content: open-quote;
-}
-
-.prose :where(blockquote p:last-of-type):not(:where([class~="not-prose"],[class~="not-prose"] *))::after {
- content: close-quote;
-}
-
-.prose :where(h1):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- color: var(--tw-prose-headings);
- font-weight: 800;
- font-size: 2.25em;
- margin-top: 0;
- margin-bottom: 0.8888889em;
- line-height: 1.1111111;
-}
-
-.prose :where(h1 strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- font-weight: 900;
- color: inherit;
-}
-
-.prose :where(h2):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- color: var(--tw-prose-headings);
- font-weight: 700;
- font-size: 1.5em;
- margin-top: 2em;
- margin-bottom: 1em;
- line-height: 1.3333333;
-}
-
-.prose :where(h2 strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- font-weight: 800;
- color: inherit;
-}
-
-.prose :where(h3):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- color: var(--tw-prose-headings);
- font-weight: 600;
- font-size: 1.25em;
- margin-top: 1.6em;
- margin-bottom: 0.6em;
- line-height: 1.6;
-}
-
-.prose :where(h3 strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- font-weight: 700;
- color: inherit;
-}
-
-.prose :where(h4):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- color: var(--tw-prose-headings);
- font-weight: 600;
- margin-top: 1.5em;
- margin-bottom: 0.5em;
- line-height: 1.5;
-}
-
-.prose :where(h4 strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- font-weight: 700;
- color: inherit;
-}
-
-.prose :where(img):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 2em;
- margin-bottom: 2em;
-}
-
-.prose :where(picture):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- display: block;
- margin-top: 2em;
- margin-bottom: 2em;
-}
-
-.prose :where(video):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 2em;
- margin-bottom: 2em;
-}
-
-.prose :where(kbd):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- font-weight: 500;
- font-family: inherit;
- color: var(--tw-prose-kbd);
- box-shadow: 0 0 0 1px rgb(var(--tw-prose-kbd-shadows) / 10%), 0 3px 0 rgb(var(--tw-prose-kbd-shadows) / 10%);
- font-size: 0.875em;
- border-radius: 0.3125rem;
- padding-top: 0.1875em;
- padding-inline-end: 0.375em;
- padding-bottom: 0.1875em;
- padding-inline-start: 0.375em;
-}
-
-.prose :where(code):not(:where([class~="not-prose"],[class~="not-prose"] *))::before {
- content: "";
-}
-
-.prose :where(code):not(:where([class~="not-prose"],[class~="not-prose"] *))::after {
- content: "";
-}
-
-.prose :where(a code):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- color: inherit;
-}
-
-.prose :where(h1 code):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- color: inherit;
-}
-
-.prose :where(h2 code):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- color: inherit;
- font-size: 0.875em;
-}
-
-.prose :where(h3 code):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- color: inherit;
- font-size: 0.9em;
-}
-
-.prose :where(h4 code):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- color: inherit;
-}
-
-.prose :where(blockquote code):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- color: inherit;
-}
-
-.prose :where(thead th code):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- color: inherit;
-}
-
-.prose :where(table):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- width: 100%;
- table-layout: auto;
- margin-top: 2em;
- margin-bottom: 2em;
- font-size: 0.875em;
- line-height: 1.7142857;
-}
-
-.prose :where(thead):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- border-bottom-width: 1px;
- border-bottom-color: var(--tw-prose-th-borders);
-}
-
-.prose :where(thead th):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- color: var(--tw-prose-headings);
- font-weight: 600;
- vertical-align: bottom;
- padding-inline-end: 0.5714286em;
- padding-bottom: 0.5714286em;
- padding-inline-start: 0.5714286em;
-}
-
-.prose :where(tbody tr):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- border-bottom-width: 1px;
- border-bottom-color: var(--tw-prose-td-borders);
-}
-
-.prose :where(tbody tr:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- border-bottom-width: 0;
-}
-
-.prose :where(tbody td):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- vertical-align: baseline;
-}
-
-.prose :where(tfoot):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- border-top-width: 1px;
- border-top-color: var(--tw-prose-th-borders);
-}
-
-.prose :where(tfoot td):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- vertical-align: top;
-}
-
-.prose :where(th, td):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- text-align: start;
-}
-
-.prose :where(figure > *):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 0;
- margin-bottom: 0;
-}
-
-.prose :where(figcaption):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- color: var(--tw-prose-captions);
- font-size: 0.875em;
- line-height: 1.4285714;
- margin-top: 0.8571429em;
-}
-
-.prose {
- --tw-prose-body: #374151;
- --tw-prose-headings: #111827;
- --tw-prose-lead: #4b5563;
- --tw-prose-links: #111827;
- --tw-prose-bold: #111827;
- --tw-prose-counters: #6b7280;
- --tw-prose-bullets: #d1d5db;
- --tw-prose-hr: #e5e7eb;
- --tw-prose-quotes: #111827;
- --tw-prose-quote-borders: #e5e7eb;
- --tw-prose-captions: #6b7280;
- --tw-prose-kbd: #111827;
- --tw-prose-kbd-shadows: 17 24 39;
- --tw-prose-code: #111827;
- --tw-prose-pre-code: #e5e7eb;
- --tw-prose-pre-bg: #1f2937;
- --tw-prose-th-borders: #d1d5db;
- --tw-prose-td-borders: #e5e7eb;
- --tw-prose-invert-body: #d1d5db;
- --tw-prose-invert-headings: #fff;
- --tw-prose-invert-lead: #9ca3af;
- --tw-prose-invert-links: #fff;
- --tw-prose-invert-bold: #fff;
- --tw-prose-invert-counters: #9ca3af;
- --tw-prose-invert-bullets: #4b5563;
- --tw-prose-invert-hr: #374151;
- --tw-prose-invert-quotes: #f3f4f6;
- --tw-prose-invert-quote-borders: #374151;
- --tw-prose-invert-captions: #9ca3af;
- --tw-prose-invert-kbd: #fff;
- --tw-prose-invert-kbd-shadows: 255 255 255;
- --tw-prose-invert-code: #fff;
- --tw-prose-invert-pre-code: #d1d5db;
- --tw-prose-invert-pre-bg: rgb(0 0 0 / 50%);
- --tw-prose-invert-th-borders: #4b5563;
- --tw-prose-invert-td-borders: #374151;
- font-size: 1rem;
- line-height: 1.75;
-}
-
-.prose :where(picture > img):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 0;
- margin-bottom: 0;
-}
-
-.prose :where(li):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 0.5em;
- margin-bottom: 0.5em;
-}
-
-.prose :where(ol > li):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- padding-inline-start: 0.375em;
-}
-
-.prose :where(ul > li):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- padding-inline-start: 0.375em;
-}
-
-.prose :where(.prose > ul > li p):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 0.75em;
- margin-bottom: 0.75em;
-}
-
-.prose :where(.prose > ul > li > p:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 1.25em;
-}
-
-.prose :where(.prose > ul > li > p:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-bottom: 1.25em;
-}
-
-.prose :where(.prose > ol > li > p:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 1.25em;
-}
-
-.prose :where(.prose > ol > li > p:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-bottom: 1.25em;
-}
-
-.prose :where(ul ul, ul ol, ol ul, ol ol):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 0.75em;
- margin-bottom: 0.75em;
-}
-
-.prose :where(dl):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 1.25em;
- margin-bottom: 1.25em;
-}
-
-.prose :where(dd):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 0.5em;
- padding-inline-start: 1.625em;
-}
-
-.prose :where(hr + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 0;
-}
-
-.prose :where(h2 + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 0;
-}
-
-.prose :where(h3 + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 0;
-}
-
-.prose :where(h4 + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 0;
-}
-
-.prose :where(thead th:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- padding-inline-start: 0;
-}
-
-.prose :where(thead th:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- padding-inline-end: 0;
-}
-
-.prose :where(tbody td, tfoot td):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- padding-top: 0.5714286em;
- padding-inline-end: 0.5714286em;
- padding-bottom: 0.5714286em;
- padding-inline-start: 0.5714286em;
-}
-
-.prose :where(tbody td:first-child, tfoot td:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- padding-inline-start: 0;
-}
-
-.prose :where(tbody td:last-child, tfoot td:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- padding-inline-end: 0;
-}
-
-.prose :where(figure):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 2em;
- margin-bottom: 2em;
-}
-
-.prose :where(.prose > :first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 0;
-}
-
-.prose :where(.prose > :last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-bottom: 0;
-}
-
-.prose :where(h1, h2, h3, h4, h5, h6):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- color: #575279;
-}
-
-.prose :where(th):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- color: #575279;
-}
-
-/*This does not seem to affect prismjs which is good*/
-
-code {
- --tw-bg-opacity: 1;
- background-color: rgb(244 237 232 / var(--tw-bg-opacity, 1));
-}
-
-@media (prefers-color-scheme: dark) {
- code {
- --tw-bg-opacity: 1;
- background-color: rgb(42 40 62 / var(--tw-bg-opacity, 1));
- }
-}
-
-.absolute {
- position: absolute;
-}
-
-.relative {
- position: relative;
-}
-
-.z-10 {
- z-index: 10;
-}
-
-.z-20 {
- z-index: 20;
-}
-
-.col-span-1 {
- grid-column: span 1 / span 1;
-}
-
-.col-span-2 {
- grid-column: span 2 / span 2;
-}
-
-.col-span-3 {
- grid-column: span 3 / span 3;
-}
-
-.col-span-9 {
- grid-column: span 9 / span 9;
-}
-
-.m-10 {
- margin: 2.5rem;
-}
-
-.m-4 {
- margin: 1rem;
-}
-
-.-mt-11 {
- margin-top: -2.75rem;
-}
-
-.mb-2 {
- margin-bottom: 0.5rem;
-}
-
-.mb-3 {
- margin-bottom: 0.75rem;
-}
-
-.ml-14 {
- margin-left: 3.5rem;
-}
-
-.mt-6 {
- margin-top: 1.5rem;
-}
-
-.block {
- display: block;
-}
-
-.inline-block {
- display: inline-block;
-}
-
-.flex {
- display: flex;
-}
-
-.grid {
- display: grid;
-}
-
-.inline-grid {
- display: inline-grid;
-}
-
-.size-5 {
- width: 1.25rem;
- height: 1.25rem;
-}
-
-.w-12 {
- width: 3rem;
-}
-
-.w-8 {
- width: 2rem;
-}
-
-.-rotate-3 {
- --tw-rotate: -3deg;
- transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
-}
-
-.list-outside {
- list-style-position: outside;
-}
-
-.list-disc {
- list-style-type: disc;
-}
-
-.list-none {
- list-style-type: none;
-}
-
-.grid-cols-1 {
- grid-template-columns: repeat(1, minmax(0, 1fr));
-}
-
-.grid-cols-3 {
- grid-template-columns: repeat(3, minmax(0, 1fr));
-}
-
-.grid-cols-9 {
- grid-template-columns: repeat(9, minmax(0, 1fr));
-}
-
-.grid-cols-\[max-content_max-content_max-content_max-content_max-content\] {
- grid-template-columns: max-content max-content max-content max-content max-content;
-}
-
-.grid-cols-\[max-content_max-content_max-content_max-content_max-content_max-content\] {
- grid-template-columns: max-content max-content max-content max-content max-content max-content;
-}
-
-.items-center {
- align-items: center;
-}
-
-.justify-center {
- justify-content: center;
-}
-
-.gap-8 {
- gap: 2rem;
-}
-
-.hyphens-auto {
- -webkit-hyphens: auto;
- hyphens: auto;
-}
-
-.rounded-full {
- border-radius: 9999px;
-}
-
-.border-b-2 {
- border-bottom-width: 2px;
-}
-
-.border-t-2 {
- border-top-width: 2px;
-}
-
-.border-rp-dawn-gold {
- --tw-border-opacity: 1;
- border-color: rgb(234 157 52 / var(--tw-border-opacity, 1));
-}
-
-.border-rp-dawn-overlay {
- --tw-border-opacity: 1;
- border-color: rgb(242 233 225 / var(--tw-border-opacity, 1));
-}
-
-.bg-rp-dawn-base {
- --tw-bg-opacity: 1;
- background-color: rgb(250 244 237 / var(--tw-bg-opacity, 1));
-}
-
-.bg-rp-dawn-gold {
- --tw-bg-opacity: 1;
- background-color: rgb(234 157 52 / var(--tw-bg-opacity, 1));
-}
-
-.bg-rp-dawn-overlay {
- --tw-bg-opacity: 1;
- background-color: rgb(242 233 225 / var(--tw-bg-opacity, 1));
-}
-
-.bg-rp-dawn-surface {
- --tw-bg-opacity: 1;
- background-color: rgb(255 250 243 / var(--tw-bg-opacity, 1));
-}
-
-.bg-gradient-to-r {
- background-image: linear-gradient(to right, var(--tw-gradient-stops));
-}
-
-.from-rp-dawn-gold {
- --tw-gradient-from: #ea9d34 var(--tw-gradient-from-position);
- --tw-gradient-to: rgb(234 157 52 / 0) var(--tw-gradient-to-position);
- --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
-}
-
-.from-10\% {
- --tw-gradient-from-position: 10%;
-}
-
-.to-rp-dawn-pine {
- --tw-gradient-to: #286983 var(--tw-gradient-to-position);
-}
-
-.to-90\% {
- --tw-gradient-to-position: 90%;
-}
-
-.p-1 {
- padding: 0.25rem;
-}
-
-.p-3 {
- padding: 0.75rem;
-}
-
-.p-4 {
- padding: 1rem;
-}
-
-.p-8 {
- padding: 2rem;
-}
-
-.px-2 {
- padding-left: 0.5rem;
- padding-right: 0.5rem;
-}
-
-.pb-4 {
- padding-bottom: 1rem;
-}
-
-.pb-60 {
- padding-bottom: 15rem;
-}
-
-.pl-4 {
- padding-left: 1rem;
-}
-
-.pt-20 {
- padding-top: 5rem;
-}
-
-.text-center {
- text-align: center;
-}
-
-.align-top {
- vertical-align: top;
-}
-
-.text-2xl {
- font-size: 1.5rem;
- line-height: 2rem;
-}
-
-.text-3xl {
- font-size: 1.875rem;
- line-height: 2.25rem;
-}
-
-.text-4xl {
- font-size: 2.25rem;
- line-height: 2.5rem;
-}
-
-.text-base {
- font-size: 1rem;
- line-height: 1.5rem;
-}
-
-.text-sm {
- font-size: 0.875rem;
- line-height: 1.25rem;
-}
-
-.text-xl {
- font-size: 1.25rem;
- line-height: 1.75rem;
-}
-
-.font-bold {
- font-weight: 700;
-}
-
-.uppercase {
- text-transform: uppercase;
-}
-
-.capitalize {
- text-transform: capitalize;
-}
-
-.italic {
- font-style: italic;
-}
-
-.text-rp-dawn-foam {
- --tw-text-opacity: 1;
- color: rgb(86 148 159 / var(--tw-text-opacity, 1));
-}
-
-.text-rp-dawn-overlay {
- --tw-text-opacity: 1;
- color: rgb(242 233 225 / var(--tw-text-opacity, 1));
-}
-
-.text-rp-dawn-pine {
- --tw-text-opacity: 1;
- color: rgb(40 105 131 / var(--tw-text-opacity, 1));
-}
-
-.text-rp-dawn-text {
- --tw-text-opacity: 1;
- color: rgb(87 82 121 / var(--tw-text-opacity, 1));
-}
-
-.no-underline {
- text-decoration-line: none;
-}
-
-.shadow {
- --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
- --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
- box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
-}
-
-/*Hide heading anchor links unless hovering over them*/
-
-.headerlink {
- margin-left: 0.25rem;
- --tw-text-opacity: 1;
- color: rgb(180 99 122 / var(--tw-text-opacity, 1));
- text-decoration-line: none;
-}
-
-.headerlink:hover {
- text-decoration-line: none;
-}
-
-@media (min-width: 768px) {
- .headerlink {
- margin-left: 0.5rem;
- }
-}
-
-.note {
- margin: 2rem;
- --tw-bg-opacity: 1;
- background-color: rgb(223 218 217 / var(--tw-bg-opacity, 1));
- padding: 1rem;
-}
-
-@media (prefers-color-scheme: dark) {
- .note {
- --tw-bg-opacity: 1;
- background-color: rgb(68 65 90 / var(--tw-bg-opacity, 1));
- }
-}
-
-.warn {
- margin: 2rem;
- --tw-bg-opacity: 1;
- background-color: rgb(87 82 121 / var(--tw-bg-opacity, 1));
- padding: 1rem;
- --tw-text-opacity: 1;
- color: rgb(250 244 237 / var(--tw-text-opacity, 1));
-}
-
-@media (prefers-color-scheme: dark) {
- .warn {
- --tw-bg-opacity: 1;
- background-color: rgb(224 222 244 / var(--tw-bg-opacity, 1));
- --tw-text-opacity: 1;
- color: rgb(35 33 54 / var(--tw-text-opacity, 1));
- }
-}
-
-/*Attempt to only float thumbnails to the right*/
-
-.image-process-article-image,
-.image-process-thumb,
-.image-process-responsive {
- /*@apply lg:float-right p-2 lg:p-4;*/
- padding-left: 0.25rem;
- padding-right: 0.25rem;
-}
-
-@media (min-width: 1024px) {
- .image-process-article-image,
-.image-process-thumb,
-.image-process-responsive {
- float: right;
- padding-left: 0.75rem;
- padding-right: 0.75rem;
- }
-}
-
-#skiptocontent a {
- position: absolute;
- top: -40px;
- left: 0px;
- /*color: white;*/
- /*border-right: 1px solid white;*/
- /*border-bottom: 1px solid white;*/
- z-index: 100;
- --tw-bg-opacity: 1;
- background-color: rgb(87 82 121 / var(--tw-bg-opacity, 1));
- --tw-text-opacity: 1;
- color: rgb(255 250 243 / var(--tw-text-opacity, 1));
-}
-
-@media (prefers-color-scheme: dark) {
- #skiptocontent a {
- --tw-bg-opacity: 1;
- background-color: rgb(224 222 244 / var(--tw-bg-opacity, 1));
- --tw-text-opacity: 1;
- color: rgb(42 39 63 / var(--tw-text-opacity, 1));
- }
-}
-
-#skiptocontent a:focus {
- position: absolute;
- left: 0px;
- top: 0px;
- outline-color: transparent;
-}
-
-@media (min-width: 768px) {
- .md\:prose-base {
- font-size: 1rem;
- line-height: 1.75;
- }
-
- .md\:prose-base :where(p):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 1.25em;
- margin-bottom: 1.25em;
- }
-
- .md\:prose-base :where([class~="lead"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- font-size: 1.25em;
- line-height: 1.6;
- margin-top: 1.2em;
- margin-bottom: 1.2em;
- }
-
- .md\:prose-base :where(blockquote):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 1.6em;
- margin-bottom: 1.6em;
- padding-inline-start: 1em;
- }
-
- .md\:prose-base :where(h1):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- font-size: 2.25em;
- margin-top: 0;
- margin-bottom: 0.8888889em;
- line-height: 1.1111111;
- }
-
- .md\:prose-base :where(h2):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- font-size: 1.5em;
- margin-top: 2em;
- margin-bottom: 1em;
- line-height: 1.3333333;
- }
-
- .md\:prose-base :where(h3):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- font-size: 1.25em;
- margin-top: 1.6em;
- margin-bottom: 0.6em;
- line-height: 1.6;
- }
-
- .md\:prose-base :where(h4):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 1.5em;
- margin-bottom: 0.5em;
- line-height: 1.5;
- }
-
- .md\:prose-base :where(img):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 2em;
- margin-bottom: 2em;
- }
-
- .md\:prose-base :where(picture):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 2em;
- margin-bottom: 2em;
- }
-
- .md\:prose-base :where(picture > img):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 0;
- margin-bottom: 0;
- }
-
- .md\:prose-base :where(video):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 2em;
- margin-bottom: 2em;
- }
-
- .md\:prose-base :where(kbd):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- font-size: 0.875em;
- border-radius: 0.3125rem;
- padding-top: 0.1875em;
- padding-inline-end: 0.375em;
- padding-bottom: 0.1875em;
- padding-inline-start: 0.375em;
- }
-
- .md\:prose-base :where(code):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- font-size: 0.875em;
- }
-
- .md\:prose-base :where(h2 code):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- font-size: 0.875em;
- }
-
- .md\:prose-base :where(h3 code):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- font-size: 0.9em;
- }
-
- .md\:prose-base :where(pre):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- font-size: 0.875em;
- line-height: 1.7142857;
- margin-top: 1.7142857em;
- margin-bottom: 1.7142857em;
- border-radius: 0.375rem;
- padding-top: 0.8571429em;
- padding-inline-end: 1.1428571em;
- padding-bottom: 0.8571429em;
- padding-inline-start: 1.1428571em;
- }
-
- .md\:prose-base :where(ol):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 1.25em;
- margin-bottom: 1.25em;
- padding-inline-start: 1.625em;
- }
-
- .md\:prose-base :where(ul):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 1.25em;
- margin-bottom: 1.25em;
- padding-inline-start: 1.625em;
- }
-
- .md\:prose-base :where(li):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 0.5em;
- margin-bottom: 0.5em;
- }
-
- .md\:prose-base :where(ol > li):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- padding-inline-start: 0.375em;
- }
-
- .md\:prose-base :where(ul > li):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- padding-inline-start: 0.375em;
- }
-
- .md\:prose-base :where(.md\:prose-base > ul > li p):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 0.75em;
- margin-bottom: 0.75em;
- }
-
- .md\:prose-base :where(.md\:prose-base > ul > li > p:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 1.25em;
- }
-
- .md\:prose-base :where(.md\:prose-base > ul > li > p:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-bottom: 1.25em;
- }
-
- .md\:prose-base :where(.md\:prose-base > ol > li > p:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 1.25em;
- }
-
- .md\:prose-base :where(.md\:prose-base > ol > li > p:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-bottom: 1.25em;
- }
-
- .md\:prose-base :where(ul ul, ul ol, ol ul, ol ol):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 0.75em;
- margin-bottom: 0.75em;
- }
-
- .md\:prose-base :where(dl):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 1.25em;
- margin-bottom: 1.25em;
- }
-
- .md\:prose-base :where(dt):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 1.25em;
- }
-
- .md\:prose-base :where(dd):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 0.5em;
- padding-inline-start: 1.625em;
- }
-
- .md\:prose-base :where(hr):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 3em;
- margin-bottom: 3em;
- }
-
- .md\:prose-base :where(hr + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 0;
- }
-
- .md\:prose-base :where(h2 + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 0;
- }
-
- .md\:prose-base :where(h3 + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 0;
- }
-
- .md\:prose-base :where(h4 + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 0;
- }
-
- .md\:prose-base :where(table):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- font-size: 0.875em;
- line-height: 1.7142857;
- }
-
- .md\:prose-base :where(thead th):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- padding-inline-end: 0.5714286em;
- padding-bottom: 0.5714286em;
- padding-inline-start: 0.5714286em;
- }
-
- .md\:prose-base :where(thead th:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- padding-inline-start: 0;
- }
-
- .md\:prose-base :where(thead th:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- padding-inline-end: 0;
- }
-
- .md\:prose-base :where(tbody td, tfoot td):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- padding-top: 0.5714286em;
- padding-inline-end: 0.5714286em;
- padding-bottom: 0.5714286em;
- padding-inline-start: 0.5714286em;
- }
-
- .md\:prose-base :where(tbody td:first-child, tfoot td:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- padding-inline-start: 0;
- }
-
- .md\:prose-base :where(tbody td:last-child, tfoot td:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- padding-inline-end: 0;
- }
-
- .md\:prose-base :where(figure):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 2em;
- margin-bottom: 2em;
- }
-
- .md\:prose-base :where(figure > *):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 0;
- margin-bottom: 0;
- }
-
- .md\:prose-base :where(figcaption):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- font-size: 0.875em;
- line-height: 1.4285714;
- margin-top: 0.8571429em;
- }
-
- .md\:prose-base :where(.md\:prose-base > :first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 0;
- }
-
- .md\:prose-base :where(.md\:prose-base > :last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-bottom: 0;
- }
-}
-
-@media (min-width: 1024px) {
- .lg\:prose-lg {
- font-size: 1.125rem;
- line-height: 1.7777778;
- }
-
- .lg\:prose-lg :where(p):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 1.3333333em;
- margin-bottom: 1.3333333em;
- }
-
- .lg\:prose-lg :where([class~="lead"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- font-size: 1.2222222em;
- line-height: 1.4545455;
- margin-top: 1.0909091em;
- margin-bottom: 1.0909091em;
- }
-
- .lg\:prose-lg :where(blockquote):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 1.6666667em;
- margin-bottom: 1.6666667em;
- padding-inline-start: 1em;
- }
-
- .lg\:prose-lg :where(h1):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- font-size: 2.6666667em;
- margin-top: 0;
- margin-bottom: 0.8333333em;
- line-height: 1;
- }
-
- .lg\:prose-lg :where(h2):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- font-size: 1.6666667em;
- margin-top: 1.8666667em;
- margin-bottom: 1.0666667em;
- line-height: 1.3333333;
- }
-
- .lg\:prose-lg :where(h3):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- font-size: 1.3333333em;
- margin-top: 1.6666667em;
- margin-bottom: 0.6666667em;
- line-height: 1.5;
- }
-
- .lg\:prose-lg :where(h4):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 1.7777778em;
- margin-bottom: 0.4444444em;
- line-height: 1.5555556;
- }
-
- .lg\:prose-lg :where(img):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 1.7777778em;
- margin-bottom: 1.7777778em;
- }
-
- .lg\:prose-lg :where(picture):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 1.7777778em;
- margin-bottom: 1.7777778em;
- }
-
- .lg\:prose-lg :where(picture > img):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 0;
- margin-bottom: 0;
- }
-
- .lg\:prose-lg :where(video):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 1.7777778em;
- margin-bottom: 1.7777778em;
- }
-
- .lg\:prose-lg :where(kbd):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- font-size: 0.8888889em;
- border-radius: 0.3125rem;
- padding-top: 0.2222222em;
- padding-inline-end: 0.4444444em;
- padding-bottom: 0.2222222em;
- padding-inline-start: 0.4444444em;
- }
-
- .lg\:prose-lg :where(code):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- font-size: 0.8888889em;
- }
-
- .lg\:prose-lg :where(h2 code):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- font-size: 0.8666667em;
- }
-
- .lg\:prose-lg :where(h3 code):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- font-size: 0.875em;
- }
-
- .lg\:prose-lg :where(pre):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- font-size: 0.8888889em;
- line-height: 1.75;
- margin-top: 2em;
- margin-bottom: 2em;
- border-radius: 0.375rem;
- padding-top: 1em;
- padding-inline-end: 1.5em;
- padding-bottom: 1em;
- padding-inline-start: 1.5em;
- }
-
- .lg\:prose-lg :where(ol):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 1.3333333em;
- margin-bottom: 1.3333333em;
- padding-inline-start: 1.5555556em;
- }
-
- .lg\:prose-lg :where(ul):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 1.3333333em;
- margin-bottom: 1.3333333em;
- padding-inline-start: 1.5555556em;
- }
-
- .lg\:prose-lg :where(li):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 0.6666667em;
- margin-bottom: 0.6666667em;
- }
-
- .lg\:prose-lg :where(ol > li):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- padding-inline-start: 0.4444444em;
- }
-
- .lg\:prose-lg :where(ul > li):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- padding-inline-start: 0.4444444em;
- }
-
- .lg\:prose-lg :where(.lg\:prose-lg > ul > li p):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 0.8888889em;
- margin-bottom: 0.8888889em;
- }
-
- .lg\:prose-lg :where(.lg\:prose-lg > ul > li > p:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 1.3333333em;
- }
-
- .lg\:prose-lg :where(.lg\:prose-lg > ul > li > p:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-bottom: 1.3333333em;
- }
-
- .lg\:prose-lg :where(.lg\:prose-lg > ol > li > p:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 1.3333333em;
- }
-
- .lg\:prose-lg :where(.lg\:prose-lg > ol > li > p:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-bottom: 1.3333333em;
- }
-
- .lg\:prose-lg :where(ul ul, ul ol, ol ul, ol ol):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 0.8888889em;
- margin-bottom: 0.8888889em;
- }
-
- .lg\:prose-lg :where(dl):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 1.3333333em;
- margin-bottom: 1.3333333em;
- }
-
- .lg\:prose-lg :where(dt):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 1.3333333em;
- }
-
- .lg\:prose-lg :where(dd):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 0.6666667em;
- padding-inline-start: 1.5555556em;
- }
-
- .lg\:prose-lg :where(hr):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 3.1111111em;
- margin-bottom: 3.1111111em;
- }
-
- .lg\:prose-lg :where(hr + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 0;
- }
-
- .lg\:prose-lg :where(h2 + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 0;
- }
-
- .lg\:prose-lg :where(h3 + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 0;
- }
-
- .lg\:prose-lg :where(h4 + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 0;
- }
-
- .lg\:prose-lg :where(table):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- font-size: 0.8888889em;
- line-height: 1.5;
- }
-
- .lg\:prose-lg :where(thead th):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- padding-inline-end: 0.75em;
- padding-bottom: 0.75em;
- padding-inline-start: 0.75em;
- }
-
- .lg\:prose-lg :where(thead th:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- padding-inline-start: 0;
- }
-
- .lg\:prose-lg :where(thead th:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- padding-inline-end: 0;
- }
-
- .lg\:prose-lg :where(tbody td, tfoot td):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- padding-top: 0.75em;
- padding-inline-end: 0.75em;
- padding-bottom: 0.75em;
- padding-inline-start: 0.75em;
- }
-
- .lg\:prose-lg :where(tbody td:first-child, tfoot td:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- padding-inline-start: 0;
- }
-
- .lg\:prose-lg :where(tbody td:last-child, tfoot td:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- padding-inline-end: 0;
- }
-
- .lg\:prose-lg :where(figure):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 1.7777778em;
- margin-bottom: 1.7777778em;
- }
-
- .lg\:prose-lg :where(figure > *):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 0;
- margin-bottom: 0;
- }
-
- .lg\:prose-lg :where(figcaption):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- font-size: 0.8888889em;
- line-height: 1.5;
- margin-top: 1em;
- }
-
- .lg\:prose-lg :where(.lg\:prose-lg > :first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-top: 0;
- }
-
- .lg\:prose-lg :where(.lg\:prose-lg > :last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- margin-bottom: 0;
- }
-}
-
-@media (prefers-color-scheme: dark) {
- .dark\:prose-dark {
- color: #c4a7e7;
- }
-
- .dark\:prose-dark :where(a):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- color: #c4a7e7;
- text-decoration-color: #3e8fb0;
- text-decoration-thickness: 2px;
- }
-
- .dark\:prose-dark :where(h1, h2, h3, h4, h5, h6):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- color: #eb6f92;
- }
-
- .dark\:prose-dark :where(blockquote):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- border-color: #393552;
- background-color: #2a273f;
- color: #e0def4;
- }
-
- .dark\:prose-dark :where(strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- color: #c4a7e7;
- font-weight: 800;
- }
-
- .dark\:prose-dark :where(th):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
- color: #c4a7e7;
- }
-}
-
-.hover\:no-underline:hover {
- text-decoration-line: none;
-}
-
-.prose-img\:mx-auto :is(:where(img):not(:where([class~="not-prose"],[class~="not-prose"] *))) {
- margin-left: auto;
- margin-right: auto;
-}
-
-.prose-img\:rounded :is(:where(img):not(:where([class~="not-prose"],[class~="not-prose"] *))) {
- border-radius: 0.25rem;
-}
-
-@media (min-width: 768px) {
- .md\:col-span-3 {
- grid-column: span 3 / span 3;
- }
-
- .md\:col-span-6 {
- grid-column: span 6 / span 6;
- }
-
- .md\:col-span-7 {
- grid-column: span 7 / span 7;
- }
-
- .md\:grid-cols-6 {
- grid-template-columns: repeat(6, minmax(0, 1fr));
- }
-
- .md\:border-l-4 {
- border-left-width: 4px;
- }
-
- .md\:text-3xl {
- font-size: 1.875rem;
- line-height: 2.25rem;
- }
-
- .md\:text-5xl {
- font-size: 3rem;
- line-height: 1;
- }
-
- .md\:text-7xl {
- font-size: 4.5rem;
- line-height: 1;
- }
-
- .md\:text-base {
- font-size: 1rem;
- line-height: 1.5rem;
- }
-
- .md\:text-xl {
- font-size: 1.25rem;
- line-height: 1.75rem;
- }
-}
-
-@media (min-width: 1024px) {
- .lg\:grid-cols-2 {
- grid-template-columns: repeat(2, minmax(0, 1fr));
- }
-
- .lg\:text-lg {
- font-size: 1.125rem;
- line-height: 1.75rem;
- }
-}
-
-@media (min-width: 1280px) {
- .xl\:grid-cols-3 {
- grid-template-columns: repeat(3, minmax(0, 1fr));
- }
-}
-
-@media (prefers-color-scheme: dark) {
- .dark\:border-rp-moon-overlay {
- --tw-border-opacity: 1;
- border-color: rgb(57 53 82 / var(--tw-border-opacity, 1));
- }
-
- .dark\:bg-rp-moon-base {
- --tw-bg-opacity: 1;
- background-color: rgb(35 33 54 / var(--tw-bg-opacity, 1));
- }
-
- .dark\:bg-rp-moon-overlay {
- --tw-bg-opacity: 1;
- background-color: rgb(57 53 82 / var(--tw-bg-opacity, 1));
- }
-
- .dark\:bg-rp-moon-pine {
- --tw-bg-opacity: 1;
- background-color: rgb(62 143 176 / var(--tw-bg-opacity, 1));
- }
-
- .dark\:bg-rp-moon-surface {
- --tw-bg-opacity: 1;
- background-color: rgb(42 39 63 / var(--tw-bg-opacity, 1));
- }
-
- .dark\:text-rp-moon-foam {
- --tw-text-opacity: 1;
- color: rgb(156 207 216 / var(--tw-text-opacity, 1));
- }
-
- .dark\:text-rp-moon-highlight-med {
- --tw-text-opacity: 1;
- color: rgb(68 65 90 / var(--tw-text-opacity, 1));
- }
-
- .dark\:text-rp-moon-iris {
- --tw-text-opacity: 1;
- color: rgb(196 167 231 / var(--tw-text-opacity, 1));
- }
-
- .dark\:text-rp-moon-rose {
- --tw-text-opacity: 1;
- color: rgb(234 154 151 / var(--tw-text-opacity, 1));
- }
-
- .dark\:text-rp-moon-surface {
- --tw-text-opacity: 1;
- color: rgb(42 39 63 / var(--tw-text-opacity, 1));
- }
-
- .dark\:text-rp-moon-text {
- --tw-text-opacity: 1;
- color: rgb(224 222 244 / var(--tw-text-opacity, 1));
- }
-}
diff --git a/static/css/prism-rose-pine-moon-alt.css b/static/css/prism-rose-pine-moon-alt.css
deleted file mode 100644
index 6ceaebae..00000000
--- a/static/css/prism-rose-pine-moon-alt.css
+++ /dev/null
@@ -1,201 +0,0 @@
-/**
- * MIT License
- * Rosé Pine Theme
- * https://github.com/rose-pine
- * Ported for PrismJS by fvrests [@fvrests]
- */
-
-code[class*="language-"],
-pre[class*="language-"] {
- color: #e0def4;
- background: #2a273f;
- font-family: "Cartograph CF", ui-monospace, SFMono-Regular, Menlo, Monaco,
- Consolas, "Liberation Mono", "Courier New", monospace;
- text-align: left;
- white-space: pre;
- word-spacing: normal;
- word-break: normal;
- word-wrap: normal;
- line-height: 1.5;
-
- -moz-tab-size: 4;
- -o-tab-size: 4;
- tab-size: 4;
-
- -webkit-hyphens: none;
- -moz-hyphens: none;
- -ms-hyphens: none;
- hyphens: none;
-
- @media print {
- text-shadow: none;
- }
-}
-
-/* Selection */
-code[class*="language-"]::-moz-selection,
-pre[class*="language-"]::-moz-selection,
-code[class*="language-"] ::-moz-selection,
-pre[class*="language-"] ::-moz-selection {
- background: #44415a;
-}
-
-code[class*="language-"]::selection,
-pre[class*="language-"]::selection,
-code[class*="language-"] ::selection,
-pre[class*="language-"] ::selection {
- background: #44415a;
-}
-
-/* Code (block & inline) */
-:not(pre) > code[class*="language-"],
-pre[class*="language-"] {
- color: #e0def4;
- background: #2a273f;
-}
-
-/* Code blocks */
-pre[class*="language-"] {
- padding: 1em;
- margin: 0.5em 0;
- overflow: auto;
-}
-
-/* Inline code */
-:not(pre) > code[class*="language-"] {
- padding: 0.1em;
- border-radius: 0.3em;
- white-space: normal;
- color: #e0def4;
- background: #2a273f;
-}
-
-/* Text style & opacity */
-.token.entity {
- cursor: help;
-}
-
-.token.important,
-.token.bold {
- font-weight: bold;
-}
-
-.token.italic,
-.token.selector,
-.token.doctype,
-.token.attr-name,
-.token.inserted,
-.token.deleted,
-.token.comment,
-.token.prolog,
-.token.cdata,
-.token.constant,
-.token.parameter,
-.token.url {
- font-style: italic;
-}
-
-.token.url {
- text-decoration: underline;
-}
-
-.namespace {
- opacity: 0.7;
-}
-
-/* Syntax highlighting */
-.token.constant {
- color: #e0def4;
-}
-
-.token.comment,
-.token.prolog,
-.token.cdata,
-.token.punctuation {
- color: #908caa;
-}
-
-.token.delimiter,
-.token.important,
-.token.atrule,
-.token.operator,
-.token.keyword {
- color: #3e8fb0;
-}
-
-.token.tag,
-.token.tag .punctuation,
-.token.doctype,
-.token.variable,
-.token.regex,
-.token.class-name,
-.token.selector,
-.token.inserted {
- color: #9ccfd8;
-}
-
-.token.boolean,
-.token.entity,
-.token.number,
-.token.symbol,
-.token.function {
- color: #ea9a97;
-}
-
-.token.string,
-.token.char,
-.token.property,
-.token.attr-value,
-.token.attr-value .punctuation {
- color: #f6c177;
-}
-
-.token.parameter,
-.token.url,
-.token.name,
-.token.attr-name,
-.token.builtin {
- color: #c4a7e7;
-}
-
-.token.deleted {
- color: #eb6f92;
-}
-
-/* Insertions & deletions */
-.token.inserted {
- background: rgba(156 207 216 0.12);
-}
-
-.token.deleted {
- background: rgba(235 111 146 0.12);
-}
-
-/* Line highlighting */
-pre[data-line] {
- position: relative;
-}
-
-pre[class*="language-"] > code[class*="language-"] {
- position: relative;
- z-index: 1;
-}
-
-.line-highlight,
-.highlight-lines .highlighted {
- position: absolute;
- left: 0;
- right: 0;
- padding: inherit 0;
- margin-top: 1em;
-
- background: #44415a;
- box-shadow: inset 5px 0 0 #e0def4;
-
- z-index: 0;
-
- pointer-events: none;
-
- line-height: inherit;
- white-space: pre;
-}
diff --git a/static/css/prism-rose-pine-moon.css b/static/css/prism-rose-pine-moon.css
deleted file mode 100644
index fd4353cc..00000000
--- a/static/css/prism-rose-pine-moon.css
+++ /dev/null
@@ -1,201 +0,0 @@
-/**
- * MIT License
- * Rosé Pine Theme
- * https://github.com/rose-pine
- * Ported for PrismJS by fvrests [@fvrests]
- */
-
-code[class*="language-"],
-pre[class*="language-"] {
- color: #e0def4;
- background: #232136;
- font-family: "Cartograph CF", ui-monospace, SFMono-Regular, Menlo, Monaco,
- Consolas, "Liberation Mono", "Courier New", monospace;
- text-align: left;
- white-space: pre;
- word-spacing: normal;
- word-break: normal;
- word-wrap: normal;
- line-height: 1.5;
-
- -moz-tab-size: 4;
- -o-tab-size: 4;
- tab-size: 4;
-
- -webkit-hyphens: none;
- -moz-hyphens: none;
- -ms-hyphens: none;
- hyphens: none;
-
- @media print {
- text-shadow: none;
- }
-}
-
-/* Selection */
-code[class*="language-"]::-moz-selection,
-pre[class*="language-"]::-moz-selection,
-code[class*="language-"] ::-moz-selection,
-pre[class*="language-"] ::-moz-selection {
- background: #44415a;
-}
-
-code[class*="language-"]::selection,
-pre[class*="language-"]::selection,
-code[class*="language-"] ::selection,
-pre[class*="language-"] ::selection {
- background: #44415a;
-}
-
-/* Code (block & inline) */
-:not(pre) > code[class*="language-"],
-pre[class*="language-"] {
- color: #e0def4;
- background: #232136;
-}
-
-/* Code blocks */
-pre[class*="language-"] {
- padding: 1em;
- margin: 0.5em 0;
- overflow: auto;
-}
-
-/* Inline code */
-:not(pre) > code[class*="language-"] {
- padding: 0.1em;
- border-radius: 0.3em;
- white-space: normal;
- color: #e0def4;
- background: #232136;
-}
-
-/* Text style & opacity */
-.token.entity {
- cursor: help;
-}
-
-.token.important,
-.token.bold {
- font-weight: bold;
-}
-
-.token.italic,
-.token.selector,
-.token.doctype,
-.token.attr-name,
-.token.inserted,
-.token.deleted,
-.token.comment,
-.token.prolog,
-.token.cdata,
-.token.constant,
-.token.parameter,
-.token.url {
- font-style: italic;
-}
-
-.token.url {
- text-decoration: underline;
-}
-
-.namespace {
- opacity: 0.7;
-}
-
-/* Syntax highlighting */
-.token.constant {
- color: #e0def4;
-}
-
-.token.comment,
-.token.prolog,
-.token.cdata,
-.token.punctuation {
- color: #908caa;
-}
-
-.token.delimiter,
-.token.important,
-.token.atrule,
-.token.operator,
-.token.keyword {
- color: #3e8fb0;
-}
-
-.token.tag,
-.token.tag .punctuation,
-.token.doctype,
-.token.variable,
-.token.regex,
-.token.class-name,
-.token.selector,
-.token.inserted {
- color: #9ccfd8;
-}
-
-.token.boolean,
-.token.entity,
-.token.number,
-.token.symbol,
-.token.function {
- color: #ea9a97;
-}
-
-.token.string,
-.token.char,
-.token.property,
-.token.attr-value,
-.token.attr-value .punctuation {
- color: #f6c177;
-}
-
-.token.parameter,
-.token.url,
-.token.name,
-.token.attr-name,
-.token.builtin {
- color: #c4a7e7;
-}
-
-.token.deleted {
- color: #eb6f92;
-}
-
-/* Insertions & deletions */
-.token.inserted {
- background: rgba(156 207 216 0.12);
-}
-
-.token.deleted {
- background: rgba(235 111 146 0.12);
-}
-
-/* Line highlighting */
-pre[data-line] {
- position: relative;
-}
-
-pre[class*="language-"] > code[class*="language-"] {
- position: relative;
- z-index: 1;
-}
-
-.line-highlight,
-.highlight-lines .highlighted {
- position: absolute;
- left: 0;
- right: 0;
- padding: inherit 0;
- margin-top: 1em;
-
- background: #44415a;
- box-shadow: inset 5px 0 0 #e0def4;
-
- z-index: 0;
-
- pointer-events: none;
-
- line-height: inherit;
- white-space: pre;
-}
diff --git a/static/css/prism.css b/static/css/prism.css
deleted file mode 100644
index 6d73b1fb..00000000
--- a/static/css/prism.css
+++ /dev/null
@@ -1,3 +0,0 @@
-/* PrismJS 1.29.0
-https://prismjs.com/download.html?themes#themes=prism&languages=markup+css+clike+javascript+bash+diff+django+dns-zone-file+docker+dot+elixir+erlang+git+go+go-module+hcl+ini+jq+lua+makefile+markdown+markup-templating+nginx+properties+python+r+rest+sql+toml+vim+yaml */
-code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}
diff --git a/static/font/C64_Pro_Mono-STYLE.woff2 b/static/font/C64_Pro_Mono-STYLE.woff2
deleted file mode 100644
index a1b72c55..00000000
Binary files a/static/font/C64_Pro_Mono-STYLE.woff2 and /dev/null differ
diff --git a/static/js/prism.js b/static/js/prism.js
deleted file mode 100644
index f65c9412..00000000
--- a/static/js/prism.js
+++ /dev/null
@@ -1,34 +0,0 @@
-/* PrismJS 1.29.0
-https://prismjs.com/download.html?themes#themes=prism&languages=markup+css+clike+javascript+bash+diff+django+dns-zone-file+docker+dot+elixir+erlang+git+go+go-module+hcl+ini+jq+lua+makefile+markdown+markup-templating+nginx+properties+python+r+rest+sql+toml+vim+yaml */
-var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(e){var n=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,t=0,r={},a={manual:e.Prism&&e.Prism.manual,disableWorkerMessageHandler:e.Prism&&e.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof i?new i(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&").replace(/=g.reach);A+=w.value.length,w=w.next){var E=w.value;if(n.length>e.length)return;if(!(E instanceof i)){var P,L=1;if(y){if(!(P=l(b,A,e,m))||P.index>=e.length)break;var S=P.index,O=P.index+P[0].length,j=A;for(j+=w.value.length;S>=j;)j+=(w=w.next).value.length;if(A=j-=w.value.length,w.value instanceof i)continue;for(var C=w;C!==n.tail&&(jg.reach&&(g.reach=W);var z=w.prev;if(_&&(z=u(n,z,_),A+=_.length),c(n,z,L),w=u(n,z,new i(f,p?a.tokenize(N,p):N,k,N)),M&&u(n,w,M),L>1){var I={cause:f+","+d,reach:W};o(e,n,t,w.prev,A,I),g&&I.reach>g.reach&&(g.reach=I.reach)}}}}}}function s(){var e={value:null,prev:null,next:null},n={value:null,prev:e,next:null};e.next=n,this.head=e,this.tail=n,this.length=0}function u(e,n,t){var r=n.next,a={value:t,prev:n,next:r};return n.next=a,r.prev=a,e.length++,a}function c(e,n,t){for(var r=n.next,a=0;a"+i.content+""+i.tag+">"},!e.document)return e.addEventListener?(a.disableWorkerMessageHandler||e.addEventListener("message",(function(n){var t=JSON.parse(n.data),r=t.language,i=t.code,l=t.immediateClose;e.postMessage(a.highlight(i,a.languages[r],r)),l&&e.close()}),!1),a):a;var g=a.util.currentScript();function f(){a.manual||a.highlightAll()}if(g&&(a.filename=g.src,g.hasAttribute("data-manual")&&(a.manual=!0)),!a.manual){var h=document.readyState;"loading"===h||"interactive"===h&&g&&g.defer?document.addEventListener("DOMContentLoaded",f):window.requestAnimationFrame?window.requestAnimationFrame(f):window.setTimeout(f,16)}return a}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism);
-Prism.languages.markup={comment:{pattern://,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern://i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/?[\da-f]{1,8};/i]},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.languages.markup.doctype.inside["internal-subset"].inside=Prism.languages.markup,Prism.hooks.add("wrap",(function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&/,"&"))})),Object.defineProperty(Prism.languages.markup.tag,"addInlined",{value:function(a,e){var s={};s["language-"+e]={pattern:/(^$)/i,lookbehind:!0,inside:Prism.languages[e]},s.cdata=/^$/i;var t={"included-cdata":{pattern://i,inside:s}};t["language-"+e]={pattern:/[\s\S]+/,inside:Prism.languages[e]};var n={};n[a]={pattern:RegExp("(<__[^>]*>)(?:))*\\]\\]>|(?!)".replace(/__/g,(function(){return a})),"i"),lookbehind:!0,greedy:!0,inside:t},Prism.languages.insertBefore("markup","cdata",n)}}),Object.defineProperty(Prism.languages.markup.tag,"addAttribute",{value:function(a,e){Prism.languages.markup.tag.inside["special-attr"].push({pattern:RegExp("(^|[\"'\\s])(?:"+a+")\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))","i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[e,"language-"+e],inside:Prism.languages[e]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup,Prism.languages.xml=Prism.languages.extend("markup",{}),Prism.languages.ssml=Prism.languages.xml,Prism.languages.atom=Prism.languages.xml,Prism.languages.rss=Prism.languages.xml;
-!function(s){var e=/(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/;s.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:RegExp("@[\\w-](?:[^;{\\s\"']|\\s+(?!\\s)|"+e.source+")*?(?:;|(?=\\s*\\{))"),inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+e.source+"|(?:[^\\\\\r\n()\"']|\\\\[^])*)\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+e.source+"$"),alias:"url"}}},selector:{pattern:RegExp("(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|"+e.source+")*(?=\\s*\\{)"),lookbehind:!0},string:{pattern:e,greedy:!0},property:{pattern:/(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,lookbehind:!0},important:/!important\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i,lookbehind:!0},punctuation:/[(){};:,]/},s.languages.css.atrule.inside.rest=s.languages.css;var t=s.languages.markup;t&&(t.tag.addInlined("style","css"),t.tag.addAttribute("style","css"))}(Prism);
-Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/};
-Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp("(^|[^\\w$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w$])"),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:RegExp("((?:^|[^$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)/(?:(?:\\[(?:[^\\]\\\\\r\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:$|[\r\n,.;:})\\]]|//))"),lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:Prism.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),Prism.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),Prism.languages.markup&&(Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.markup.tag.addAttribute("on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)","javascript")),Prism.languages.js=Prism.languages.javascript;
-!function(e){var t="\\b(?:BASH|BASHOPTS|BASH_ALIASES|BASH_ARGC|BASH_ARGV|BASH_CMDS|BASH_COMPLETION_COMPAT_DIR|BASH_LINENO|BASH_REMATCH|BASH_SOURCE|BASH_VERSINFO|BASH_VERSION|COLORTERM|COLUMNS|COMP_WORDBREAKS|DBUS_SESSION_BUS_ADDRESS|DEFAULTS_PATH|DESKTOP_SESSION|DIRSTACK|DISPLAY|EUID|GDMSESSION|GDM_LANG|GNOME_KEYRING_CONTROL|GNOME_KEYRING_PID|GPG_AGENT_INFO|GROUPS|HISTCONTROL|HISTFILE|HISTFILESIZE|HISTSIZE|HOME|HOSTNAME|HOSTTYPE|IFS|INSTANCE|JOB|LANG|LANGUAGE|LC_ADDRESS|LC_ALL|LC_IDENTIFICATION|LC_MEASUREMENT|LC_MONETARY|LC_NAME|LC_NUMERIC|LC_PAPER|LC_TELEPHONE|LC_TIME|LESSCLOSE|LESSOPEN|LINES|LOGNAME|LS_COLORS|MACHTYPE|MAILCHECK|MANDATORY_PATH|NO_AT_BRIDGE|OLDPWD|OPTERR|OPTIND|ORBIT_SOCKETDIR|OSTYPE|PAPERSIZE|PATH|PIPESTATUS|PPID|PS1|PS2|PS3|PS4|PWD|RANDOM|REPLY|SECONDS|SELINUX_INIT|SESSION|SESSIONTYPE|SESSION_MANAGER|SHELL|SHELLOPTS|SHLVL|SSH_AUTH_SOCK|TERM|UID|UPSTART_EVENTS|UPSTART_INSTANCE|UPSTART_JOB|UPSTART_SESSION|USER|WINDOWID|XAUTHORITY|XDG_CONFIG_DIRS|XDG_CURRENT_DESKTOP|XDG_DATA_DIRS|XDG_GREETER_DATA_DIR|XDG_MENU_PREFIX|XDG_RUNTIME_DIR|XDG_SEAT|XDG_SEAT_PATH|XDG_SESSION_DESKTOP|XDG_SESSION_ID|XDG_SESSION_PATH|XDG_SESSION_TYPE|XDG_VTNR|XMODIFIERS)\\b",a={pattern:/(^(["']?)\w+\2)[ \t]+\S.*/,lookbehind:!0,alias:"punctuation",inside:null},n={bash:a,environment:{pattern:RegExp("\\$"+t),alias:"constant"},variable:[{pattern:/\$?\(\([\s\S]+?\)\)/,greedy:!0,inside:{variable:[{pattern:/(^\$\(\([\s\S]+)\)\)/,lookbehind:!0},/^\$\(\(/],number:/\b0x[\dA-Fa-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:[Ee]-?\d+)?/,operator:/--|\+\+|\*\*=?|<<=?|>>=?|&&|\|\||[=!+\-*/%<>^&|]=?|[?~:]/,punctuation:/\(\(?|\)\)?|,|;/}},{pattern:/\$\((?:\([^)]+\)|[^()])+\)|`[^`]+`/,greedy:!0,inside:{variable:/^\$\(|^`|\)$|`$/}},{pattern:/\$\{[^}]+\}/,greedy:!0,inside:{operator:/:[-=?+]?|[!\/]|##?|%%?|\^\^?|,,?/,punctuation:/[\[\]]/,environment:{pattern:RegExp("(\\{)"+t),lookbehind:!0,alias:"constant"}}},/\$(?:\w+|[#?*!@$])/],entity:/\\(?:[abceEfnrtv\\"]|O?[0-7]{1,3}|U[0-9a-fA-F]{8}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{1,2})/};e.languages.bash={shebang:{pattern:/^#!\s*\/.*/,alias:"important"},comment:{pattern:/(^|[^"{\\$])#.*/,lookbehind:!0},"function-name":[{pattern:/(\bfunction\s+)[\w-]+(?=(?:\s*\(?:\s*\))?\s*\{)/,lookbehind:!0,alias:"function"},{pattern:/\b[\w-]+(?=\s*\(\s*\)\s*\{)/,alias:"function"}],"for-or-select":{pattern:/(\b(?:for|select)\s+)\w+(?=\s+in\s)/,alias:"variable",lookbehind:!0},"assign-left":{pattern:/(^|[\s;|&]|[<>]\()\w+(?:\.\w+)*(?=\+?=)/,inside:{environment:{pattern:RegExp("(^|[\\s;|&]|[<>]\\()"+t),lookbehind:!0,alias:"constant"}},alias:"variable",lookbehind:!0},parameter:{pattern:/(^|\s)-{1,2}(?:\w+:[+-]?)?\w+(?:\.\w+)*(?=[=\s]|$)/,alias:"variable",lookbehind:!0},string:[{pattern:/((?:^|[^<])<<-?\s*)(\w+)\s[\s\S]*?(?:\r?\n|\r)\2/,lookbehind:!0,greedy:!0,inside:n},{pattern:/((?:^|[^<])<<-?\s*)(["'])(\w+)\2\s[\s\S]*?(?:\r?\n|\r)\3/,lookbehind:!0,greedy:!0,inside:{bash:a}},{pattern:/(^|[^\\](?:\\\\)*)"(?:\\[\s\S]|\$\([^)]+\)|\$(?!\()|`[^`]+`|[^"\\`$])*"/,lookbehind:!0,greedy:!0,inside:n},{pattern:/(^|[^$\\])'[^']*'/,lookbehind:!0,greedy:!0},{pattern:/\$'(?:[^'\\]|\\[\s\S])*'/,greedy:!0,inside:{entity:n.entity}}],environment:{pattern:RegExp("\\$?"+t),alias:"constant"},variable:n.variable,function:{pattern:/(^|[\s;|&]|[<>]\()(?:add|apropos|apt|apt-cache|apt-get|aptitude|aspell|automysqlbackup|awk|basename|bash|bc|bconsole|bg|bzip2|cal|cargo|cat|cfdisk|chgrp|chkconfig|chmod|chown|chroot|cksum|clear|cmp|column|comm|composer|cp|cron|crontab|csplit|curl|cut|date|dc|dd|ddrescue|debootstrap|df|diff|diff3|dig|dir|dircolors|dirname|dirs|dmesg|docker|docker-compose|du|egrep|eject|env|ethtool|expand|expect|expr|fdformat|fdisk|fg|fgrep|file|find|fmt|fold|format|free|fsck|ftp|fuser|gawk|git|gparted|grep|groupadd|groupdel|groupmod|groups|grub-mkconfig|gzip|halt|head|hg|history|host|hostname|htop|iconv|id|ifconfig|ifdown|ifup|import|install|ip|java|jobs|join|kill|killall|less|link|ln|locate|logname|logrotate|look|lpc|lpr|lprint|lprintd|lprintq|lprm|ls|lsof|lynx|make|man|mc|mdadm|mkconfig|mkdir|mke2fs|mkfifo|mkfs|mkisofs|mknod|mkswap|mmv|more|most|mount|mtools|mtr|mutt|mv|nano|nc|netstat|nice|nl|node|nohup|notify-send|npm|nslookup|op|open|parted|passwd|paste|pathchk|ping|pkill|pnpm|podman|podman-compose|popd|pr|printcap|printenv|ps|pushd|pv|quota|quotacheck|quotactl|ram|rar|rcp|reboot|remsync|rename|renice|rev|rm|rmdir|rpm|rsync|scp|screen|sdiff|sed|sendmail|seq|service|sftp|sh|shellcheck|shuf|shutdown|sleep|slocate|sort|split|ssh|stat|strace|su|sudo|sum|suspend|swapon|sync|sysctl|tac|tail|tar|tee|time|timeout|top|touch|tr|traceroute|tsort|tty|umount|uname|unexpand|uniq|units|unrar|unshar|unzip|update-grub|uptime|useradd|userdel|usermod|users|uudecode|uuencode|v|vcpkg|vdir|vi|vim|virsh|vmstat|wait|watch|wc|wget|whereis|which|who|whoami|write|xargs|xdg-open|yarn|yes|zenity|zip|zsh|zypper)(?=$|[)\s;|&])/,lookbehind:!0},keyword:{pattern:/(^|[\s;|&]|[<>]\()(?:case|do|done|elif|else|esac|fi|for|function|if|in|select|then|until|while)(?=$|[)\s;|&])/,lookbehind:!0},builtin:{pattern:/(^|[\s;|&]|[<>]\()(?:\.|:|alias|bind|break|builtin|caller|cd|command|continue|declare|echo|enable|eval|exec|exit|export|getopts|hash|help|let|local|logout|mapfile|printf|pwd|read|readarray|readonly|return|set|shift|shopt|source|test|times|trap|type|typeset|ulimit|umask|unalias|unset)(?=$|[)\s;|&])/,lookbehind:!0,alias:"class-name"},boolean:{pattern:/(^|[\s;|&]|[<>]\()(?:false|true)(?=$|[)\s;|&])/,lookbehind:!0},"file-descriptor":{pattern:/\B&\d\b/,alias:"important"},operator:{pattern:/\d?<>|>\||\+=|=[=~]?|!=?|<<[<-]?|[&\d]?>>|\d[<>]&?|[<>][&=]?|&[>&]?|\|[&|]?/,inside:{"file-descriptor":{pattern:/^\d/,alias:"important"}}},punctuation:/\$?\(\(?|\)\)?|\.\.|[{}[\];\\]/,number:{pattern:/(^|\s)(?:[1-9]\d*|0)(?:[.,]\d+)?\b/,lookbehind:!0}},a.inside=e.languages.bash;for(var s=["comment","function-name","for-or-select","assign-left","parameter","string","environment","function","keyword","builtin","boolean","file-descriptor","operator","punctuation","number"],o=n.variable[1].inside,i=0;i",unchanged:" ",diff:"!"};Object.keys(n).forEach((function(a){var i=n[a],r=[];/^\w+$/.test(a)||r.push(/\w+/.exec(a)[0]),"diff"===a&&r.push("bold"),e.languages.diff[a]={pattern:RegExp("^(?:["+i+"].*(?:\r\n?|\n|(?![\\s\\S])))+","m"),alias:r,inside:{line:{pattern:/(.)(?=[\s\S]).*(?:\r\n?|\n)?/,lookbehind:!0},prefix:{pattern:/[\s\S]/,alias:/\w+/.exec(a)[0]}}}})),Object.defineProperty(e.languages.diff,"PREFIXES",{value:n})}(Prism);
-!function(e){function n(e,n){return"___"+e.toUpperCase()+n+"___"}Object.defineProperties(e.languages["markup-templating"]={},{buildPlaceholders:{value:function(t,a,r,o){if(t.language===a){var c=t.tokenStack=[];t.code=t.code.replace(r,(function(e){if("function"==typeof o&&!o(e))return e;for(var r,i=c.length;-1!==t.code.indexOf(r=n(a,i));)++i;return c[i]=e,r})),t.grammar=e.languages.markup}}},tokenizePlaceholders:{value:function(t,a){if(t.language===a&&t.tokenStack){t.grammar=e.languages[a];var r=0,o=Object.keys(t.tokenStack);!function c(i){for(var u=0;u=o.length);u++){var g=i[u];if("string"==typeof g||g.content&&"string"==typeof g.content){var l=o[r],s=t.tokenStack[l],f="string"==typeof g?g:g.content,p=n(a,l),k=f.indexOf(p);if(k>-1){++r;var m=f.substring(0,k),d=new e.Token(a,e.tokenize(s,t.grammar),"language-"+a,s),h=f.substring(k+p.length),v=[];m&&v.push.apply(v,c([m])),v.push(d),h&&v.push.apply(v,c([h])),"string"==typeof g?i.splice.apply(i,[u,1].concat(v)):g.content=v}}else g.content&&c(g.content)}return i}(t.tokens)}}}})}(Prism);
-!function(e){e.languages.django={comment:/^\{#[\s\S]*?#\}$/,tag:{pattern:/(^\{%[+-]?\s*)\w+/,lookbehind:!0,alias:"keyword"},delimiter:{pattern:/^\{[{%][+-]?|[+-]?[}%]\}$/,alias:"punctuation"},string:{pattern:/("|')(?:\\.|(?!\1)[^\\\r\n])*\1/,greedy:!0},filter:{pattern:/(\|)\w+/,lookbehind:!0,alias:"function"},test:{pattern:/(\bis\s+(?:not\s+)?)(?!not\b)\w+/,lookbehind:!0,alias:"function"},function:/\b[a-z_]\w+(?=\s*\()/i,keyword:/\b(?:and|as|by|else|for|if|import|in|is|loop|not|or|recursive|with|without)\b/,operator:/[-+%=]=?|!=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]/,number:/\b\d+(?:\.\d+)?\b/,boolean:/[Ff]alse|[Nn]one|[Tt]rue/,variable:/\b\w+\b/,punctuation:/[{}[\](),.:;]/};var n=/\{\{[\s\S]*?\}\}|\{%[\s\S]*?%\}|\{#[\s\S]*?#\}/g,o=e.languages["markup-templating"];e.hooks.add("before-tokenize",(function(e){o.buildPlaceholders(e,"django",n)})),e.hooks.add("after-tokenize",(function(e){o.tokenizePlaceholders(e,"django")})),e.languages.jinja2=e.languages.django,e.hooks.add("before-tokenize",(function(e){o.buildPlaceholders(e,"jinja2",n)})),e.hooks.add("after-tokenize",(function(e){o.tokenizePlaceholders(e,"jinja2")}))}(Prism);
-Prism.languages["dns-zone-file"]={comment:/;.*/,string:{pattern:/"(?:\\.|[^"\\\r\n])*"/,greedy:!0},variable:[{pattern:/(^\$ORIGIN[ \t]+)\S+/m,lookbehind:!0},{pattern:/(^|\s)@(?=\s|$)/,lookbehind:!0}],keyword:/^\$(?:INCLUDE|ORIGIN|TTL)(?=\s|$)/m,class:{pattern:/(^|\s)(?:CH|CS|HS|IN)(?=\s|$)/,lookbehind:!0,alias:"keyword"},type:{pattern:/(^|\s)(?:A|A6|AAAA|AFSDB|APL|ATMA|CAA|CDNSKEY|CDS|CERT|CNAME|DHCID|DLV|DNAME|DNSKEY|DS|EID|GID|GPOS|HINFO|HIP|IPSECKEY|ISDN|KEY|KX|LOC|MAILA|MAILB|MB|MD|MF|MG|MINFO|MR|MX|NAPTR|NB|NBSTAT|NIMLOC|NINFO|NS|NSAP|NSAP-PTR|NSEC|NSEC3|NSEC3PARAM|NULL|NXT|OPENPGPKEY|PTR|PX|RKEY|RP|RRSIG|RT|SIG|SINK|SMIMEA|SOA|SPF|SRV|SSHFP|TA|TKEY|TLSA|TSIG|TXT|UID|UINFO|UNSPEC|URI|WKS|X25)(?=\s|$)/,lookbehind:!0,alias:"keyword"},punctuation:/[()]/},Prism.languages["dns-zone"]=Prism.languages["dns-zone-file"];
-!function(e){var n="(?:[ \t]+(?![ \t])(?:)?|)".replace(//g,(function(){return"\\\\[\r\n](?:\\s|\\\\[\r\n]|#.*(?!.))*(?![\\s#]|\\\\[\r\n])"})),r="\"(?:[^\"\\\\\r\n]|\\\\(?:\r\n|[^]))*\"|'(?:[^'\\\\\r\n]|\\\\(?:\r\n|[^]))*'",t="--[\\w-]+=(?:|(?![\"'])(?:[^\\s\\\\]|\\\\.)+)".replace(//g,(function(){return r})),o={pattern:RegExp(r),greedy:!0},i={pattern:/(^[ \t]*)#.*/m,lookbehind:!0,greedy:!0};function a(e,r){return e=e.replace(//g,(function(){return t})).replace(//g,(function(){return n})),RegExp(e,r)}e.languages.docker={instruction:{pattern:/(^[ \t]*)(?:ADD|ARG|CMD|COPY|ENTRYPOINT|ENV|EXPOSE|FROM|HEALTHCHECK|LABEL|MAINTAINER|ONBUILD|RUN|SHELL|STOPSIGNAL|USER|VOLUME|WORKDIR)(?=\s)(?:\\.|[^\r\n\\])*(?:\\$(?:\s|#.*$)*(?![\s#])(?:\\.|[^\r\n\\])*)*/im,lookbehind:!0,greedy:!0,inside:{options:{pattern:a("(^(?:ONBUILD)?\\w+)(?:)*","i"),lookbehind:!0,greedy:!0,inside:{property:{pattern:/(^|\s)--[\w-]+/,lookbehind:!0},string:[o,{pattern:/(=)(?!["'])(?:[^\s\\]|\\.)+/,lookbehind:!0}],operator:/\\$/m,punctuation:/=/}},keyword:[{pattern:a("(^(?:ONBUILD)?HEALTHCHECK(?:)*)(?:CMD|NONE)\\b","i"),lookbehind:!0,greedy:!0},{pattern:a("(^(?:ONBUILD)?FROM(?:)*(?!--)[^ \t\\\\]+)AS","i"),lookbehind:!0,greedy:!0},{pattern:a("(^ONBUILD)\\w+","i"),lookbehind:!0,greedy:!0},{pattern:/^\w+/,greedy:!0}],comment:i,string:o,variable:/\$(?:\w+|\{[^{}"'\\]*\})/,operator:/\\$/m}},comment:i},e.languages.dockerfile=e.languages.docker}(Prism);
-!function(e){var a="(?:"+["[a-zA-Z_\\x80-\\uFFFF][\\w\\x80-\\uFFFF]*","-?(?:\\.\\d+|\\d+(?:\\.\\d*)?)",'"[^"\\\\]*(?:\\\\[^][^"\\\\]*)*"',"<(?:[^<>]|(?!\x3c!--)<(?:[^<>\"']|\"[^\"]*\"|'[^']*')+>|\x3c!--(?:[^-]|-(?!->))*--\x3e)*>"].join("|")+")",n={markup:{pattern:/(^<)[\s\S]+(?=>$)/,lookbehind:!0,alias:["language-markup","language-html","language-xml"],inside:e.languages.markup}};function r(e,n){return RegExp(e.replace(//g,(function(){return a})),n)}e.languages.dot={comment:{pattern:/\/\/.*|\/\*[\s\S]*?\*\/|^#.*/m,greedy:!0},"graph-name":{pattern:r("(\\b(?:digraph|graph|subgraph)[ \t\r\n]+)","i"),lookbehind:!0,greedy:!0,alias:"class-name",inside:n},"attr-value":{pattern:r("(=[ \t\r\n]*)"),lookbehind:!0,greedy:!0,inside:n},"attr-name":{pattern:r("([\\[;, \t\r\n])(?=[ \t\r\n]*=)"),lookbehind:!0,greedy:!0,inside:n},keyword:/\b(?:digraph|edge|graph|node|strict|subgraph)\b/i,"compass-point":{pattern:/(:[ \t\r\n]*)(?:[ewc_]|[ns][ew]?)(?![\w\x80-\uFFFF])/,lookbehind:!0,alias:"builtin"},node:{pattern:r("(^|[^-.\\w\\x80-\\uFFFF\\\\])"),lookbehind:!0,greedy:!0,inside:n},operator:/[=:]|-[->]/,punctuation:/[\[\]{};,]/},e.languages.gv=e.languages.dot}(Prism);
-Prism.languages.elixir={doc:{pattern:/@(?:doc|moduledoc)\s+(?:("""|''')[\s\S]*?\1|("|')(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2)/,inside:{attribute:/^@\w+/,string:/['"][\s\S]+/}},comment:{pattern:/#.*/,greedy:!0},regex:{pattern:/~[rR](?:("""|''')(?:\\[\s\S]|(?!\1)[^\\])+\1|([\/|"'])(?:\\.|(?!\2)[^\\\r\n])+\2|\((?:\\.|[^\\)\r\n])+\)|\[(?:\\.|[^\\\]\r\n])+\]|\{(?:\\.|[^\\}\r\n])+\}|<(?:\\.|[^\\>\r\n])+>)[uismxfr]*/,greedy:!0},string:[{pattern:/~[cCsSwW](?:("""|''')(?:\\[\s\S]|(?!\1)[^\\])+\1|([\/|"'])(?:\\.|(?!\2)[^\\\r\n])+\2|\((?:\\.|[^\\)\r\n])+\)|\[(?:\\.|[^\\\]\r\n])+\]|\{(?:\\.|#\{[^}]+\}|#(?!\{)|[^#\\}\r\n])+\}|<(?:\\.|[^\\>\r\n])+>)[csa]?/,greedy:!0,inside:{}},{pattern:/("""|''')[\s\S]*?\1/,greedy:!0,inside:{}},{pattern:/("|')(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0,inside:{}}],atom:{pattern:/(^|[^:]):\w+/,lookbehind:!0,alias:"symbol"},module:{pattern:/\b[A-Z]\w*\b/,alias:"class-name"},"attr-name":/\b\w+\??:(?!:)/,argument:{pattern:/(^|[^&])&\d+/,lookbehind:!0,alias:"variable"},attribute:{pattern:/@\w+/,alias:"variable"},function:/\b[_a-zA-Z]\w*[?!]?(?:(?=\s*(?:\.\s*)?\()|(?=\/\d))/,number:/\b(?:0[box][a-f\d_]+|\d[\d_]*)(?:\.[\d_]+)?(?:e[+-]?[\d_]+)?\b/i,keyword:/\b(?:after|alias|and|case|catch|cond|def(?:callback|delegate|exception|impl|macro|module|n|np|p|protocol|struct)?|do|else|end|fn|for|if|import|not|or|quote|raise|require|rescue|try|unless|unquote|use|when)\b/,boolean:/\b(?:false|nil|true)\b/,operator:[/\bin\b|&&?|\|[|>]?|\\\\|::|\.\.\.?|\+\+?|-[->]?|<[-=>]|>=|!==?|\B!|=(?:==?|[>~])?|[*\/^]/,{pattern:/([^<])<(?!<)/,lookbehind:!0},{pattern:/([^>])>(?!>)/,lookbehind:!0}],punctuation:/<<|>>|[.,%\[\]{}()]/},Prism.languages.elixir.string.forEach((function(e){e.inside={interpolation:{pattern:/#\{[^}]+\}/,inside:{delimiter:{pattern:/^#\{|\}$/,alias:"punctuation"},rest:Prism.languages.elixir}}}}));
-Prism.languages.erlang={comment:/%.+/,string:{pattern:/"(?:\\.|[^\\"\r\n])*"/,greedy:!0},"quoted-function":{pattern:/'(?:\\.|[^\\'\r\n])+'(?=\()/,alias:"function"},"quoted-atom":{pattern:/'(?:\\.|[^\\'\r\n])+'/,alias:"atom"},boolean:/\b(?:false|true)\b/,keyword:/\b(?:after|begin|case|catch|end|fun|if|of|receive|try|when)\b/,number:[/\$\\?./,/\b\d+#[a-z0-9]+/i,/(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i],function:/\b[a-z][\w@]*(?=\()/,variable:{pattern:/(^|[^@])(?:\b|\?)[A-Z_][\w@]*/,lookbehind:!0},operator:[/[=\/<>:]=|=[:\/]=|\+\+?|--?|[=*\/!]|\b(?:and|andalso|band|bnot|bor|bsl|bsr|bxor|div|not|or|orelse|rem|xor)\b/,{pattern:/(^|[^<])<(?!<)/,lookbehind:!0},{pattern:/(^|[^>])>(?!>)/,lookbehind:!0}],atom:/\b[a-z][\w@]*/,punctuation:/[()[\]{}:;,.#|]|<<|>>/};
-Prism.languages.git={comment:/^#.*/m,deleted:/^[-–].*/m,inserted:/^\+.*/m,string:/("|')(?:\\.|(?!\1)[^\\\r\n])*\1/,command:{pattern:/^.*\$ git .*$/m,inside:{parameter:/\s--?\w+/}},coord:/^@@.*@@$/m,"commit-sha1":/^commit \w{40}$/m};
-Prism.languages.go=Prism.languages.extend("clike",{string:{pattern:/(^|[^\\])"(?:\\.|[^"\\\r\n])*"|`[^`]*`/,lookbehind:!0,greedy:!0},keyword:/\b(?:break|case|chan|const|continue|default|defer|else|fallthrough|for|func|go(?:to)?|if|import|interface|map|package|range|return|select|struct|switch|type|var)\b/,boolean:/\b(?:_|false|iota|nil|true)\b/,number:[/\b0(?:b[01_]+|o[0-7_]+)i?\b/i,/\b0x(?:[a-f\d_]+(?:\.[a-f\d_]*)?|\.[a-f\d_]+)(?:p[+-]?\d+(?:_\d+)*)?i?(?!\w)/i,/(?:\b\d[\d_]*(?:\.[\d_]*)?|\B\.\d[\d_]*)(?:e[+-]?[\d_]+)?i?(?!\w)/i],operator:/[*\/%^!=]=?|\+[=+]?|-[=-]?|\|[=|]?|&(?:=|&|\^=?)?|>(?:>=?|=)?|<(?:<=?|=|-)?|:=|\.\.\./,builtin:/\b(?:append|bool|byte|cap|close|complex|complex(?:64|128)|copy|delete|error|float(?:32|64)|u?int(?:8|16|32|64)?|imag|len|make|new|panic|print(?:ln)?|real|recover|rune|string|uintptr)\b/}),Prism.languages.insertBefore("go","string",{char:{pattern:/'(?:\\.|[^'\\\r\n]){0,10}'/,greedy:!0}}),delete Prism.languages.go["class-name"];
-Prism.languages["go-mod"]=Prism.languages["go-module"]={comment:{pattern:/\/\/.*/,greedy:!0},version:{pattern:/(^|[\s()[\],])v\d+\.\d+\.\d+(?:[+-][-+.\w]*)?(?![^\s()[\],])/,lookbehind:!0,alias:"number"},"go-version":{pattern:/((?:^|\s)go\s+)\d+(?:\.\d+){1,2}/,lookbehind:!0,alias:"number"},keyword:{pattern:/^([ \t]*)(?:exclude|go|module|replace|require|retract)\b/m,lookbehind:!0},operator:/=>/,punctuation:/[()[\],]/};
-Prism.languages.hcl={comment:/(?:\/\/|#).*|\/\*[\s\S]*?(?:\*\/|$)/,heredoc:{pattern:/<<-?(\w+\b)[\s\S]*?^[ \t]*\1/m,greedy:!0,alias:"string"},keyword:[{pattern:/(?:data|resource)\s+(?:"(?:\\[\s\S]|[^\\"])*")(?=\s+"[\w-]+"\s+\{)/i,inside:{type:{pattern:/(resource|data|\s+)(?:"(?:\\[\s\S]|[^\\"])*")/i,lookbehind:!0,alias:"variable"}}},{pattern:/(?:backend|module|output|provider|provisioner|variable)\s+(?:[\w-]+|"(?:\\[\s\S]|[^\\"])*")\s+(?=\{)/i,inside:{type:{pattern:/(backend|module|output|provider|provisioner|variable)\s+(?:[\w-]+|"(?:\\[\s\S]|[^\\"])*")\s+/i,lookbehind:!0,alias:"variable"}}},/[\w-]+(?=\s+\{)/],property:[/[-\w\.]+(?=\s*=(?!=))/,/"(?:\\[\s\S]|[^\\"])+"(?=\s*[:=])/],string:{pattern:/"(?:[^\\$"]|\\[\s\S]|\$(?:(?=")|\$+(?!\$)|[^"${])|\$\{(?:[^{}"]|"(?:[^\\"]|\\[\s\S])*")*\})*"/,greedy:!0,inside:{interpolation:{pattern:/(^|[^$])\$\{(?:[^{}"]|"(?:[^\\"]|\\[\s\S])*")*\}/,lookbehind:!0,inside:{type:{pattern:/(\b(?:count|data|local|module|path|self|terraform|var)\b\.)[\w\*]+/i,lookbehind:!0,alias:"variable"},keyword:/\b(?:count|data|local|module|path|self|terraform|var)\b/i,function:/\w+(?=\()/,string:{pattern:/"(?:\\[\s\S]|[^\\"])*"/,greedy:!0},number:/\b0x[\da-f]+\b|\b\d+(?:\.\d*)?(?:e[+-]?\d+)?/i,punctuation:/[!\$#%&'()*+,.\/;<=>@\[\\\]^`{|}~?:]/}}}},number:/\b0x[\da-f]+\b|\b\d+(?:\.\d*)?(?:e[+-]?\d+)?/i,boolean:/\b(?:false|true)\b/i,punctuation:/[=\[\]{}]/};
-Prism.languages.ini={comment:{pattern:/(^[ \f\t\v]*)[#;][^\n\r]*/m,lookbehind:!0},section:{pattern:/(^[ \f\t\v]*)\[[^\n\r\]]*\]?/m,lookbehind:!0,inside:{"section-name":{pattern:/(^\[[ \f\t\v]*)[^ \f\t\v\]]+(?:[ \f\t\v]+[^ \f\t\v\]]+)*/,lookbehind:!0,alias:"selector"},punctuation:/\[|\]/}},key:{pattern:/(^[ \f\t\v]*)[^ \f\n\r\t\v=]+(?:[ \f\t\v]+[^ \f\n\r\t\v=]+)*(?=[ \f\t\v]*=)/m,lookbehind:!0,alias:"attr-name"},value:{pattern:/(=[ \f\t\v]*)[^ \f\n\r\t\v]+(?:[ \f\t\v]+[^ \f\n\r\t\v]+)*/,lookbehind:!0,alias:"attr-value",inside:{"inner-value":{pattern:/^("|').+(?=\1$)/,lookbehind:!0}}},punctuation:/=/};
-!function(e){var n="\\\\\\((?:[^()]|\\([^()]*\\))*\\)",t=RegExp('(^|[^\\\\])"(?:[^"\r\n\\\\]|\\\\[^\r\n(]|__)*"'.replace(/__/g,(function(){return n}))),i={interpolation:{pattern:RegExp("((?:^|[^\\\\])(?:\\\\{2})*)"+n),lookbehind:!0,inside:{content:{pattern:/^(\\\()[\s\S]+(?=\)$)/,lookbehind:!0,inside:null},punctuation:/^\\\(|\)$/}}},a=e.languages.jq={comment:/#.*/,property:{pattern:RegExp(t.source+"(?=\\s*:(?!:))"),lookbehind:!0,greedy:!0,inside:i},string:{pattern:t,lookbehind:!0,greedy:!0,inside:i},function:{pattern:/(\bdef\s+)[a-z_]\w+/i,lookbehind:!0},variable:/\B\$\w+/,"property-literal":{pattern:/\b[a-z_]\w*(?=\s*:(?!:))/i,alias:"property"},keyword:/\b(?:as|break|catch|def|elif|else|end|foreach|if|import|include|label|module|modulemeta|null|reduce|then|try|while)\b/,boolean:/\b(?:false|true)\b/,number:/(?:\b\d+\.|\B\.)?\b\d+(?:[eE][+-]?\d+)?\b/,operator:[{pattern:/\|=?/,alias:"pipe"},/\.\.|[!=<>]?=|\?\/\/|\/\/=?|[-+*/%]=?|[<>?]|\b(?:and|not|or)\b/],"c-style-function":{pattern:/\b[a-z_]\w*(?=\s*\()/i,alias:"function"},punctuation:/::|[()\[\]{},:;]|\.(?=\s*[\[\w$])/,dot:{pattern:/\./,alias:"important"}};i.interpolation.inside.content.inside=a}(Prism);
-Prism.languages.lua={comment:/^#!.+|--(?:\[(=*)\[[\s\S]*?\]\1\]|.*)/m,string:{pattern:/(["'])(?:(?!\1)[^\\\r\n]|\\z(?:\r\n|\s)|\\(?:\r\n|[^z]))*\1|\[(=*)\[[\s\S]*?\]\2\]/,greedy:!0},number:/\b0x[a-f\d]+(?:\.[a-f\d]*)?(?:p[+-]?\d+)?\b|\b\d+(?:\.\B|(?:\.\d*)?(?:e[+-]?\d+)?\b)|\B\.\d+(?:e[+-]?\d+)?\b/i,keyword:/\b(?:and|break|do|else|elseif|end|false|for|function|goto|if|in|local|nil|not|or|repeat|return|then|true|until|while)\b/,function:/(?!\d)\w+(?=\s*(?:[({]))/,operator:[/[-+*%^&|#]|\/\/?|<[<=]?|>[>=]?|[=~]=?/,{pattern:/(^|[^.])\.\.(?!\.)/,lookbehind:!0}],punctuation:/[\[\](){},;]|\.+|:+/};
-Prism.languages.makefile={comment:{pattern:/(^|[^\\])#(?:\\(?:\r\n|[\s\S])|[^\\\r\n])*/,lookbehind:!0},string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"builtin-target":{pattern:/\.[A-Z][^:#=\s]+(?=\s*:(?!=))/,alias:"builtin"},target:{pattern:/^(?:[^:=\s]|[ \t]+(?![\s:]))+(?=\s*:(?!=))/m,alias:"symbol",inside:{variable:/\$+(?:(?!\$)[^(){}:#=\s]+|(?=[({]))/}},variable:/\$+(?:(?!\$)[^(){}:#=\s]+|\([@*%<^+?][DF]\)|(?=[({]))/,keyword:/-include\b|\b(?:define|else|endef|endif|export|ifn?def|ifn?eq|include|override|private|sinclude|undefine|unexport|vpath)\b/,function:{pattern:/(\()(?:abspath|addsuffix|and|basename|call|dir|error|eval|file|filter(?:-out)?|findstring|firstword|flavor|foreach|guile|if|info|join|lastword|load|notdir|or|origin|patsubst|realpath|shell|sort|strip|subst|suffix|value|warning|wildcard|word(?:list|s)?)(?=[ \t])/,lookbehind:!0},operator:/(?:::|[?:+!])?=|[|@]/,punctuation:/[:;(){}]/};
-!function(n){function e(n){return n=n.replace(//g,(function(){return"(?:\\\\.|[^\\\\\n\r]|(?:\n|\r\n?)(?![\r\n]))"})),RegExp("((?:^|[^\\\\])(?:\\\\{2})*)(?:"+n+")")}var t="(?:\\\\.|``(?:[^`\r\n]|`(?!`))+``|`[^`\r\n]+`|[^\\\\|\r\n`])+",a="\\|?__(?:\\|__)+\\|?(?:(?:\n|\r\n?)|(?![^]))".replace(/__/g,(function(){return t})),i="\\|?[ \t]*:?-{3,}:?[ \t]*(?:\\|[ \t]*:?-{3,}:?[ \t]*)+\\|?(?:\n|\r\n?)";n.languages.markdown=n.languages.extend("markup",{}),n.languages.insertBefore("markdown","prolog",{"front-matter-block":{pattern:/(^(?:\s*[\r\n])?)---(?!.)[\s\S]*?[\r\n]---(?!.)/,lookbehind:!0,greedy:!0,inside:{punctuation:/^---|---$/,"front-matter":{pattern:/\S+(?:\s+\S+)*/,alias:["yaml","language-yaml"],inside:n.languages.yaml}}},blockquote:{pattern:/^>(?:[\t ]*>)*/m,alias:"punctuation"},table:{pattern:RegExp("^"+a+i+"(?:"+a+")*","m"),inside:{"table-data-rows":{pattern:RegExp("^("+a+i+")(?:"+a+")*$"),lookbehind:!0,inside:{"table-data":{pattern:RegExp(t),inside:n.languages.markdown},punctuation:/\|/}},"table-line":{pattern:RegExp("^("+a+")"+i+"$"),lookbehind:!0,inside:{punctuation:/\||:?-{3,}:?/}},"table-header-row":{pattern:RegExp("^"+a+"$"),inside:{"table-header":{pattern:RegExp(t),alias:"important",inside:n.languages.markdown},punctuation:/\|/}}}},code:[{pattern:/((?:^|\n)[ \t]*\n|(?:^|\r\n?)[ \t]*\r\n?)(?: {4}|\t).+(?:(?:\n|\r\n?)(?: {4}|\t).+)*/,lookbehind:!0,alias:"keyword"},{pattern:/^```[\s\S]*?^```$/m,greedy:!0,inside:{"code-block":{pattern:/^(```.*(?:\n|\r\n?))[\s\S]+?(?=(?:\n|\r\n?)^```$)/m,lookbehind:!0},"code-language":{pattern:/^(```).+/,lookbehind:!0},punctuation:/```/}}],title:[{pattern:/\S.*(?:\n|\r\n?)(?:==+|--+)(?=[ \t]*$)/m,alias:"important",inside:{punctuation:/==+$|--+$/}},{pattern:/(^\s*)#.+/m,lookbehind:!0,alias:"important",inside:{punctuation:/^#+|#+$/}}],hr:{pattern:/(^\s*)([*-])(?:[\t ]*\2){2,}(?=\s*$)/m,lookbehind:!0,alias:"punctuation"},list:{pattern:/(^\s*)(?:[*+-]|\d+\.)(?=[\t ].)/m,lookbehind:!0,alias:"punctuation"},"url-reference":{pattern:/!?\[[^\]]+\]:[\t ]+(?:\S+|<(?:\\.|[^>\\])+>)(?:[\t ]+(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\)))?/,inside:{variable:{pattern:/^(!?\[)[^\]]+/,lookbehind:!0},string:/(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\))$/,punctuation:/^[\[\]!:]|[<>]/},alias:"url"},bold:{pattern:e("\\b__(?:(?!_)|_(?:(?!_))+_)+__\\b|\\*\\*(?:(?!\\*)|\\*(?:(?!\\*))+\\*)+\\*\\*"),lookbehind:!0,greedy:!0,inside:{content:{pattern:/(^..)[\s\S]+(?=..$)/,lookbehind:!0,inside:{}},punctuation:/\*\*|__/}},italic:{pattern:e("\\b_(?:(?!_)|__(?:(?!_))+__)+_\\b|\\*(?:(?!\\*)|\\*\\*(?:(?!\\*))+\\*\\*)+\\*"),lookbehind:!0,greedy:!0,inside:{content:{pattern:/(^.)[\s\S]+(?=.$)/,lookbehind:!0,inside:{}},punctuation:/[*_]/}},strike:{pattern:e("(~~?)(?:(?!~))+\\2"),lookbehind:!0,greedy:!0,inside:{content:{pattern:/(^~~?)[\s\S]+(?=\1$)/,lookbehind:!0,inside:{}},punctuation:/~~?/}},"code-snippet":{pattern:/(^|[^\\`])(?:``[^`\r\n]+(?:`[^`\r\n]+)*``(?!`)|`[^`\r\n]+`(?!`))/,lookbehind:!0,greedy:!0,alias:["code","keyword"]},url:{pattern:e('!?\\[(?:(?!\\]))+\\](?:\\([^\\s)]+(?:[\t ]+"(?:\\\\.|[^"\\\\])*")?\\)|[ \t]?\\[(?:(?!\\]))+\\])'),lookbehind:!0,greedy:!0,inside:{operator:/^!/,content:{pattern:/(^\[)[^\]]+(?=\])/,lookbehind:!0,inside:{}},variable:{pattern:/(^\][ \t]?\[)[^\]]+(?=\]$)/,lookbehind:!0},url:{pattern:/(^\]\()[^\s)]+/,lookbehind:!0},string:{pattern:/(^[ \t]+)"(?:\\.|[^"\\])*"(?=\)$)/,lookbehind:!0}}}}),["url","bold","italic","strike"].forEach((function(e){["url","bold","italic","strike","code-snippet"].forEach((function(t){e!==t&&(n.languages.markdown[e].inside.content.inside[t]=n.languages.markdown[t])}))})),n.hooks.add("after-tokenize",(function(n){"markdown"!==n.language&&"md"!==n.language||function n(e){if(e&&"string"!=typeof e)for(var t=0,a=e.length;t",quot:'"'},l=String.fromCodePoint||String.fromCharCode;n.languages.md=n.languages.markdown}(Prism);
-!function(e){var n=/\$(?:\w[a-z\d]*(?:_[^\x00-\x1F\s"'\\()$]*)?|\{[^}\s"'\\]+\})/i;e.languages.nginx={comment:{pattern:/(^|[\s{};])#.*/,lookbehind:!0,greedy:!0},directive:{pattern:/(^|\s)\w(?:[^;{}"'\\\s]|\\.|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\s+(?:#.*(?!.)|(?![#\s])))*?(?=\s*[;{])/,lookbehind:!0,greedy:!0,inside:{string:{pattern:/((?:^|[^\\])(?:\\\\)*)(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/,lookbehind:!0,greedy:!0,inside:{escape:{pattern:/\\["'\\nrt]/,alias:"entity"},variable:n}},comment:{pattern:/(\s)#.*/,lookbehind:!0,greedy:!0},keyword:{pattern:/^\S+/,greedy:!0},boolean:{pattern:/(\s)(?:off|on)(?!\S)/,lookbehind:!0},number:{pattern:/(\s)\d+[a-z]*(?!\S)/i,lookbehind:!0},variable:n}},punctuation:/[{};]/}}(Prism);
-Prism.languages.properties={comment:/^[ \t]*[#!].*$/m,value:{pattern:/(^[ \t]*(?:\\(?:\r\n|[\s\S])|[^\\\s:=])+(?: *[=:] *(?! )| ))(?:\\(?:\r\n|[\s\S])|[^\\\r\n])+/m,lookbehind:!0,alias:"attr-value"},key:{pattern:/^[ \t]*(?:\\(?:\r\n|[\s\S])|[^\\\s:=])+(?= *[=:]| )/m,alias:"attr-name"},punctuation:/[=:]/};
-Prism.languages.python={comment:{pattern:/(^|[^\\])#.*/,lookbehind:!0,greedy:!0},"string-interpolation":{pattern:/(?:f|fr|rf)(?:("""|''')[\s\S]*?\1|("|')(?:\\.|(?!\2)[^\\\r\n])*\2)/i,greedy:!0,inside:{interpolation:{pattern:/((?:^|[^{])(?:\{\{)*)\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}])+\})+\})+\}/,lookbehind:!0,inside:{"format-spec":{pattern:/(:)[^:(){}]+(?=\}$)/,lookbehind:!0},"conversion-option":{pattern://,alias:"punctuation"},rest:null}},string:/[\s\S]+/}},"triple-quoted-string":{pattern:/(?:[rub]|br|rb)?("""|''')[\s\S]*?\1/i,greedy:!0,alias:"string"},string:{pattern:/(?:[rub]|br|rb)?("|')(?:\\.|(?!\1)[^\\\r\n])*\1/i,greedy:!0},function:{pattern:/((?:^|\s)def[ \t]+)[a-zA-Z_]\w*(?=\s*\()/g,lookbehind:!0},"class-name":{pattern:/(\bclass\s+)\w+/i,lookbehind:!0},decorator:{pattern:/(^[\t ]*)@\w+(?:\.\w+)*/m,lookbehind:!0,alias:["annotation","punctuation"],inside:{punctuation:/\./}},keyword:/\b(?:_(?=\s*:)|and|as|assert|async|await|break|case|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|match|nonlocal|not|or|pass|print|raise|return|try|while|with|yield)\b/,builtin:/\b(?:__import__|abs|all|any|apply|ascii|basestring|bin|bool|buffer|bytearray|bytes|callable|chr|classmethod|cmp|coerce|compile|complex|delattr|dict|dir|divmod|enumerate|eval|execfile|file|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|intern|isinstance|issubclass|iter|len|list|locals|long|map|max|memoryview|min|next|object|oct|open|ord|pow|property|range|raw_input|reduce|reload|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|unichr|unicode|vars|xrange|zip)\b/,boolean:/\b(?:False|None|True)\b/,number:/\b0(?:b(?:_?[01])+|o(?:_?[0-7])+|x(?:_?[a-f0-9])+)\b|(?:\b\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\B\.\d+(?:_\d+)*)(?:e[+-]?\d+(?:_\d+)*)?j?(?!\w)/i,operator:/[-+%=]=?|!=|:=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]/,punctuation:/[{}[\];(),.:]/},Prism.languages.python["string-interpolation"].inside.interpolation.inside.rest=Prism.languages.python,Prism.languages.py=Prism.languages.python;
-Prism.languages.r={comment:/#.*/,string:{pattern:/(['"])(?:\\.|(?!\1)[^\\\r\n])*\1/,greedy:!0},"percent-operator":{pattern:/%[^%\s]*%/,alias:"operator"},boolean:/\b(?:FALSE|TRUE)\b/,ellipsis:/\.\.(?:\.|\d+)/,number:[/\b(?:Inf|NaN)\b/,/(?:\b0x[\dA-Fa-f]+(?:\.\d*)?|\b\d+(?:\.\d*)?|\B\.\d+)(?:[EePp][+-]?\d+)?[iL]?/],keyword:/\b(?:NA|NA_character_|NA_complex_|NA_integer_|NA_real_|NULL|break|else|for|function|if|in|next|repeat|while)\b/,operator:/->?>?|<(?:=|-)?|[>=!]=?|::?|&&?|\|\|?|[+*\/^$@~]/,punctuation:/[(){}\[\],;]/};
-Prism.languages.rest={table:[{pattern:/(^[\t ]*)(?:\+[=-]+)+\+(?:\r?\n|\r)(?:\1[+|].+[+|](?:\r?\n|\r))+\1(?:\+[=-]+)+\+/m,lookbehind:!0,inside:{punctuation:/\||(?:\+[=-]+)+\+/}},{pattern:/(^[\t ]*)=+ [ =]*=(?:(?:\r?\n|\r)\1.+)+(?:\r?\n|\r)\1=+ [ =]*=(?=(?:\r?\n|\r){2}|\s*$)/m,lookbehind:!0,inside:{punctuation:/[=-]+/}}],"substitution-def":{pattern:/(^[\t ]*\.\. )\|(?:[^|\s](?:[^|]*[^|\s])?)\| [^:]+::/m,lookbehind:!0,inside:{substitution:{pattern:/^\|(?:[^|\s]|[^|\s][^|]*[^|\s])\|/,alias:"attr-value",inside:{punctuation:/^\||\|$/}},directive:{pattern:/( )(?! )[^:]+::/,lookbehind:!0,alias:"function",inside:{punctuation:/::$/}}}},"link-target":[{pattern:/(^[\t ]*\.\. )\[[^\]]+\]/m,lookbehind:!0,alias:"string",inside:{punctuation:/^\[|\]$/}},{pattern:/(^[\t ]*\.\. )_(?:`[^`]+`|(?:[^:\\]|\\.)+):/m,lookbehind:!0,alias:"string",inside:{punctuation:/^_|:$/}}],directive:{pattern:/(^[\t ]*\.\. )[^:]+::/m,lookbehind:!0,alias:"function",inside:{punctuation:/::$/}},comment:{pattern:/(^[\t ]*\.\.)(?:(?: .+)?(?:(?:\r?\n|\r).+)+| .+)(?=(?:\r?\n|\r){2}|$)/m,lookbehind:!0},title:[{pattern:/^(([!"#$%&'()*+,\-.\/:;<=>?@\[\\\]^_`{|}~])\2+)(?:\r?\n|\r).+(?:\r?\n|\r)\1$/m,inside:{punctuation:/^[!"#$%&'()*+,\-.\/:;<=>?@\[\\\]^_`{|}~]+|[!"#$%&'()*+,\-.\/:;<=>?@\[\\\]^_`{|}~]+$/,important:/.+/}},{pattern:/(^|(?:\r?\n|\r){2}).+(?:\r?\n|\r)([!"#$%&'()*+,\-.\/:;<=>?@\[\\\]^_`{|}~])\2+(?=\r?\n|\r|$)/,lookbehind:!0,inside:{punctuation:/[!"#$%&'()*+,\-.\/:;<=>?@\[\\\]^_`{|}~]+$/,important:/.+/}}],hr:{pattern:/((?:\r?\n|\r){2})([!"#$%&'()*+,\-.\/:;<=>?@\[\\\]^_`{|}~])\2{3,}(?=(?:\r?\n|\r){2})/,lookbehind:!0,alias:"punctuation"},field:{pattern:/(^[\t ]*):[^:\r\n]+:(?= )/m,lookbehind:!0,alias:"attr-name"},"command-line-option":{pattern:/(^[\t ]*)(?:[+-][a-z\d]|(?:--|\/)[a-z\d-]+)(?:[ =](?:[a-z][\w-]*|<[^<>]+>))?(?:, (?:[+-][a-z\d]|(?:--|\/)[a-z\d-]+)(?:[ =](?:[a-z][\w-]*|<[^<>]+>))?)*(?=(?:\r?\n|\r)? {2,}\S)/im,lookbehind:!0,alias:"symbol"},"literal-block":{pattern:/::(?:\r?\n|\r){2}([ \t]+)(?![ \t]).+(?:(?:\r?\n|\r)\1.+)*/,inside:{"literal-block-punctuation":{pattern:/^::/,alias:"punctuation"}}},"quoted-literal-block":{pattern:/::(?:\r?\n|\r){2}([!"#$%&'()*+,\-.\/:;<=>?@\[\\\]^_`{|}~]).*(?:(?:\r?\n|\r)\1.*)*/,inside:{"literal-block-punctuation":{pattern:/^(?:::|([!"#$%&'()*+,\-.\/:;<=>?@\[\\\]^_`{|}~])\1*)/m,alias:"punctuation"}}},"list-bullet":{pattern:/(^[\t ]*)(?:[*+\-•‣⁃]|\(?(?:\d+|[a-z]|[ivxdclm]+)\)|(?:\d+|[a-z]|[ivxdclm]+)\.)(?= )/im,lookbehind:!0,alias:"punctuation"},"doctest-block":{pattern:/(^[\t ]*)>>> .+(?:(?:\r?\n|\r).+)*/m,lookbehind:!0,inside:{punctuation:/^>>>/}},inline:[{pattern:/(^|[\s\-:\/'"<(\[{])(?::[^:]+:`.*?`|`.*?`:[^:]+:|(\*\*?|``?|\|)(?!\s)(?:(?!\2).)*\S\2(?=[\s\-.,:;!?\\\/'")\]}]|$))/m,lookbehind:!0,inside:{bold:{pattern:/(^\*\*).+(?=\*\*$)/,lookbehind:!0},italic:{pattern:/(^\*).+(?=\*$)/,lookbehind:!0},"inline-literal":{pattern:/(^``).+(?=``$)/,lookbehind:!0,alias:"symbol"},role:{pattern:/^:[^:]+:|:[^:]+:$/,alias:"function",inside:{punctuation:/^:|:$/}},"interpreted-text":{pattern:/(^`).+(?=`$)/,lookbehind:!0,alias:"attr-value"},substitution:{pattern:/(^\|).+(?=\|$)/,lookbehind:!0,alias:"attr-value"},punctuation:/\*\*?|``?|\|/}}],link:[{pattern:/\[[^\[\]]+\]_(?=[\s\-.,:;!?\\\/'")\]}]|$)/,alias:"string",inside:{punctuation:/^\[|\]_$/}},{pattern:/(?:\b[a-z\d]+(?:[_.:+][a-z\d]+)*_?_|`[^`]+`_?_|_`[^`]+`)(?=[\s\-.,:;!?\\\/'")\]}]|$)/i,alias:"string",inside:{punctuation:/^_?`|`$|`?_?_$/}}],punctuation:{pattern:/(^[\t ]*)(?:\|(?= |$)|(?:---?|—|\.\.|__)(?= )|\.\.$)/m,lookbehind:!0}};
-Prism.languages.sql={comment:{pattern:/(^|[^\\])(?:\/\*[\s\S]*?\*\/|(?:--|\/\/|#).*)/,lookbehind:!0},variable:[{pattern:/@(["'`])(?:\\[\s\S]|(?!\1)[^\\])+\1/,greedy:!0},/@[\w.$]+/],string:{pattern:/(^|[^@\\])("|')(?:\\[\s\S]|(?!\2)[^\\]|\2\2)*\2/,greedy:!0,lookbehind:!0},identifier:{pattern:/(^|[^@\\])`(?:\\[\s\S]|[^`\\]|``)*`/,greedy:!0,lookbehind:!0,inside:{punctuation:/^`|`$/}},function:/\b(?:AVG|COUNT|FIRST|FORMAT|LAST|LCASE|LEN|MAX|MID|MIN|MOD|NOW|ROUND|SUM|UCASE)(?=\s*\()/i,keyword:/\b(?:ACTION|ADD|AFTER|ALGORITHM|ALL|ALTER|ANALYZE|ANY|APPLY|AS|ASC|AUTHORIZATION|AUTO_INCREMENT|BACKUP|BDB|BEGIN|BERKELEYDB|BIGINT|BINARY|BIT|BLOB|BOOL|BOOLEAN|BREAK|BROWSE|BTREE|BULK|BY|CALL|CASCADED?|CASE|CHAIN|CHAR(?:ACTER|SET)?|CHECK(?:POINT)?|CLOSE|CLUSTERED|COALESCE|COLLATE|COLUMNS?|COMMENT|COMMIT(?:TED)?|COMPUTE|CONNECT|CONSISTENT|CONSTRAINT|CONTAINS(?:TABLE)?|CONTINUE|CONVERT|CREATE|CROSS|CURRENT(?:_DATE|_TIME|_TIMESTAMP|_USER)?|CURSOR|CYCLE|DATA(?:BASES?)?|DATE(?:TIME)?|DAY|DBCC|DEALLOCATE|DEC|DECIMAL|DECLARE|DEFAULT|DEFINER|DELAYED|DELETE|DELIMITERS?|DENY|DESC|DESCRIBE|DETERMINISTIC|DISABLE|DISCARD|DISK|DISTINCT|DISTINCTROW|DISTRIBUTED|DO|DOUBLE|DROP|DUMMY|DUMP(?:FILE)?|DUPLICATE|ELSE(?:IF)?|ENABLE|ENCLOSED|END|ENGINE|ENUM|ERRLVL|ERRORS|ESCAPED?|EXCEPT|EXEC(?:UTE)?|EXISTS|EXIT|EXPLAIN|EXTENDED|FETCH|FIELDS|FILE|FILLFACTOR|FIRST|FIXED|FLOAT|FOLLOWING|FOR(?: EACH ROW)?|FORCE|FOREIGN|FREETEXT(?:TABLE)?|FROM|FULL|FUNCTION|GEOMETRY(?:COLLECTION)?|GLOBAL|GOTO|GRANT|GROUP|HANDLER|HASH|HAVING|HOLDLOCK|HOUR|IDENTITY(?:COL|_INSERT)?|IF|IGNORE|IMPORT|INDEX|INFILE|INNER|INNODB|INOUT|INSERT|INT|INTEGER|INTERSECT|INTERVAL|INTO|INVOKER|ISOLATION|ITERATE|JOIN|KEYS?|KILL|LANGUAGE|LAST|LEAVE|LEFT|LEVEL|LIMIT|LINENO|LINES|LINESTRING|LOAD|LOCAL|LOCK|LONG(?:BLOB|TEXT)|LOOP|MATCH(?:ED)?|MEDIUM(?:BLOB|INT|TEXT)|MERGE|MIDDLEINT|MINUTE|MODE|MODIFIES|MODIFY|MONTH|MULTI(?:LINESTRING|POINT|POLYGON)|NATIONAL|NATURAL|NCHAR|NEXT|NO|NONCLUSTERED|NULLIF|NUMERIC|OFF?|OFFSETS?|ON|OPEN(?:DATASOURCE|QUERY|ROWSET)?|OPTIMIZE|OPTION(?:ALLY)?|ORDER|OUT(?:ER|FILE)?|OVER|PARTIAL|PARTITION|PERCENT|PIVOT|PLAN|POINT|POLYGON|PRECEDING|PRECISION|PREPARE|PREV|PRIMARY|PRINT|PRIVILEGES|PROC(?:EDURE)?|PUBLIC|PURGE|QUICK|RAISERROR|READS?|REAL|RECONFIGURE|REFERENCES|RELEASE|RENAME|REPEAT(?:ABLE)?|REPLACE|REPLICATION|REQUIRE|RESIGNAL|RESTORE|RESTRICT|RETURN(?:ING|S)?|REVOKE|RIGHT|ROLLBACK|ROUTINE|ROW(?:COUNT|GUIDCOL|S)?|RTREE|RULE|SAVE(?:POINT)?|SCHEMA|SECOND|SELECT|SERIAL(?:IZABLE)?|SESSION(?:_USER)?|SET(?:USER)?|SHARE|SHOW|SHUTDOWN|SIMPLE|SMALLINT|SNAPSHOT|SOME|SONAME|SQL|START(?:ING)?|STATISTICS|STATUS|STRIPED|SYSTEM_USER|TABLES?|TABLESPACE|TEMP(?:ORARY|TABLE)?|TERMINATED|TEXT(?:SIZE)?|THEN|TIME(?:STAMP)?|TINY(?:BLOB|INT|TEXT)|TOP?|TRAN(?:SACTIONS?)?|TRIGGER|TRUNCATE|TSEQUAL|TYPES?|UNBOUNDED|UNCOMMITTED|UNDEFINED|UNION|UNIQUE|UNLOCK|UNPIVOT|UNSIGNED|UPDATE(?:TEXT)?|USAGE|USE|USER|USING|VALUES?|VAR(?:BINARY|CHAR|CHARACTER|YING)|VIEW|WAITFOR|WARNINGS|WHEN|WHERE|WHILE|WITH(?: ROLLUP|IN)?|WORK|WRITE(?:TEXT)?|YEAR)\b/i,boolean:/\b(?:FALSE|NULL|TRUE)\b/i,number:/\b0x[\da-f]+\b|\b\d+(?:\.\d*)?|\B\.\d+\b/i,operator:/[-+*\/=%^~]|&&?|\|\|?|!=?|<(?:=>?|<|>)?|>[>=]?|\b(?:AND|BETWEEN|DIV|ILIKE|IN|IS|LIKE|NOT|OR|REGEXP|RLIKE|SOUNDS LIKE|XOR)\b/i,punctuation:/[;[\]()`,.]/};
-!function(e){function n(e){return e.replace(/__/g,(function(){return"(?:[\\w-]+|'[^'\n\r]*'|\"(?:\\\\.|[^\\\\\"\r\n])*\")"}))}e.languages.toml={comment:{pattern:/#.*/,greedy:!0},table:{pattern:RegExp(n("(^[\t ]*\\[\\s*(?:\\[\\s*)?)__(?:\\s*\\.\\s*__)*(?=\\s*\\])"),"m"),lookbehind:!0,greedy:!0,alias:"class-name"},key:{pattern:RegExp(n("(^[\t ]*|[{,]\\s*)__(?:\\s*\\.\\s*__)*(?=\\s*=)"),"m"),lookbehind:!0,greedy:!0,alias:"property"},string:{pattern:/"""(?:\\[\s\S]|[^\\])*?"""|'''[\s\S]*?'''|'[^'\n\r]*'|"(?:\\.|[^\\"\r\n])*"/,greedy:!0},date:[{pattern:/\b\d{4}-\d{2}-\d{2}(?:[T\s]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?)?\b/i,alias:"number"},{pattern:/\b\d{2}:\d{2}:\d{2}(?:\.\d+)?\b/,alias:"number"}],number:/(?:\b0(?:x[\da-zA-Z]+(?:_[\da-zA-Z]+)*|o[0-7]+(?:_[0-7]+)*|b[10]+(?:_[10]+)*))\b|[-+]?\b\d+(?:_\d+)*(?:\.\d+(?:_\d+)*)?(?:[eE][+-]?\d+(?:_\d+)*)?\b|[-+]?\b(?:inf|nan)\b/,boolean:/\b(?:false|true)\b/,punctuation:/[.,=[\]{}]/}}(Prism);
-Prism.languages.vim={string:/"(?:[^"\\\r\n]|\\.)*"|'(?:[^'\r\n]|'')*'/,comment:/".*/,function:/\b\w+(?=\()/,keyword:/\b(?:N|Next|P|Print|X|XMLent|XMLns|ab|abbreviate|abc|abclear|abo|aboveleft|al|all|ar|arga|argadd|argd|argdelete|argdo|arge|argedit|argg|argglobal|argl|arglocal|args|argu|argument|as|ascii|b|bN|bNext|ba|bad|badd|ball|bd|bdelete|be|bel|belowright|bf|bfirst|bl|blast|bm|bmodified|bn|bnext|bo|botright|bp|bprevious|br|brea|break|breaka|breakadd|breakd|breakdel|breakl|breaklist|brewind|bro|browse|bufdo|buffer|buffers|bun|bunload|bw|bwipeout|c|cN|cNext|cNfcNfile|ca|cabbrev|cabc|cabclear|cad|caddb|caddbuffer|caddexpr|caddf|caddfile|cal|call|cat|catch|cb|cbuffer|cc|ccl|cclose|cd|ce|center|cex|cexpr|cf|cfile|cfir|cfirst|cg|cgetb|cgetbuffer|cgete|cgetexpr|cgetfile|change|changes|chd|chdir|che|checkpath|checkt|checktime|cl|cla|clast|clist|clo|close|cmapc|cmapclear|cn|cnew|cnewer|cnext|cnf|cnfile|cnorea|cnoreabbrev|co|col|colder|colo|colorscheme|comc|comclear|comp|compiler|con|conf|confirm|continue|cope|copen|copy|cp|cpf|cpfile|cprevious|cq|cquit|cr|crewind|cu|cuna|cunabbrev|cunmap|cw|cwindow|d|debugg|debuggreedy|delc|delcommand|delete|delf|delfunction|delm|delmarks|di|diffg|diffget|diffoff|diffpatch|diffpu|diffput|diffsplit|diffthis|diffu|diffupdate|dig|digraphs|display|dj|djump|dl|dlist|dr|drop|ds|dsearch|dsp|dsplit|e|earlier|echoe|echoerr|echom|echomsg|echon|edit|el|else|elsei|elseif|em|emenu|en|endf|endfo|endfor|endfun|endfunction|endif|endt|endtry|endw|endwhile|ene|enew|ex|exi|exit|exu|exusage|f|file|files|filetype|fin|fina|finally|find|fini|finish|fir|first|fix|fixdel|fo|fold|foldc|foldclose|foldd|folddoc|folddoclosed|folddoopen|foldo|foldopen|for|fu|fun|function|go|goto|gr|grep|grepa|grepadd|h|ha|hardcopy|help|helpf|helpfind|helpg|helpgrep|helpt|helptags|hid|hide|his|history|ia|iabbrev|iabc|iabclear|if|ij|ijump|il|ilist|imapc|imapclear|in|inorea|inoreabbrev|isearch|isp|isplit|iu|iuna|iunabbrev|iunmap|j|join|ju|jumps|k|kee|keepalt|keepj|keepjumps|keepmarks|l|lN|lNext|lNf|lNfile|la|lad|laddb|laddbuffer|laddexpr|laddf|laddfile|lan|language|last|later|lb|lbuffer|lc|lcd|lch|lchdir|lcl|lclose|left|lefta|leftabove|let|lex|lexpr|lf|lfile|lfir|lfirst|lg|lgetb|lgetbuffer|lgete|lgetexpr|lgetfile|lgr|lgrep|lgrepa|lgrepadd|lh|lhelpgrep|list|ll|lla|llast|lli|llist|lm|lmak|lmake|lmap|lmapc|lmapclear|ln|lne|lnew|lnewer|lnext|lnf|lnfile|lnoremap|lo|loadview|loc|lockmarks|lockv|lockvar|lol|lolder|lop|lopen|lp|lpf|lpfile|lprevious|lr|lrewind|ls|lt|ltag|lu|lunmap|lv|lvimgrep|lvimgrepa|lvimgrepadd|lw|lwindow|m|ma|mak|make|mark|marks|mat|match|menut|menutranslate|mk|mkexrc|mks|mksession|mksp|mkspell|mkv|mkvie|mkview|mkvimrc|mod|mode|move|mz|mzf|mzfile|mzscheme|n|nbkey|new|next|nmapc|nmapclear|noh|nohlsearch|norea|noreabbrev|nu|number|nun|nunmap|o|omapc|omapclear|on|only|open|opt|options|ou|ounmap|p|pc|pclose|pe|ped|pedit|perl|perld|perldo|po|pop|popu|popup|pp|ppop|pre|preserve|prev|previous|print|prof|profd|profdel|profile|promptf|promptfind|promptr|promptrepl|ps|psearch|ptN|ptNext|pta|ptag|ptf|ptfirst|ptj|ptjump|ptl|ptlast|ptn|ptnext|ptp|ptprevious|ptr|ptrewind|pts|ptselect|pu|put|pw|pwd|py|pyf|pyfile|python|q|qa|qall|quit|quita|quitall|r|read|rec|recover|red|redi|redir|redo|redr|redraw|redraws|redrawstatus|reg|registers|res|resize|ret|retab|retu|return|rew|rewind|ri|right|rightb|rightbelow|ru|rub|ruby|rubyd|rubydo|rubyf|rubyfile|runtime|rv|rviminfo|sN|sNext|sa|sal|sall|san|sandbox|sargument|sav|saveas|sb|sbN|sbNext|sba|sball|sbf|sbfirst|sbl|sblast|sbm|sbmodified|sbn|sbnext|sbp|sbprevious|sbr|sbrewind|sbuffer|scrip|scripte|scriptencoding|scriptnames|se|set|setf|setfiletype|setg|setglobal|setl|setlocal|sf|sfind|sfir|sfirst|sh|shell|sign|sil|silent|sim|simalt|sl|sla|slast|sleep|sm|smagic|smap|smapc|smapclear|sme|smenu|sn|snext|sni|sniff|sno|snomagic|snor|snoremap|snoreme|snoremenu|so|sor|sort|source|sp|spe|spelld|spelldump|spellgood|spelli|spellinfo|spellr|spellrepall|spellu|spellundo|spellw|spellwrong|split|spr|sprevious|sre|srewind|st|sta|stag|star|startg|startgreplace|startinsert|startr|startreplace|stj|stjump|stop|stopi|stopinsert|sts|stselect|sun|sunhide|sunm|sunmap|sus|suspend|sv|sview|syncbind|t|tN|tNext|ta|tab|tabN|tabNext|tabc|tabclose|tabd|tabdo|tabe|tabedit|tabf|tabfind|tabfir|tabfirst|tabl|tablast|tabm|tabmove|tabn|tabnew|tabnext|tabo|tabonly|tabp|tabprevious|tabr|tabrewind|tabs|tag|tags|tc|tcl|tcld|tcldo|tclf|tclfile|te|tearoff|tf|tfirst|th|throw|tj|tjump|tl|tlast|tm|tmenu|tn|tnext|to|topleft|tp|tprevious|tr|trewind|try|ts|tselect|tu|tunmenu|u|una|unabbreviate|undo|undoj|undojoin|undol|undolist|unh|unhide|unlet|unlo|unlockvar|unm|unmap|up|update|ve|verb|verbose|version|vert|vertical|vi|vie|view|vim|vimgrep|vimgrepa|vimgrepadd|visual|viu|viusage|vmapc|vmapclear|vne|vnew|vs|vsplit|vu|vunmap|w|wN|wNext|wa|wall|wh|while|win|winc|wincmd|windo|winp|winpos|winsize|wn|wnext|wp|wprevious|wq|wqa|wqall|write|ws|wsverb|wv|wviminfo|x|xa|xall|xit|xm|xmap|xmapc|xmapclear|xme|xmenu|xn|xnoremap|xnoreme|xnoremenu|xu|xunmap|y|yank)\b/,builtin:/\b(?:acd|ai|akm|aleph|allowrevins|altkeymap|ambiwidth|ambw|anti|antialias|arab|arabic|arabicshape|ari|arshape|autochdir|autocmd|autoindent|autoread|autowrite|autowriteall|aw|awa|background|backspace|backup|backupcopy|backupdir|backupext|backupskip|balloondelay|ballooneval|balloonexpr|bdir|bdlay|beval|bex|bexpr|bg|bh|bin|binary|biosk|bioskey|bk|bkc|bomb|breakat|brk|browsedir|bs|bsdir|bsk|bt|bufhidden|buflisted|buftype|casemap|ccv|cdpath|cedit|cfu|ch|charconvert|ci|cin|cindent|cink|cinkeys|cino|cinoptions|cinw|cinwords|clipboard|cmdheight|cmdwinheight|cmp|cms|columns|com|comments|commentstring|compatible|complete|completefunc|completeopt|consk|conskey|copyindent|cot|cpo|cpoptions|cpt|cscopepathcomp|cscopeprg|cscopequickfix|cscopetag|cscopetagorder|cscopeverbose|cspc|csprg|csqf|cst|csto|csverb|cuc|cul|cursorcolumn|cursorline|cwh|debug|deco|def|define|delcombine|dex|dg|dict|dictionary|diff|diffexpr|diffopt|digraph|dip|dir|directory|dy|ea|ead|eadirection|eb|ed|edcompatible|ef|efm|ei|ek|enc|encoding|endofline|eol|ep|equalalways|equalprg|errorbells|errorfile|errorformat|esckeys|et|eventignore|expandtab|exrc|fcl|fcs|fdc|fde|fdi|fdl|fdls|fdm|fdn|fdo|fdt|fen|fenc|fencs|fex|ff|ffs|fileencoding|fileencodings|fileformat|fileformats|fillchars|fk|fkmap|flp|fml|fmr|foldcolumn|foldenable|foldexpr|foldignore|foldlevel|foldlevelstart|foldmarker|foldmethod|foldminlines|foldnestmax|foldtext|formatexpr|formatlistpat|formatoptions|formatprg|fp|fs|fsync|ft|gcr|gd|gdefault|gfm|gfn|gfs|gfw|ghr|gp|grepformat|grepprg|gtl|gtt|guicursor|guifont|guifontset|guifontwide|guiheadroom|guioptions|guipty|guitablabel|guitabtooltip|helpfile|helpheight|helplang|hf|hh|hi|hidden|highlight|hk|hkmap|hkmapp|hkp|hl|hlg|hls|hlsearch|ic|icon|iconstring|ignorecase|im|imactivatekey|imak|imc|imcmdline|imd|imdisable|imi|iminsert|ims|imsearch|inc|include|includeexpr|incsearch|inde|indentexpr|indentkeys|indk|inex|inf|infercase|insertmode|invacd|invai|invakm|invallowrevins|invaltkeymap|invanti|invantialias|invar|invarab|invarabic|invarabicshape|invari|invarshape|invautochdir|invautoindent|invautoread|invautowrite|invautowriteall|invaw|invawa|invbackup|invballooneval|invbeval|invbin|invbinary|invbiosk|invbioskey|invbk|invbl|invbomb|invbuflisted|invcf|invci|invcin|invcindent|invcompatible|invconfirm|invconsk|invconskey|invcopyindent|invcp|invcscopetag|invcscopeverbose|invcst|invcsverb|invcuc|invcul|invcursorcolumn|invcursorline|invdeco|invdelcombine|invdg|invdiff|invdigraph|invdisable|invea|inveb|inved|invedcompatible|invek|invendofline|inveol|invequalalways|inverrorbells|invesckeys|invet|invex|invexpandtab|invexrc|invfen|invfk|invfkmap|invfoldenable|invgd|invgdefault|invguipty|invhid|invhidden|invhk|invhkmap|invhkmapp|invhkp|invhls|invhlsearch|invic|invicon|invignorecase|invim|invimc|invimcmdline|invimd|invincsearch|invinf|invinfercase|invinsertmode|invis|invjoinspaces|invjs|invlazyredraw|invlbr|invlinebreak|invlisp|invlist|invloadplugins|invlpl|invlz|invma|invmacatsui|invmagic|invmh|invml|invmod|invmodeline|invmodifiable|invmodified|invmore|invmousef|invmousefocus|invmousehide|invnu|invnumber|invodev|invopendevice|invpaste|invpi|invpreserveindent|invpreviewwindow|invprompt|invpvw|invreadonly|invremap|invrestorescreen|invrevins|invri|invrightleft|invrightleftcmd|invrl|invrlc|invro|invrs|invru|invruler|invsb|invsc|invscb|invscrollbind|invscs|invsecure|invsft|invshellslash|invshelltemp|invshiftround|invshortname|invshowcmd|invshowfulltag|invshowmatch|invshowmode|invsi|invsm|invsmartcase|invsmartindent|invsmarttab|invsmd|invsn|invsol|invspell|invsplitbelow|invsplitright|invspr|invsr|invssl|invsta|invstartofline|invstmp|invswapfile|invswf|invta|invtagbsearch|invtagrelative|invtagstack|invtbi|invtbidi|invtbs|invtermbidi|invterse|invtextauto|invtextmode|invtf|invtgst|invtildeop|invtimeout|invtitle|invto|invtop|invtr|invttimeout|invttybuiltin|invttyfast|invtx|invvb|invvisualbell|invwa|invwarn|invwb|invweirdinvert|invwfh|invwfw|invwildmenu|invwinfixheight|invwinfixwidth|invwiv|invwmnu|invwrap|invwrapscan|invwrite|invwriteany|invwritebackup|invws|isf|isfname|isi|isident|isk|iskeyword|isprint|joinspaces|js|key|keymap|keymodel|keywordprg|km|kmp|kp|langmap|langmenu|laststatus|lazyredraw|lbr|lcs|linebreak|lines|linespace|lisp|lispwords|listchars|loadplugins|lpl|lsp|lz|macatsui|magic|makeef|makeprg|matchpairs|matchtime|maxcombine|maxfuncdepth|maxmapdepth|maxmem|maxmempattern|maxmemtot|mco|mef|menuitems|mfd|mh|mis|mkspellmem|ml|mls|mm|mmd|mmp|mmt|modeline|modelines|modifiable|modified|more|mouse|mousef|mousefocus|mousehide|mousem|mousemodel|mouses|mouseshape|mouset|mousetime|mp|mps|msm|mzq|mzquantum|nf|noacd|noai|noakm|noallowrevins|noaltkeymap|noanti|noantialias|noar|noarab|noarabic|noarabicshape|noari|noarshape|noautochdir|noautoindent|noautoread|noautowrite|noautowriteall|noaw|noawa|nobackup|noballooneval|nobeval|nobin|nobinary|nobiosk|nobioskey|nobk|nobl|nobomb|nobuflisted|nocf|noci|nocin|nocindent|nocompatible|noconfirm|noconsk|noconskey|nocopyindent|nocp|nocscopetag|nocscopeverbose|nocst|nocsverb|nocuc|nocul|nocursorcolumn|nocursorline|nodeco|nodelcombine|nodg|nodiff|nodigraph|nodisable|noea|noeb|noed|noedcompatible|noek|noendofline|noeol|noequalalways|noerrorbells|noesckeys|noet|noex|noexpandtab|noexrc|nofen|nofk|nofkmap|nofoldenable|nogd|nogdefault|noguipty|nohid|nohidden|nohk|nohkmap|nohkmapp|nohkp|nohls|noic|noicon|noignorecase|noim|noimc|noimcmdline|noimd|noincsearch|noinf|noinfercase|noinsertmode|nois|nojoinspaces|nojs|nolazyredraw|nolbr|nolinebreak|nolisp|nolist|noloadplugins|nolpl|nolz|noma|nomacatsui|nomagic|nomh|noml|nomod|nomodeline|nomodifiable|nomodified|nomore|nomousef|nomousefocus|nomousehide|nonu|nonumber|noodev|noopendevice|nopaste|nopi|nopreserveindent|nopreviewwindow|noprompt|nopvw|noreadonly|noremap|norestorescreen|norevins|nori|norightleft|norightleftcmd|norl|norlc|noro|nors|noru|noruler|nosb|nosc|noscb|noscrollbind|noscs|nosecure|nosft|noshellslash|noshelltemp|noshiftround|noshortname|noshowcmd|noshowfulltag|noshowmatch|noshowmode|nosi|nosm|nosmartcase|nosmartindent|nosmarttab|nosmd|nosn|nosol|nospell|nosplitbelow|nosplitright|nospr|nosr|nossl|nosta|nostartofline|nostmp|noswapfile|noswf|nota|notagbsearch|notagrelative|notagstack|notbi|notbidi|notbs|notermbidi|noterse|notextauto|notextmode|notf|notgst|notildeop|notimeout|notitle|noto|notop|notr|nottimeout|nottybuiltin|nottyfast|notx|novb|novisualbell|nowa|nowarn|nowb|noweirdinvert|nowfh|nowfw|nowildmenu|nowinfixheight|nowinfixwidth|nowiv|nowmnu|nowrap|nowrapscan|nowrite|nowriteany|nowritebackup|nows|nrformats|numberwidth|nuw|odev|oft|ofu|omnifunc|opendevice|operatorfunc|opfunc|osfiletype|pa|para|paragraphs|paste|pastetoggle|patchexpr|patchmode|path|pdev|penc|pex|pexpr|pfn|ph|pheader|pi|pm|pmbcs|pmbfn|popt|preserveindent|previewheight|previewwindow|printdevice|printencoding|printexpr|printfont|printheader|printmbcharset|printmbfont|printoptions|prompt|pt|pumheight|pvh|pvw|qe|quoteescape|readonly|remap|report|restorescreen|revins|rightleft|rightleftcmd|rl|rlc|ro|rs|rtp|ruf|ruler|rulerformat|runtimepath|sbo|sc|scb|scr|scroll|scrollbind|scrolljump|scrolloff|scrollopt|scs|sect|sections|secure|sel|selection|selectmode|sessionoptions|sft|shcf|shellcmdflag|shellpipe|shellquote|shellredir|shellslash|shelltemp|shelltype|shellxquote|shiftround|shiftwidth|shm|shortmess|shortname|showbreak|showcmd|showfulltag|showmatch|showmode|showtabline|shq|si|sidescroll|sidescrolloff|siso|sj|slm|smartcase|smartindent|smarttab|smc|smd|softtabstop|sol|spc|spell|spellcapcheck|spellfile|spelllang|spellsuggest|spf|spl|splitbelow|splitright|sps|sr|srr|ss|ssl|ssop|stal|startofline|statusline|stl|stmp|su|sua|suffixes|suffixesadd|sw|swapfile|swapsync|swb|swf|switchbuf|sws|sxq|syn|synmaxcol|syntax|t_AB|t_AF|t_AL|t_CS|t_CV|t_Ce|t_Co|t_Cs|t_DL|t_EI|t_F1|t_F2|t_F3|t_F4|t_F5|t_F6|t_F7|t_F8|t_F9|t_IE|t_IS|t_K1|t_K3|t_K4|t_K5|t_K6|t_K7|t_K8|t_K9|t_KA|t_KB|t_KC|t_KD|t_KE|t_KF|t_KG|t_KH|t_KI|t_KJ|t_KK|t_KL|t_RI|t_RV|t_SI|t_Sb|t_Sf|t_WP|t_WS|t_ZH|t_ZR|t_al|t_bc|t_cd|t_ce|t_cl|t_cm|t_cs|t_da|t_db|t_dl|t_fs|t_k1|t_k2|t_k3|t_k4|t_k5|t_k6|t_k7|t_k8|t_k9|t_kB|t_kD|t_kI|t_kN|t_kP|t_kb|t_kd|t_ke|t_kh|t_kl|t_kr|t_ks|t_ku|t_le|t_mb|t_md|t_me|t_mr|t_ms|t_nd|t_op|t_se|t_so|t_sr|t_te|t_ti|t_ts|t_ue|t_us|t_ut|t_vb|t_ve|t_vi|t_vs|t_xs|tabline|tabpagemax|tabstop|tagbsearch|taglength|tagrelative|tagstack|tal|tb|tbi|tbidi|tbis|tbs|tenc|term|termbidi|termencoding|terse|textauto|textmode|textwidth|tgst|thesaurus|tildeop|timeout|timeoutlen|title|titlelen|titleold|titlestring|toolbar|toolbariconsize|top|tpm|tsl|tsr|ttimeout|ttimeoutlen|ttm|tty|ttybuiltin|ttyfast|ttym|ttymouse|ttyscroll|ttytype|tw|tx|uc|ul|undolevels|updatecount|updatetime|ut|vb|vbs|vdir|verbosefile|vfile|viewdir|viewoptions|viminfo|virtualedit|visualbell|vop|wak|warn|wb|wc|wcm|wd|weirdinvert|wfh|wfw|whichwrap|wi|wig|wildchar|wildcharm|wildignore|wildmenu|wildmode|wildoptions|wim|winaltkeys|window|winfixheight|winfixwidth|winheight|winminheight|winminwidth|winwidth|wiv|wiw|wm|wmh|wmnu|wmw|wop|wrap|wrapmargin|wrapscan|writeany|writebackup|writedelay|ww)\b/,number:/\b(?:0x[\da-f]+|\d+(?:\.\d+)?)\b/i,operator:/\|\||&&|[-+.]=?|[=!](?:[=~][#?]?)?|[<>]=?[#?]?|[*\/%?]|\b(?:is(?:not)?)\b/,punctuation:/[{}[\](),;:]/};
-!function(e){var n=/[*&][^\s[\]{},]+/,r=/!(?:<[\w\-%#;/?:@&=+$,.!~*'()[\]]+>|(?:[a-zA-Z\d-]*!)?[\w\-%#;/?:@&=+$.~*'()]+)?/,t="(?:"+r.source+"(?:[ \t]+"+n.source+")?|"+n.source+"(?:[ \t]+"+r.source+")?)",a="(?:[^\\s\\x00-\\x08\\x0e-\\x1f!\"#%&'*,\\-:>?@[\\]`{|}\\x7f-\\x84\\x86-\\x9f\\ud800-\\udfff\\ufffe\\uffff]|[?:-])(?:[ \t]*(?:(?![#:])|:))*".replace(//g,(function(){return"[^\\s\\x00-\\x08\\x0e-\\x1f,[\\]{}\\x7f-\\x84\\x86-\\x9f\\ud800-\\udfff\\ufffe\\uffff]"})),d="\"(?:[^\"\\\\\r\n]|\\\\.)*\"|'(?:[^'\\\\\r\n]|\\\\.)*'";function o(e,n){n=(n||"").replace(/m/g,"")+"m";var r="([:\\-,[{]\\s*(?:\\s<>[ \t]+)?)(?:<>)(?=[ \t]*(?:$|,|\\]|\\}|(?:[\r\n]\\s*)?#))".replace(/<>/g,(function(){return t})).replace(/<>/g,(function(){return e}));return RegExp(r,n)}e.languages.yaml={scalar:{pattern:RegExp("([\\-:]\\s*(?:\\s<>[ \t]+)?[|>])[ \t]*(?:((?:\r?\n|\r)[ \t]+)\\S[^\r\n]*(?:\\2[^\r\n]+)*)".replace(/<>/g,(function(){return t}))),lookbehind:!0,alias:"string"},comment:/#.*/,key:{pattern:RegExp("((?:^|[:\\-,[{\r\n?])[ \t]*(?:<>[ \t]+)?)<>(?=\\s*:\\s)".replace(/<>/g,(function(){return t})).replace(/<>/g,(function(){return"(?:"+a+"|"+d+")"}))),lookbehind:!0,greedy:!0,alias:"atrule"},directive:{pattern:/(^[ \t]*)%.+/m,lookbehind:!0,alias:"important"},datetime:{pattern:o("\\d{4}-\\d\\d?-\\d\\d?(?:[tT]|[ \t]+)\\d\\d?:\\d{2}:\\d{2}(?:\\.\\d*)?(?:[ \t]*(?:Z|[-+]\\d\\d?(?::\\d{2})?))?|\\d{4}-\\d{2}-\\d{2}|\\d\\d?:\\d{2}(?::\\d{2}(?:\\.\\d*)?)?"),lookbehind:!0,alias:"number"},boolean:{pattern:o("false|true","i"),lookbehind:!0,alias:"important"},null:{pattern:o("null|~","i"),lookbehind:!0,alias:"important"},string:{pattern:o(d),lookbehind:!0,greedy:!0},number:{pattern:o("[+-]?(?:0x[\\da-f]+|0o[0-7]+|(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[+-]?\\d+)?|\\.inf|\\.nan)","i"),lookbehind:!0},tag:r,important:n,punctuation:/---|[:[\]{}\-,|>?]|\.\.\./},e.languages.yml=e.languages.yaml}(Prism);
diff --git a/tailwind.config.js b/tailwind.config.js
deleted file mode 100644
index b7b9c40d..00000000
--- a/tailwind.config.js
+++ /dev/null
@@ -1,126 +0,0 @@
-/** @type {import('tailwindcss').Config} */
-module.exports = {
- content: ["./templates/*.html"],
- plugins: [require("@tailwindcss/typography")],
- variants: {
- typography: ["dark"],
- },
- theme: {
- fontFamily: {
- texts: ["Erode", "serif"],
- headings: ["Fira Sans", "sans-serif"],
- },
- extend: {
- // NOTE: this is for non-prose (so not Markdown rendered with typography plugin)
- colors: {
- // https://rosepinetheme.com/palette/
- "rp-dawn-base": "#faf4ed", // main background
- "rp-dawn-surface": "#fffaf3", // navigation background
- "rp-dawn-overlay": "#f2e9e1", // content background
- "rp-dawn-muted": "#9893a5",
- "rp-dawn-subtle": "#797593",
- "rp-dawn-text": "#575279", // text
- "rp-dawn-love": "#b4637a",
- "rp-dawn-gold": "#ea9d34",
- "rp-dawn-rose": "#d7827e",
- "rp-dawn-pine": "#286983",
- "rp-dawn-foam": "#56949f", // main site title
- "rp-dawn-iris": "#907aa9", // links
- "rp-dawn-highlight-low": "#f4ede8",
- "rp-dawn-highlight-med": "#dfdad9", // footer
- "rp-dawn-highlight-high": "#cecacd",
- "rp-moon-base": "#232136", // main background
- "rp-moon-surface": "#2a273f", // navigation background
- "rp-moon-overlay": "#393552", // content background
- "rp-moon-muted": "#6e6a86",
- "rp-moon-subtle": "#908caa",
- "rp-moon-text": "#e0def4",
- "rp-moon-love": "#eb6f92",
- "rp-moon-gold": "#f6c177",
- "rp-moon-rose": "#ea9a97",
- "rp-moon-pine": "#3e8fb0",
- "rp-moon-foam": "#9ccfd8", // main site title
- "rp-moon-iris": "#c4a7e7", // text,
- "rp-moon-highlight-low": "#2a283e",
- "rp-moon-highlight-med": "#44415a", // footer
- "rp-moon-highlight-high": "#56526e",
- },
- typography: (theme) => ({
- // NOTE: This is for prose (Markdown) in LIGHT mode
- DEFAULT: {
- css: {
- maxWidth: "80ch",
- pre: null,
- code: null,
- "code::before": null,
- "code::after": null,
- "pre code": null,
- "pre code::before": null,
- "pre code::after": null,
- // remove backticks from typography for inline code
- "code::before": {
- content: '""',
- },
- "code::after": {
- content: '""',
- },
- color: theme("colors.rp-dawn-text"), // main text
- a: {
- color: theme("colors.rp-dawn-text"), // align w/ in.css
- "text-decoration-color": theme("colors.rp-dawn-gold"), // align w/ in.css
- "text-decoration-thickness": "2px", // align w/ in.css
- },
- "h1, h2, h3, h4, h5, h6": {
- color: theme("colors.rp-dawn-text"),
- },
- blockquote: {
- "border-color": theme("colors.rp-dawn-muted"),
- "background-color": theme("colors.rp-dawn-overlay"),
- color: theme("colors.rp-dawn-text"),
- },
- strong: {
- color: theme("colors.rp-dawn-text"), // align w/ main text color
- fontWeight: "800",
- },
- th: {
- color: theme("colors.rp-dawn-text"),
- },
- },
- },
- // NOTE: This is for prose (Markdown) in DARK mode
- dark: {
- css: {
- pre: null,
- code: null,
- "pre code": null,
- color: theme("colors.rp-moon-iris"), // main text
- a: {
- color: theme("colors.rp-moon-iris"), // align w/ in.css
- "text-decoration-color": theme("colors.rp-moon-pine"), // align w/ in.css
- "text-decoration-thickness": "2px", // align w/ in.css
- },
- "h1, h2, h3, h4, h5, h6": {
- color: theme("colors.rp-moon-love"),
- },
- blockquote: {
- "border-color": theme("colors.rp-moon-overlay"),
- "background-color": theme("colors.rp-moon-surface"),
- color: theme("colors.rp-moon-text"),
- },
- strong: {
- color: theme("colors.rp-moon-iris"), // align w/ main text color
- fontWeight: "800",
- },
- th: {
- color: theme("colors.rp-moon-iris"),
- },
- },
- },
- }),
- },
- },
- corePlugins: {
- // preflight: false,
- divideStyle: true,
- },
-};
diff --git a/tasks.py b/tasks.py
new file mode 100644
index 00000000..76c48834
--- /dev/null
+++ b/tasks.py
@@ -0,0 +1,116 @@
+import os
+from pathlib import Path
+from shutil import which
+
+from invoke import task
+
+PKG_NAME = "pelican"
+PKG_PATH = Path("pelican")
+DOCS_PORT = os.environ.get("DOCS_PORT", 8000)
+BIN_DIR = "bin" if os.name != "nt" else "Scripts"
+ACTIVE_VENV = os.environ.get("VIRTUAL_ENV", None)
+VENV_HOME = Path(os.environ.get("WORKON_HOME", "~/virtualenvs"))
+VENV_PATH = Path(ACTIVE_VENV) if ACTIVE_VENV else (VENV_HOME / PKG_NAME)
+VENV = str(VENV_PATH.expanduser())
+VENV_BIN = Path(VENV) / Path(BIN_DIR)
+
+TOOLS = ["poetry", "pre-commit"]
+POETRY = which("poetry") if which("poetry") else (VENV_BIN / "poetry")
+PRECOMMIT = (
+ which("pre-commit") if which("pre-commit") else (VENV_BIN / "pre-commit")
+)
+
+
+@task
+def docbuild(c):
+ """Build documentation"""
+ c.run(f"{VENV_BIN}/sphinx-build -W docs docs/_build")
+
+
+@task(docbuild)
+def docserve(c):
+ """Serve docs at http://localhost:$DOCS_PORT/ (default port is 8000)"""
+ from livereload import Server
+
+ server = Server()
+ server.watch("docs/conf.py", lambda: docbuild(c))
+ server.watch("CONTRIBUTING.rst", lambda: docbuild(c))
+ server.watch("docs/*.rst", lambda: docbuild(c))
+ server.serve(port=DOCS_PORT, root="docs/_build")
+
+
+@task
+def tests(c):
+ """Run the test suite"""
+ c.run(f"{VENV_BIN}/pytest", pty=True)
+
+
+@task
+def black(c, check=False, diff=False):
+ """Run Black auto-formatter, optionally with --check or --diff"""
+ check_flag, diff_flag = "", ""
+ if check:
+ check_flag = "--check"
+ if diff:
+ diff_flag = "--diff"
+ c.run(f"{VENV_BIN}/black {check_flag} {diff_flag} {PKG_PATH} tasks.py")
+
+
+@task
+def isort(c, check=False, diff=False):
+ check_flag, diff_flag = "", ""
+ if check:
+ check_flag = "-c"
+ if diff:
+ diff_flag = "--diff"
+ c.run(
+ f"{VENV_BIN}/isort {check_flag} {diff_flag} ."
+ )
+
+
+@task
+def flake8(c):
+ c.run(f"{VENV_BIN}/flake8 {PKG_PATH} tasks.py")
+
+
+@task
+def lint(c):
+ isort(c, check=True)
+ black(c, check=True)
+ flake8(c)
+
+
+@task
+def tools(c):
+ """Install tools in the virtual environment if not already on PATH"""
+ for tool in TOOLS:
+ if not which(tool):
+ c.run(f"{VENV_BIN}/python -m pip install {tool}")
+
+
+@task
+def precommit(c):
+ """Install pre-commit hooks to .git/hooks/pre-commit"""
+ c.run(f"{PRECOMMIT} install")
+
+
+@task
+def setup(c):
+ c.run(f"{VENV_BIN}/python -m pip install -U pip")
+ tools(c)
+ c.run(f"{POETRY} install")
+ precommit(c)
+
+
+@task
+def update_functional_tests(c):
+ """Update the generated functional test output"""
+ c.run(
+ f"bash -c 'LC_ALL=en_US.utf8 pelican -o {PKG_PATH}/tests/output/custom/ -s samples/pelican.conf.py samples/content/'"
+ )
+ c.run(
+ f"bash -c 'LC_ALL=fr_FR.utf8 pelican -o {PKG_PATH}/tests/output/custom_locale/ -s samples/pelican.conf_FR.py samples/content/'"
+ )
+ c.run(
+ f"bash -c 'LC_ALL=en_US.utf8 pelican -o {PKG_PATH}/tests/output/basic/ samples/content/'"
+ )
diff --git a/templates/archives.html b/templates/archives.html
deleted file mode 100644
index 9b15a164..00000000
--- a/templates/archives.html
+++ /dev/null
@@ -1,28 +0,0 @@
-{% extends "base.html" %}
-
-{% block title %}Blog archive - {{ SITENAME|striptags }}{% endblock %}
-
-{% block content %}
-
-
Blog post archive
-
- {% for article in dates %}
- {% set year = article.date.strftime('%Y') %}
- {% if loop.first %}
-
- {% else %}
- {% set prevyear = loop.previtem.date.strftime('%Y') %}
- {% if prevyear != year %}
-
- {% endif %}
- {% endif %}
- {% set month = article.date.strftime('%m') %}
- {% set day = article.date.strftime('%d') %}
- {{ article.locale_date }}: {{ article.title }}
- {%if article.subtitle %}
- {{ article.subtitle }}
- {% endif %}
- {% endfor %}
-
-
-{% endblock %}
diff --git a/templates/article.html b/templates/article.html
deleted file mode 100644
index c4dc973d..00000000
--- a/templates/article.html
+++ /dev/null
@@ -1,139 +0,0 @@
-{% extends "base.html" %}
-{% block html_lang %}{{ article.lang }}{% endblock %}
-
-{% block title %}{{ article.title|striptags }} - {{ SITENAME|striptags }}{% endblock %}
-
-{% block head %}
- {{ super() }}
-
- {% if article.description %}
-
- {% endif %}
-
- {% for tag in article.tags %}
-
- {% endfor %}
-
-{% endblock %}
-
-{% block content %}
-
-
- {{ article.content }}
-
-
-
- {% if article.category %}
-
- Category
-
- {% endif %}
- {% if article.tags %}
-
- Tags
-
- {% endif %}
-
-
- {% if article.category %}
-
- {{ article.category }}
-
- {% endif %}
- {% if article.tags %}
-
- {% for tag in article.tags %}
- {{ tag }} {{"," if not loop.last }}
- {% endfor %}
-
- {% endif %}
-
-
- {% if article.share_post and article.status != 'draft' %}
-
- {% endif %}
-
-
-{% endblock %}
diff --git a/templates/author.html b/templates/author.html
deleted file mode 100644
index 1a5a8903..00000000
--- a/templates/author.html
+++ /dev/null
@@ -1,7 +0,0 @@
-{% extends "index.html" %}
-
-{% block title %}{{ SITENAME|striptags }} - Articles by {{ author }}{% endblock %}
-
-{% block content_title %}
- Articles by {{ author }}
-{% endblock %}
diff --git a/templates/authors.html b/templates/authors.html
deleted file mode 100644
index 548421fb..00000000
--- a/templates/authors.html
+++ /dev/null
@@ -1,13 +0,0 @@
-{% extends "base.html" %}
-
-{% block title %}Authors - {{ SITENAME|striptags }}{% endblock %}
-
-{% block content %}
- Authors
- It's all me, silly you.
-
-
-
-
-
-{% endblock %}
diff --git a/templates/base.html b/templates/base.html
deleted file mode 100644
index ad7df453..00000000
--- a/templates/base.html
+++ /dev/null
@@ -1,160 +0,0 @@
-
-
-
- {% block head %}
- {% block title %}{{ SITENAME|striptags }}{% endblock title %}
-
-
-
- {% if SITESUBTITLE %}
-
- {% endif %}
-
- {% if STYLESHEET_URL %}
-
- {% endif %}
- {% if FEED_ALL_ATOM %}
-
- {% endif %}
- {% if FEED_ALL_RSS %}
-
- {% endif %}
- {% if FEED_ATOM %}
-
- {% endif %}
- {% if FEED_RSS %}
-
- {% endif %}
- {% if CATEGORY_FEED_ATOM and category %}
-
- {% endif %}
- {% if CATEGORY_FEED_RSS and category %}
-
- {% endif %}
- {% if TAG_FEED_ATOM and tag %}
-
- {% endif %}
- {% if TAG_FEED_RSS and tag %}
-
- {% endif %}
-
-
-
-
- {% endblock head %}
-
-
-
-
-
-
-
-
-
- {% if SITESUBTITLE %}
- {{ SITESUBTITLE }}
- {% endif %}
-
-
-
-
-
-
- {% if DISPLAY_PAGES_ON_MENU %}
-
- {% endif %}
- {% for title, link in MENUITEMS %}
- {{ title }}
- {% endfor %}
-
- {% for title, link in LINKS %}
-
{{ title }}
- {% endfor %}
-
- {% if DISPLAY_CATEGORIES_ON_MENU %}
-
- {% for cat, null in categories %}
-
{{ cat}}
- {% endfor %}
-
- {% endif %}
-
-
-
-
-
-
-
-
-
- {% block content %}
- {% endblock %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/templates/categories.html b/templates/categories.html
deleted file mode 100644
index 0f298e8c..00000000
--- a/templates/categories.html
+++ /dev/null
@@ -1,14 +0,0 @@
-{% extends "base.html" %}
-
-{% block title %}Categories - {{ SITENAME|striptags }}{% endblock %}
-
-{% block content %}
-
-
Blog categories
-
- {% for category, articles in categories|sort %}
- {{ category }} ({{ articles|count }})
- {% endfor %}
-
-
-{% endblock %}
diff --git a/templates/category.html b/templates/category.html
deleted file mode 100644
index 8edc3bc1..00000000
--- a/templates/category.html
+++ /dev/null
@@ -1,7 +0,0 @@
-{% extends "index.html" %}
-
-{% block title %}{{ category }} - {{ SITENAME|striptags }}{% endblock %}
-
-{% block content_title %}
-Tales from the {{ category }} department
-{% endblock %}
diff --git a/templates/index.html b/templates/index.html
deleted file mode 100644
index dcdd4cbc..00000000
--- a/templates/index.html
+++ /dev/null
@@ -1,27 +0,0 @@
-{% extends "base.html" %}
-{% block content %}
- {% block content_title %}
- Recent blog posts
- {% endblock %}
-
-
- {% for article in articles_page.object_list %}
-
-
-
- {{ article.locale_date }}
-
-
-
-
-
- {% endfor %}
-
-
- {% if articles_page.has_other_pages() %}
- {% include 'pagination.html' %}
- {% endif %}
-{% endblock content %}
diff --git a/templates/page.html b/templates/page.html
deleted file mode 100644
index fbc1985d..00000000
--- a/templates/page.html
+++ /dev/null
@@ -1,16 +0,0 @@
-{% extends "base.html" %}
-{% block html_lang %}{{ page.lang }}{% endblock %}
-{% block title %}{{ page.title|striptags }} - {{ SITENAME|striptags }} {%endblock%}
-
-{% block head %}
- {{ super() }}
-{% endblock %}
-
-{% block content %}
-
-
-
{{ page.title }}
- {{ page.content }}
-
-
-{% endblock %}
diff --git a/templates/pagination.html b/templates/pagination.html
deleted file mode 100644
index 933904be..00000000
--- a/templates/pagination.html
+++ /dev/null
@@ -1,60 +0,0 @@
-{% if DEFAULT_PAGINATION %}
-{% set first_page = articles_paginator.page(1) %}
-{% set last_page = articles_paginator.page(articles_paginator.num_pages) %}
-
- {% if articles_page.has_previous() %}
-
-
-
- «
-
-
- {% else %}
-
-
- {% endif %}
-
-
-
- {{ articles_page.number }}
-
-
- {% if articles_page.has_next() %}
-
-
- {{ articles_page.next_page_number() }}
-
-
-
- …
-
-
-
- {{ articles_paginator.num_pages }}
-
-
-
-
- »
-
-
- {% endif %}
-
-
-{% endif %}
diff --git a/templates/tag.html b/templates/tag.html
deleted file mode 100644
index 0696ceb2..00000000
--- a/templates/tag.html
+++ /dev/null
@@ -1,7 +0,0 @@
-{% extends "index.html" %}
-
-{% block title %} {{ tag|capitalize }} - {{ SITENAME|striptags }}{% endblock %}
-
-{% block content_title %}
-Tales tagged with {{ tag }}
-{% endblock %}
diff --git a/templates/tags.html b/templates/tags.html
deleted file mode 100644
index 3280842f..00000000
--- a/templates/tags.html
+++ /dev/null
@@ -1,26 +0,0 @@
-{% extends "base.html" %}
-
-{% block title %}Tags - {{ SITENAME|striptags }}{% endblock %}
-
-{% block content %}
- Article tags
-
-
- {% for tag in tag_cloud %}
-
-
- {{ tag.0 }}
-
- {% if TAG_CLOUD_BADGE %}
- {{ tag.2 }}
- {% endif %}
-
- {% endfor %}
-
-
-
-
-
-
-
-{% endblock %}
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 00000000..69281e30
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,44 @@
+[tox]
+envlist = py{3.5,3.6,3.7,3.8},docs,flake8
+
+[testenv]
+basepython =
+ py3.5: python3.5
+ py3.6: python3.6
+ py3.7: python3.7
+ py3.8: python3.8
+passenv = *
+usedevelop=True
+deps =
+ -rrequirements/test.pip
+
+commands =
+ {envpython} --version
+ pytest -s --cov=pelican pelican
+
+[testenv:docs]
+basepython = python3.6
+deps =
+ -rrequirements/docs.pip
+changedir = docs
+commands =
+ sphinx-build -W -b html -d {envtmpdir}/doctrees . _build/html
+
+[pytest]
+filterwarnings =
+ default::DeprecationWarning
+ error:.*:Warning:pelican
+addopts = -n auto -r a
+
+[flake8]
+application-import-names = pelican
+import-order-style = cryptography
+max-line-length = 88
+
+[testenv:flake8]
+basepython = python3.6
+deps =
+ -rrequirements/style.pip
+commands =
+ flake8 --version
+ flake8 pelican